大家在看java设计模式之 代理模式这篇文章的时候, 可以发现动态代理无非就是以下四个步骤,我们完全可以自己模拟实现。因为java的class文件格式是公开的,只要最终生成的class格式正确并且可以加载到JVM中我们就可以正常使用啦。
本质上,动态代理是在程序运行过程中创建生成一个类并且将它加载到JVM中,通过上面的实现步骤,他是把额外的代码(spring中叫切面)植入到被代理类(方法)中以后合成一个类。与静态代理的实现是一样的.
下面这段代码就是聚合模式实现的静态代理:
public interface HelloWorld {
void sayHello(String name);
}
public class HelloWorldImpl implements HelloWorld {
@Override
public void sayHello(String name) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是RealSubject。 Hello " + name);
}
}
/**
* Created by liubenlong on 2017/2/4.
* 聚合模式实现静态代理
*/
public class HelloWorldProxy implements HelloWorld {
private HelloWorldImpl helloWorld;
public HelloWorldProxy(HelloWorldImpl helloWorld){
this.helloWorld = helloWorld;
}
@Override
public void sayHello(String name) {
System.out.println("before log...");
helloWorld.sayHello(name);
System.out.println("end log...。该方法总共执行时间是10毫秒。");
}
}
测试代码:
HelloWorldProxy helloWorldProxy = new HelloWorldProxy(new HelloWorldImpl());
helloWorldProxy.sayHello("zhangsan");
结果:
before log...
我是RealSubject。 Hello zhangsan
end log...。该方法总共执行时间是10毫秒。
JavaCompiler
必须jdk6及以上
假设我们已经组装好了代码(其实就是上面那段静态代理的代码),我们就可以将其加载到JVM中来使用了。
这里说的组装代码的过程在JDK中是使用反射来完成的。
package com.lbl.proxy;
import org.apache.commons.io.FileUtils;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
public class Proxy {
private static final String RT = "\r\n";
public static Object newProxyInstance(Class classObj) throws Exception{
//声明一段源码
String sourceCode =
"package com.lbl.proxy;" + RT +
"/**" + RT +
" * Created by liubenlong on 2017/2/4." + RT +
"* 聚合模式实现静态代理" + RT +
"*/" + RT +
"public class $Proxy1 implements HelloWorld {" + RT +
" private HelloWorld helloWorld;" + RT +
" public $Proxy1(HelloWorld helloWorld){" + RT +
" this.helloWorld = helloWorld;" + RT +
" }" + RT +
" @Override" + RT +
" public void sayHello(String name) {" + RT +
" System.out.println(\"before log...\");" + RT +
" helloWorld.sayHello(name);" + RT +
" System.out.println(\"end log...。该方法总共执行时间是10毫秒。\");" + RT +
" }" + RT +
"}";
String filename = System.getProperty("user.dir") + "/src/main/java/com/lbl/proxy/$Proxy1.java";
File file = new File(filename);
//使用org.apache.commons.io.FileUtils.writeStringToFile()将源码写入磁盘
//编写到处,可以运行一下程序,可以在当前目录中看到生成的.java文件
FileUtils.writeStringToFile(file,sourceCode);
//获得当前系统中的编译器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
System.out.println(complier.getClass().getName());
//获得文件管理者
StandardJavaFileManager fileMgr = complier.getStandardFileManager(null, null, null);
Iterable its = fileMgr.getJavaFileObjects(filename);
//编译任务
JavaCompiler.CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its);
//开始编译,执行完可在当前目录下看到.class文件
task.call();
fileMgr.close();
//load到内存
URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class cls = urlClassLoader.loadClass("com.lbl.proxy.$Proxy1");
//生成代理类对象
Constructor ct = cls.getConstructor(HelloWorld.class);//找一个参数类型是HelloWorldImpl.class的构造方法
return ct.newInstance(new HelloWorldImpl());
}
}
class test{
public static void main(String[] args) throws Exception {
HelloWorld proxyObj = (HelloWorld) Proxy.newProxyInstance(HelloWorld.class);
proxyObj.sayHello("zhangsan");
}
}
代码的含义上面注释已经写得很清楚了。运行一下,可以看到在项目目录下出现两个类:$Proxy1.java
和$Proxy1.class
。
输出结果:
com.sun.tools.javac.api.JavacTool
before log...
我是RealSubject。 Hello zhangsan
end log...。该方法总共执行时间是10毫秒。
上面的模拟代码中,只能支持实现了
HelloWOrld
接口的类。那么怎么才能实现与接口无关,支持任何接口,并且将植入的代码独立出来呢?也就是把生成代理类的代码独立出来与业务无关,这也满足设计模式中的**低耦合、职责单一
**原则。
要想支持任何接口,即生成代理代码的类与实际的接口无关。这个比较简单,因为我们的类都是实现了接口的,而接口中有哪些方法和参数,都是可以通过反射获取到的,那么我们只需要将其实现的接口传递过去即可。
那么怎么将具体的和业务相关的代理逻辑代码抽离出来呢?前面提到了反射,反射可以通过Method
调用某个方法,那么我们就可以在调用方法前后做一些事情了。
通过上面的简单分析可以发现,关键点在于反射。我们可以把接口和代理实现作为参数传递进去就好啦。
我们要做的就是把接口中定义的方法通过反射的方式获取到,进行生成代码。并且将其内部实现转移到Handler
中去。Handler
类是具体的要植入的代码类。就是把生成的下面这段代码修改掉:
@Override
public void sayHello(String name) {
System.out.println("before log...");
helloWorld.sayHello(name);
System.out.println("end log...。该方法总共执行时间是10毫秒。");
}
代理可以有多种多样,怎么取写代码呢?每当出现多种不同的实现的时候我们就应该考虑到java的多态了。我们把代理抽象出一个接口MyInvocationHandler
,里面只有一个方法,该方法用于对被代理对象的调用。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by liubenlong on 2017/2/4.
*/
public interface MyInvocationHandler {
public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException;
}
我们写两个实现类:日志代理类和计时代理类:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by liubenlong on 2017/2/4.
*/
public class MyLogProxy implements MyInvocationHandler {
@Override
public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException {
System.out.println("aa");
Object invoke = method.invoke(o, params);
System.out.println("bb");
return invoke;
}
}
package com.lbl.proxy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by liubenlong on 2017/2/4.
*/
public class MyTimeProxy implements MyInvocationHandler {
@Override
public Object invoke(Method method, Object o, Object ... params) throws InvocationTargetException, IllegalAccessException{
long begin = System.currentTimeMillis();
Object invoke = method.invoke(o, params);
System.out.println(method.getName() + " 执行总共耗时:"+(System.currentTimeMillis() - begin) + "毫秒。");
return invoke;
}
}
修改后的代理生成类:
package com.lbl.proxy;
import org.apache.commons.io.FileUtils;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.atomic.AtomicLong;
/**
*
*/
public class Proxy2 {
private static final String RT = "\r\n";
public static final AtomicLong proxyNum = new AtomicLong(0);
/**
* @param interfaceObj 参数接口
* @return
* @throws Exception
*/
public static Object newProxyInstance(Class interfaceObj, Object o, MyInvocationHandler handler) throws Exception{
Method[] methods = interfaceObj.getMethods();
StringBuilder b = new StringBuilder();
for (Method method : methods) {
int parameterCount = method.getParameterCount();
Class<?>[] parameterTypes = method.getParameterTypes();
StringBuilder paramStr = new StringBuilder();
StringBuilder paramClasses = new StringBuilder();
for(int i = 0 ; i < parameterCount ; i ++){
paramStr.append(parameterTypes[i].getName()).append(" ").append("arg").append(i).append(",");
paramClasses.append(parameterTypes[i].getName()).append(".class,");
}
b.append(" @Override" + RT )
.append(" public void "+method.getName()+"(" + paramStr.substring(0, paramStr.length()-1) + ") {" + RT)//这里我们假设所有方法都没有参数
.append("try {")
.append(" Method method = HelloWorld.class.getMethod(\""+method.getName()+"\", "+paramClasses.substring(0, paramClasses.length()-1)+");")
.append(" handler.invoke(method, obj, arg0);" + RT )
.append("} catch (Exception e) {e.printStackTrace();}")
.append(" }" + RT)
;
}
long num = proxyNum.getAndIncrement();
//声明一段源码
String sourceCode =
"package com.lbl.proxy;" + RT +
"import java.lang.reflect.Method;" + RT +
"/**" + RT +
" * Created by liubenlong on 2017/2/4." + RT +
"* 聚合模式实现静态代理" + RT +
"*/" + RT +
"public class $Proxy"+num+" implements " + interfaceObj.getName() + " {" + RT +
" private "+interfaceObj.getName()+" obj;" + RT +
" private MyInvocationHandler handler;" + RT +
" public $Proxy" + num +"("+interfaceObj.getName()+" obj, MyInvocationHandler handler){" + RT +
" this.obj = obj;" + RT +
" this.handler = handler;" + RT +
" }" + RT +
b.toString() +
"}";
String filename = System.getProperty("user.dir") + "/src/main/java/com/lbl/proxy/$Proxy" + num + ".java";
File file = new File(filename);
//使用org.apache.commons.io.FileUtils.writeStringToFile()将源码写入磁盘
//编写到处,可以运行一下程序,可以在当前目录中看到生成的.java文件
//并不是一定要生成文件,如果可以直接生成二进制代码load到JVM中,则不必要那么麻烦生成文件了
FileUtils.writeStringToFile(file,sourceCode);
//获得当前系统中的编译器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
System.out.println(complier.getClass().getName());
//获得文件管理者
StandardJavaFileManager fileMgr = complier.getStandardFileManager(null, null, null);
Iterable its = fileMgr.getJavaFileObjects(filename);
//编译任务
JavaCompiler.CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its);
//开始编译,执行完可在当前目录下看到.class文件
task.call();
fileMgr.close();
//load到内存
URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class cls = urlClassLoader.loadClass("com.lbl.proxy.$Proxy" + num);
//生成代理类对象
Constructor ct = cls.getConstructor(interfaceObj, MyInvocationHandler.class);//找一个参数类型是HelloWorldImpl.class的构造方法
return ct.newInstance(o, handler);
}
}
class test2{
public static void main(String[] args) throws Exception {
HelloWorld proxyObj = (HelloWorld) Proxy2.newProxyInstance(HelloWorld.class, new HelloWorldImpl(), new MyTimeProxy());
proxyObj.sayHello("zhangsan");
}
}
运行一下,我们查看一下输出结果:
com.sun.tools.javac.api.JavacTool
我是RealSubject。 Hello zhangsan
sayHello 执行总共耗时:1502毫秒。
然后看一下代码生成的代理类
import java.lang.reflect.Method;
/**
* Created by liubenlong on 2017/2/4.
* 聚合模式实现静态代理
*/
public class $Proxy0 implements com.lbl.proxy.HelloWorld {
private com.lbl.proxy.HelloWorld obj;
private MyInvocationHandler handler;
public $Proxy0(com.lbl.proxy.HelloWorld obj, MyInvocationHandler handler){
this.obj = obj;
this.handler = handler;
}
@Override
public void sayHello(java.lang.String arg0) {
try {
//最关键的两行代码
Method method = HelloWorld.class.getMethod("sayHello", java.lang.String.class);
handler.invoke(method, obj, arg0);
} catch (Exception e) {
e.printStackTrace();
}
}
}
到此为止,代理类可以永远不需要改变了。
每当需要新的代理需求,则编写一个实现了MyInvocationHandler
的类就行,然后调用也相当的方便,只需要修改对应的这一行代码的参数即可:HelloWorld proxyObj = (HelloWorld) Proxy2.newProxyInstance(HelloWorld.class, new HelloWorldImpl(), new MyTimeProxy());