經過前面幾篇的分析,咱們知道代理類是經過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源碼,也可與筆者交流學習心得,指出筆者分析不當的地方,共同窗習,共同進步。