上一篇文章我们讲了静态代理的实现方式,并比较了聚合实现静态代理和继承实现效果的不同。今天我们来逐步实现动态代理,并模仿 JDK 动态代理的实现。
-
什么是动态代理?
我们首先考虑,之前我们是通过编写新的类实现与被代理对象同样的接口最终来达到目的的,这样对于每一个被代理对象,我们都要写一个新的代理类,这样的方式肯定不好,如果有这样一个类,只要我们传进去被代理对象,它能自动帮助我们生成代理对象,那不就好了吗? Client 客户端的要求如下:
Tank tank = new Tank() ; Moveable tankTimeProxy = (Moveable)Proxy.newInstance(tank) ;
-
动态代理所需要的步骤:
- 既然要求自动帮助我们生成被代理对象,必须要解决的是:生成被代理类,编译被代理类,将该类 load 到内存中,生成代理对象。所以我们先以上一篇的时间代理为例子,来解决这个问题:
public class Proxy { public static Moveable newInstance(Moveable m) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { // 要生成的代理类,以字符串的形式写出 String str = "package dynamicProxy ;" + "public class TankTimeProxy implements Moveable {\n" + " public TankTimeProxy(Moveable m) {\n" + " this.m = m;\n" + " }\n" + "\n" + " private Moveable m ;\n" + "\n" + " @Override\n" + " public void move() throws InterruptedException {\n" + " long startTime = System.currentTimeMillis() ;\n" + " m.move();\n" + " long endTime = System.currentTimeMillis() ;\n" + " System.out.println(\"time : \" + (endTime - startTime));\n" + " }\n" + "}\n" ; // 将定义好的被代理类写入文件中 // System.getProperty("user.dir") 为获取当前项目的路径 String fileName = System.getProperty("user.dir") + "/src/dynamicProxy/TankTimeProxy.java" ; File file = new File(fileName) ; FileWriter fw = new FileWriter(file) ; fw.write(str); fw.flush(); fw.close(); // 编译被代理类,这里的实现是才用的 JDK6 之后的版本所提供的编译器 JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler() ; // 该编译器的代码需要交由 StandardJavaFileManager 来管理 StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(null,null,null) ; // 获取所要编译的单元 Iterable units = standardJavaFileManager.getJavaFileObjects(fileName) ; // 获取编译任务 JavaCompiler.CompilationTask task = javaCompiler.getTask(null,standardJavaFileManager,null,null,null,units) ; // 执行编译任务 task.call() ; standardJavaFileManager.close(); // 将编译好的代理类 load 到内存中并创建代理对象 // 获取我们编译好的 class 文件路径 URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src/")} ; // 获取 classLoader URLClassLoader ucl = new URLClassLoader(urls) ; // laod 到内存中 Class c = ucl.loadClass("dynamicProxy.TankTimeProxy") ; // 获取参数为 Moveable 的构造器 Constructor constructor = c.getConstructor(Moveable.class) ; // 实例化代理对象 Moveable proxy = (Moveable) constructor.newInstance(new Tank()); return proxy ; } }
通过以上代码,我们就解决了系统生成代理对象所需要的步骤。那么问题来了,现在我们只能实现静态的代理,如何实现动态代理呢?
- 接着我们先来进行动态代理不同的接口的实现。这里要考虑的问题应该是,对于被代理的对象,我们首先要获取其所有的方法,并对其所有的方法实现代理,所以就要修改一部分代理类的代码了。
- 首先是方法体的定义,应该改为接收被代理对象的接口类,即改:
为 :public static Moveable newInstance(Moveable m)
public static Object newInstance(Class inface)
- 获取该接口中所有的方法:(这里涉及到 java 的反射机制)
Method[] methods = Method[] methods = inface.getMethods() ;
- 拼接代理类代码的方法字符串:
这里也许很多人会有疑问,String methodStr ="" ; for(Method m : methods){ methodStr += " @Override\n" + " public void " + m.getName() + "() {\n" + " try{\n" + " Method md = " + inface.getName() + ".class.getMethod(\""+ m.getName() +"\");\n" + " handler.invoke(this,md) ;\n" + " } catch (Exception e){\n" + " e.printStackTrace() ;\n" + " }\n" + " }" ; }
Method md = " + inface.getName() + ".class.getMethod(\""+ m.getName() +"\");\n"
这里为什么不能用Method md = m
来代替呢?其实这里涉及到内外层代码的嵌套问题。 methodStr 是为了生成代理类的字符串,其包含的 method 是内层代码,要获取其方法,就要通过传进去的接口及方法名来获取,而 m 是我们的生成代码,是外层代码。详细的区别,大家可以具体去代码里面尝试一下,看看生成的代理类的区别就应该可以看出来了。- 修改生成代码 str ,加入方法字符串,并修改实现的接口:
String str = "package dynamicProxy ;\n" + "import java.lang.reflect.Method; \n" + "public class TankTimeProxy implements " + inface.getName() + " {\n" + " public TankTimeProxy(Moveable m) {\n" + " this.m = m;\n" + " }\n" + "\n" + " private " + inface.getName + " m ;\n" + "\n" + methodStr + "}\n" ;
- 修改构造器的参数和返回的对象:
Constructor constructor = c.getConstructor(inface) ; Object proxy = constructor.newInstance(new Tank());
到了这里,我们就完成了能对所有的接口的所有方法进行代理。但还有一个问题,就是代理的逻辑已经被写死了,所以下一步我们要完成的就是实现能够自定义代理逻辑的动态代理了。