前言
Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的。本文就自己写一个Proxy类出来,功能和java.lang.reflect.Proxy一样,传入接口、代理内容,生成代理。
抛砖引玉吧,个人觉得自己写一些JDK里面的那些类挺好的,写一遍和看一遍真的是两个不同的概念,写一遍既加深了对于这些类的理解、提升了自己的写代码水平,也可以在写完之后对比一下自己的实现有哪些写得不好、又有哪些没考虑到的地方,这样可以显著地提高自己,像我就自己写过JDK里面主要的集合类、工具类、String里面常用方法等。
本文的代码基础来源于马士兵Proxy的视频(顺便说一句,个人觉得马士兵的视频讲得比较拖拉,但是关于一些原理性、偏底层的东西讲得还蛮好的),一共分三个版本。可能有人觉得,人家视频上的内容拿过来写个文章,有意思吗?真不是,我是这么认为的:
1、把别人的东西变成自己的东西是一个过程,尽管代码是基于马士兵Proxy的视频的,但是所有的代码都是在自己这里手打、运行通过并自己充分理解了的,把别人的东西不加思考地复制黏贴没有意义,但是把别人的知识变成自己的理解并分享我觉得是一件好事
2、代码尽管基于马士兵Proxy的基础上,但在这个基础上也是做了自己的优化过的
动态代理的实现应用到的技术
1、动态编译技术,可以使用Java自带的JavaCompiler类,也可以使用CGLIB、ASM等字节码增强技术,Java的动态代理包括Spring的内部实现貌似用的都是这个
2、反射,包括对于类.class和getClass()方法的理解,Method类、Constructor类的理解
3、IO流,主要就是字符输出流FileWriter
4、对于ClassLoader的理解
基础类
先把基础类定义在这儿,首先是一个HelloWorld接口:
public interface HelloWorld { void print(); }
HelloWorld接口的实现类:
public class HelloWorldImpl implements HelloWorld { public void print() { System.out.println("Hello World"); } }
为这个接口写一个简单的静态代理类:
public class StaticProxy implements HelloWorld { private HelloWorld helloWorld; public StaticProxy(HelloWorld helloWorld) { this.helloWorld = helloWorld; } public void print() { System.out.println("Before Hello World!"); helloWorld.print(); System.out.println("After Hello World!"); } }
版本1:为一个静态代理动态生成一个代理类
我们知道如果用静态代理的话,那么每个接口都要为之写一个.java的代理类,这样就可能造成代理类无限膨胀,如果可以让Java帮我们自动生成一个就好了,不过还真的可以,看下第一个版本的代码:
1 public class ProxyVersion_0 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance() throws Exception 6 { 7 String src = "package com.xrq.proxy; " + 8 "public class StaticProxy implements HelloWorld " + 9 "{ " + 10 " HelloWorld helloWorld; " + 11 " public StaticProxy(HelloWorld helloWorld) " + 12 " { " + 13 " this.helloWorld = helloWorld; " + 14 " } " + 15 " public void print() " + 16 " { " + 17 " System.out.println("Before Hello World!"); " + 18 " helloWorld.print(); " + 19 " System.out.println("After Hello World!"); " + 20 " } " + 21 "}"; 22 23 /** 生成一段Java代码 */ 24 String fileDir = System.getProperty("user.dir"); 25 String fileName = fileDir + "\src\com\xrq\proxy\StaticProxy.java"; 26 File javaFile = new File(fileName); 27 Writer writer = new FileWriter(javaFile); 28 writer.write(src); 29 writer.close(); 30 31 /** 动态编译这段Java代码,生成.class文件 */ 32 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 33 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null); 34 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName); 35 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter); 36 ct.call(); 37 sjfm.close(); 38 39 /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */ 40 URL[] urls = new URL[] {(new URL("file:\" + System.getProperty("user.dir") + "\src"))}; 41 URLClassLoader ul = new URLClassLoader(urls); 42 Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy"); 43 44 /** 利用反射将c实例化出来 */ 45 Constructor<?> constructor = c.getConstructor(HelloWorld.class); 46 HelloWorld helloWorldImpl = new HelloWorldImpl(); 47 HelloWorld helloWorld = (HelloWorld)constructor.newInstance(helloWorldImpl); 48 49 /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */ 50 File classFile = new File(fileDir + "\src\com\xrq\proxy\StaticProxy.class"); 51 javaFile.delete(); 52 classFile.delete(); 53 54 return helloWorld; 55 } 56 }
每一步的注释都在上面了,解释一下大致思路:
1、我们在另外一个类里面自己拼一段静态代理的代码的字符串
2、为这个字符串生成一个.java文件,并放在我们工程的某个目录下面,因为是.java文件,所以在src下
3、利用JavaCompiler类动态编译这段.java代码使之被编译成一个.class文件,JavaCompiler不熟悉没关系,知道就好了
4、因为在src下生成编译之后的.java文件,而默认的ClassLoader只能加载CLASSPATH下的.class文件,所以用URLClassLoader
5、由于代理类只有一个带参数的构造方法,所以要用java.lang.reflect.Constructor
6、最后把生成的StaticProxy.class文件删除(最好生成的StaticProxy.java也删除,这里没删除,是因为StaticProxy是生成的一个重要的中间类,功能都在它这儿,所以不删,出了错都要靠看这个类来定位问题的),这样代理的中间内容都没了,把反射newInstance()出来的内容返回出去就大功告成了
可以自己看一下生成的StaticProxy.java对不对,写一段代码测试一下:
public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); HelloWorld helloWorld = (HelloWorld)ProxyVersion_0.newProxyInstance(); System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println(); }
结果为:
动态生成代理耗时:387ms Before Hello World! Hello World After Hello World!
没有问题。可能有些人运行会报错"Exception in thread "main" java.lang.ClassNotFoundException: com.xrq.proxy.StaticProxy",没关系,那是因为虽然你的src目录下生成了StaticProxy.class,但没有出来,点击src文件夹,再按F5(或者右键,点击Refresh也行)刷新一下就可以了
版本二:为指定接口生成代理类
版本一已经实现了动态生成一个代理的.class文件了,算是成功的第一步,接下来要做进一步的改进。版本一只可以为固定的一个接口生成代理,现在改进成,传入某个接口的java.lang.Class对象,可以为这个接口及里面的方法都生成代理内容,代码这么写:
1 public class ProxyVersion_1 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance(Class<?> interfaces) throws Exception 6 { 7 Method[] methods = interfaces.getMethods(); 8 9 StringBuilder sb = new StringBuilder(700); 10 11 sb.append("package com.xrq.proxy; "); 12 sb.append("public class StaticProxy implements " + interfaces.getSimpleName() + " "); 13 sb.append("{ "); 14 sb.append(" " + interfaces.getSimpleName() + " interfaces; "); 15 sb.append(" public StaticProxy(" + interfaces.getSimpleName() + " interfaces) "); 16 sb.append(" { "); 17 sb.append(" this.interfaces = interfaces; "); 18 sb.append(" } "); 19 for (Method m : methods) 20 { 21 sb.append(" public " + m.getReturnType() + " " + m.getName() + "() "); 22 sb.append(" { "); 23 sb.append(" System.out.println("Before Hello World!"); "); 24 sb.append(" interfaces." + m.getName() + "(); "); 25 sb.append(" System.out.println("After Hello World!"); "); 26 sb.append(" } "); 27 } 28 sb.append("}"); 29 30 /** 生成一段Java代码 */ 31 String fileDir = System.getProperty("user.dir"); 32 String fileName = fileDir + "\src\com\xrq\proxy\StaticProxy.java"; 33 File javaFile = new File(fileName); 34 Writer writer = new FileWriter(javaFile); 35 writer.write(sb.toString()); 36 writer.close(); 37 38 /** 动态编译这段Java代码,生成.class文件 */ 39 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 40 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null); 41 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName); 42 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter); 43 ct.call(); 44 sjfm.close(); 45 46 /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */ 47 URL[] urls = new URL[] {(new URL("file:\" + System.getProperty("user.dir") + "\src"))}; 48 URLClassLoader ul = new URLClassLoader(urls); 49 Class<?> c = ul.loadClass("com.xrq.proxy.StaticProxy"); 50 51 /** 利用反射将c实例化出来 */ 52 Constructor<?> constructor = c.getConstructor(HelloWorld.class); 53 HelloWorld helloWorldImpl = new HelloWorldImpl(); 54 Object obj = constructor.newInstance(helloWorldImpl); 55 56 /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */ 57 /*File classFile = new File(fileDir + "\src\com\xrq\proxy\StaticProxy.class"); 58 javaFile.delete(); 59 classFile.delete();*/ 60 61 return obj; 62 } 63 }
看到下面都没有变化,变化的地方就是在生成StaticProxy.java的地方,通过反射获取接口及方法的信息,这个版本的改进应该很好理解,写一段代码测试一下:
public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); HelloWorld helloWorld = (HelloWorld)ProxyVersion_1.newProxyInstance(HelloWorld.class); System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println(); }
运行结果为:
动态生成代理耗时:389ms Before Hello World! Hello World After Hello World!
也没有问题
版本三:让代理内容可复用
接下来要到最后一个版本了,版本二解决的问题是可以为任何接口生成代理,那最后一个版本要解决的问题自然是可以为任何接口生成任何代理的问题了,首先定义一个接口InvocationHandler,这么起名字是因为JDK提供的代理实例处理程序的接口也是InvocationHandler:
public interface InvocationHandler { void invoke(Object proxy, Method method) throws Exception; }
所以我们的Proxy类也要修改了,改为:
1 public class ProxyVersion_2 implements Serializable 2 { 3 private static final long serialVersionUID = 1L; 4 5 public static Object newProxyInstance(Class<?> interfaces, InvocationHandler h) throws Exception 6 { 7 Method[] methods = interfaces.getMethods(); 8 StringBuilder sb = new StringBuilder(1024); 9 10 sb.append("package com.xrq.proxy; "); 11 sb.append("import java.lang.reflect.Method; "); 12 sb.append("public class $Proxy1 implements " + interfaces.getSimpleName() + " "); 13 sb.append("{ "); 14 sb.append(" InvocationHandler h; "); 15 sb.append(" public $Proxy1(InvocationHandler h) "); 16 sb.append(" { "); 17 sb.append(" this.h = h; "); 18 sb.append(" } "); 19 for (Method m : methods) 20 { 21 sb.append(" public " + m.getReturnType() + " " + m.getName() + "() "); 22 sb.append(" { "); 23 sb.append(" try "); 24 sb.append(" { "); 25 sb.append(" Method md = " + interfaces.getName() + ".class.getMethod("" + m.getName() + ""); "); 26 sb.append(" h.invoke(this, md); "); 27 sb.append(" } "); 28 sb.append(" catch (Exception e) "); 29 sb.append(" { "); 30 sb.append(" e.printStackTrace(); "); 31 sb.append(" } "); 32 sb.append(" } "); 33 } 34 sb.append("}"); 35 36 /** 生成一段Java代码 */ 37 String fileDir = System.getProperty("user.dir"); 38 String fileName = fileDir + "\src\com\xrq\proxy\$Proxy1.java"; 39 File javaFile = new File(fileName); 40 Writer writer = new FileWriter(javaFile); 41 writer.write(sb.toString()); 42 writer.close(); 43 44 /** 动态编译这段Java代码,生成.class文件 */ 45 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 46 StandardJavaFileManager sjfm = compiler.getStandardFileManager(null, null, null); 47 Iterable<? extends JavaFileObject> iter = sjfm.getJavaFileObjects(fileName); 48 CompilationTask ct = compiler.getTask(null, sjfm, null, null, null, iter); 49 ct.call(); 50 sjfm.close(); 51 52 /** 将生成的.class文件载入内存,默认的ClassLoader只能载入CLASSPATH下的.class文件 */ 53 URL[] urls = new URL[] {(new URL("file:\" + System.getProperty("user.dir") + "\src"))}; 54 URLClassLoader ul = new URLClassLoader(urls); 55 Class<?> c = Class.forName("com.xrq.proxy.$Proxy1", false, ul); 56 57 /** 利用反射将c实例化出来 */ 58 Constructor<?> constructor = c.getConstructor(InvocationHandler.class); 59 Object obj = constructor.newInstance(h); 60 61 /** 使用完毕删除生成的代理.java文件和.class文件,这样就看不到动态生成的内容了 */ 62 File classFile = new File(fileDir + "\src\com\xrq\proxy\$Proxy1.class"); 63 javaFile.delete(); 64 classFile.delete(); 65 66 return obj; 67 } 68 }
最明显的变化,代理的名字变了,从StaticProxy变成了$Proxy1,因为JDK也是这么命名的,用过代理的应该有印象。这个改进中拼接$Proxy1的.java文件是一个难点,不过我觉得可以不用纠结在这里,关注重点,看一下生成的$Proxy1.java的内容是什么:
public class $Proxy1 implements HelloWorld { InvocationHandler h; public $Proxy1(InvocationHandler h) { this.h = h; } public void print() { try { Method md = com.xrq.proxy.HelloWorld.class.getMethod("print"); h.invoke(this, md); } catch (Exception e) { e.printStackTrace(); } } }
看到,我们把对于待生成代理的接口方法的调用,变成了对于InvocationHandler接口实现类的invoke方法的调用(这就是动态代理最关键的一点),并传入了待调用的接口方法,这样不就实现了我们的要求了吗?我们InvocationHandler接口的实现类写invoke方法的具体实现,传入的第二个参数md.invoke就是调用被代理对象的方法,在这个方法前后都是代理内容,想加什么加什么,不就实现了动态代理了?所以,我们看一个InvocationHandler实现类的写法:
public class HelloInvocationHandler implements InvocationHandler { private Object obj; public HelloInvocationHandler(Object obj) { this.obj = obj; } public void invoke(Object proxy, Method method) { System.out.println("Before Hello World!"); try { method.invoke(obj, new Object[]{}); } catch (Exception e) { e.printStackTrace(); } System.out.println("After Hello World!"); } }
写个main函数测试一下:
public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); HelloWorld helloWorldImpl = new HelloWorldImpl(); InvocationHandler ih = new HelloInvocationHandler(helloWorldImpl); HelloWorld helloWorld = (HelloWorld)ProxyVersion_2.newProxyInstance(HelloWorld.class, ih); System.out.println("动态生成代理耗时:" + (System.currentTimeMillis() - start) + "ms"); helloWorld.print(); System.out.println(); }
运行结果为:
动态生成代理耗时:351ms Before Hello World! Hello World After Hello World!
没有问题。
后记
虽然我们自己写了Proxy,但是JDK绝对不会用这种方式实现,原因无他,就是太慢。看到三个版本的代码,运行时间都在300ms以上,效率如此低的实现,如何能给开发者使用?我拿JDK提供的Proxy和InvocationHandler自己写了一个简单的动态代理,耗时基本只在5ms左右。所以,文章的内容仅供学习、研究,知识点很多,如果能把这篇文章里面的东西都弄懂,对于个人水平、对于Java很多知识点的理解,绝对是一个非常大的提高。