经过前面几篇的分析,咱们知道代理类是经过Proxy类的ProxyClassFactory工厂生成的,这个工厂类会去调用ProxyGenerator类的generateProxyClass()方法来生成代理类的字节码。ProxyGenerator这个类存放在sun.misc包下,咱们能够经过OpenJDK源码来找到这个类,该类的generateProxyClass()静态方法的核心内容就是去调用generateClassFile()实例方法来生成Class文件。咱们直接来看generateClassFile()这个方法内部作了些什么。java
1 private byte[] generateClassFile() { 2 //第一步, 将全部的方法组装成ProxyMethod对象 3 //首先为代理类生成toString, hashCode, equals等代理方法 4 addProxyMethod(hashCodeMethod, Object.class); 5 addProxyMethod(equalsMethod, Object.class); 6 addProxyMethod(toStringMethod, Object.class); 7 //遍历每个接口的每个方法, 而且为其生成ProxyMethod对象 8 for (int i = 0; i < interfaces.length; i++) { 9 Method[] methods = interfaces[i].getMethods(); 10 for (int j = 0; j < methods.length; j++) { 11 addProxyMethod(methods[j], interfaces[i]); 12 } 13 } 14 //对于具备相同签名的代理方法, 检验方法的返回值是否兼容 15 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { 16 checkReturnTypes(sigmethods); 17 } 18 19 //第二步, 组装要生成的class文件的全部的字段信息和方法信息 20 try { 21 //添加构造器方法 22 methods.add(generateConstructor()); 23 //遍历缓存中的代理方法 24 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { 25 for (ProxyMethod pm : sigmethods) { 26 //添加代理类的静态字段, 例如:private static Method m1; 27 fields.add(new FieldInfo(pm.methodFieldName, 28 "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC)); 29 //添加代理类的代理方法 30 methods.add(pm.generateMethod()); 31 } 32 } 33 //添加代理类的静态字段初始化方法 34 methods.add(generateStaticInitializer()); 35 } catch (IOException e) { 36 throw new InternalError("unexpected I/O Exception"); 37 } 38 39 //验证方法和字段集合不能大于65535 40 if (methods.size() > 65535) { 41 throw new IllegalArgumentException("method limit exceeded"); 42 } 43 if (fields.size() > 65535) { 44 throw new IllegalArgumentException("field limit exceeded"); 45 } 46 47 //第三步, 写入最终的class文件 48 //验证常量池中存在代理类的全限定名 49 cp.getClass(dotToSlash(className)); 50 //验证常量池中存在代理类父类的全限定名, 父类名为:"java/lang/reflect/Proxy" 51 cp.getClass(superclassName); 52 //验证常量池存在代理类接口的全限定名 53 for (int i = 0; i < interfaces.length; i++) { 54 cp.getClass(dotToSlash(interfaces[i].getName())); 55 } 56 //接下来要开始写入文件了,设置常量池只读 57 cp.setReadOnly(); 58 59 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 60 DataOutputStream dout = new DataOutputStream(bout); 61 try { 62 //1.写入魔数 63 dout.writeInt(0xCAFEBABE); 64 //2.写入次版本号 65 dout.writeShort(CLASSFILE_MINOR_VERSION); 66 //3.写入主版本号 67 dout.writeShort(CLASSFILE_MAJOR_VERSION); 68 //4.写入常量池 69 cp.write(dout); 70 //5.写入访问修饰符 71 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER); 72 //6.写入类索引 73 dout.writeShort(cp.getClass(dotToSlash(className))); 74 //7.写入父类索引, 生成的代理类都继承自Proxy 75 dout.writeShort(cp.getClass(superclassName)); 76 //8.写入接口计数值 77 dout.writeShort(interfaces.length); 78 //9.写入接口集合 79 for (int i = 0; i < interfaces.length; i++) { 80 dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName()))); 81 } 82 //10.写入字段计数值 83 dout.writeShort(fields.size()); 84 //11.写入字段集合 85 for (FieldInfo f : fields) { 86 f.write(dout); 87 } 88 //12.写入方法计数值 89 dout.writeShort(methods.size()); 90 //13.写入方法集合 91 for (MethodInfo m : methods) { 92 m.write(dout); 93 } 94 //14.写入属性计数值, 代理类class文件没有属性因此为0 95 dout.writeShort(0); 96 } catch (IOException e) { 97 throw new InternalError("unexpected I/O Exception"); 98 } 99 //转换成二进制数组输出 100 return bout.toByteArray(); 101 }
能够看到generateClassFile()方法是按照Class文件结构进行动态拼接的。什么是Class文件呢?在这里咱们先要说明下,咱们平时编写的Java文件是以.java结尾的,在编写好了以后经过编译器进行编译会生成.class文件,这个.class文件就是Class文件。Java程序的执行只依赖于Class文件,和Java文件是没有关系的。这个Class文件描述了一个类的信息,当咱们须要使用到一个类时,Java虚拟机就会提早去加载这个类的Class文件并进行初始化和相关的检验工做,Java虚拟机可以保证在你使用到这个类以前就会完成这些工做,咱们只须要安心的去使用它就行了,而没必要关心Java虚拟机是怎样加载它的。固然,Class文件并不必定非得经过编译Java文件而来,你甚至能够直接经过文本编辑器来编写Class文件。在这里,JDK动态代理就是经过程序来动态生成Class文件的。咱们再次回到上面的代码中,能够看到,生成Class文件主要分为三步:数组
第一步:收集全部要生成的代理方法,将其包装成ProxyMethod对象并注册到Map集合中。缓存
第二步:收集全部要为Class文件生成的字段信息和方法信息。编辑器
第三步:完成了上面的工做后,开始组装Class文件。ide
咱们知道一个类的核心部分就是它的字段和方法。咱们重点聚焦第二步,看看它为代理类生成了哪些字段和方法。在第二步中,按顺序作了下面四件事。学习
1.为代理类生成一个带参构造器,传入InvocationHandler实例的引用并调用父类的带参构造器。测试
2.遍历代理方法Map集合,为每一个代理方法生成对应的Method类型静态域,并将其添加到fields集合中。this
3.遍历代理方法Map集合,为每一个代理方法生成对应的MethodInfo对象,并将其添加到methods集合中。spa
4.为代理类生成静态初始化方法,该静态初始化方法主要是将每一个代理方法的引用赋值给对应的静态字段。代理
经过以上分析,咱们能够大体知道JDK动态代理最终会为咱们生成以下结构的代理类:
1 public class Proxy0 extends Proxy implements UserDao { 2 3 //第一步, 生成构造器 4 protected Proxy0(InvocationHandler h) { 5 super(h); 6 } 7 8 //第二步, 生成静态域 9 private static Method m1; //hashCode方法 10 private static Method m2; //equals方法 11 private static Method m3; //toString方法 12 private static Method m4; //... 13 14 //第三步, 生成代理方法 15 @Override 16 public int hashCode() { 17 try { 18 return (int) h.invoke(this, m1, null); 19 } catch (Throwable e) { 20 throw new UndeclaredThrowableException(e); 21 } 22 } 23 24 @Override 25 public boolean equals(Object obj) { 26 try { 27 Object[] args = new Object[] {obj}; 28 return (boolean) h.invoke(this, m2, args); 29 } catch (Throwable e) { 30 throw new UndeclaredThrowableException(e); 31 } 32 } 33 34 @Override 35 public String toString() { 36 try { 37 return (String) h.invoke(this, m3, null); 38 } catch (Throwable e) { 39 throw new UndeclaredThrowableException(e); 40 } 41 } 42 43 @Override 44 public void save(User user) { 45 try { 46 //构造参数数组, 若是有多个参数日后面添加就好了 47 Object[] args = new Object[] {user}; 48 h.invoke(this, m4, args); 49 } catch (Throwable e) { 50 throw new UndeclaredThrowableException(e); 51 } 52 } 53 54 //第四步, 生成静态初始化方法 55 static { 56 try { 57 Class c1 = Class.forName(Object.class.getName()); 58 Class c2 = Class.forName(UserDao.class.getName()); 59 m1 = c1.getMethod("hashCode", null); 60 m2 = c1.getMethod("equals", new Class[]{Object.class}); 61 m3 = c1.getMethod("toString", null); 62 m4 = c2.getMethod("save", new Class[]{User.class}); 63 //... 64 } catch (Exception e) { 65 e.printStackTrace(); 66 } 67 } 68 69 }
至此,通过层层分析,深刻探究JDK源码,咱们还原了动态生成的代理类的原本面目,以前心中存在的一些疑问也随之获得了很好的解释
1.代理类默认继承Porxy类,由于Java中只支持单继承,因此JDK动态代理只能去实现接口。
2.代理方法都会去调用InvocationHandler的invoke()方法,所以咱们须要重写InvocationHandler的invoke()方法。
3.调用invoke()方法时会传入代理实例自己,目标方法和目标方法参数。解释了invoke()方法的参数是怎样来的。
使用刚刚构造出来的Proxy0做为代理类再次进行测试,能够看到最终的结果与使用JDK动态生成的代理类的效果是同样的。再次验证了咱们的分析是可靠且准确的。至此,JDK动态代理系列文章宣告结束。经过本系列的分析,笔者解决了心中长久以来的疑惑,相信读者们对JDK动态代理的理解也更深了一步。可是纸上得来终觉浅,想要更好的掌握JDK动态代理技术,读者可参照本系列文章自行查阅JDK源码,也可与笔者交流学习心得,指出笔者分析不当的地方,共同窗习,共同进步。