通過前面幾篇的分析,我們知道代理類是通過Proxy類的ProxyClassFactory工廠生成的,這個工廠類會去調用ProxyGenerator類的generateProxyClass()方法來生成代理類的字節碼。ProxyGenerator這個類存放在sun.misc包下,我們可以通過OpenJDK源碼來找到這個類,該類的generateProxyClass()靜態方法的核心內容就是去調用generateClassFile()實例方法來生成Class文件。我們直接來看generateClassFile()這個方法內部做了些什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
private byte [] generateClassFile() { //第一步, 將所有的方法組裝成ProxyMethod對象 //首先為代理類生成toString, hashCode, equals等代理方法 addProxyMethod(hashCodeMethod, Object. class ); addProxyMethod(equalsMethod, Object. class ); addProxyMethod(toStringMethod, Object. class ); //遍歷每一個接口的每一個方法, 并且為其生成ProxyMethod對象 for ( int i = 0 ; i < interfaces.length; i++) { Method[] methods = interfaces[i].getMethods(); for ( int j = 0 ; j < methods.length; j++) { addProxyMethod(methods[j], interfaces[i]); } } //對于具有相同簽名的代理方法, 檢驗方法的返回值是否兼容 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { checkReturnTypes(sigmethods); } //第二步, 組裝要生成的class文件的所有的字段信息和方法信息 try { //添加構造器方法 methods.add(generateConstructor()); //遍歷緩存中的代理方法 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyMethod pm : sigmethods) { //添加代理類的靜態字段, 例如:private static Method m1; fields.add( new FieldInfo(pm.methodFieldName, "Ljava/lang/reflect/Method;" , ACC_PRIVATE | ACC_STATIC)); //添加代理類的代理方法 methods.add(pm.generateMethod()); } } //添加代理類的靜態字段初始化方法 methods.add(generateStaticInitializer()); } catch (IOException e) { throw new InternalError( "unexpected I/O Exception" ); } //驗證方法和字段集合不能大于65535 if (methods.size() > 65535 ) { throw new IllegalArgumentException( "method limit exceeded" ); } if (fields.size() > 65535 ) { throw new IllegalArgumentException( "field limit exceeded" ); } //第三步, 寫入最終的class文件 //驗證常量池中存在代理類的全限定名 cp.getClass(dotToSlash(className)); //驗證常量池中存在代理類父類的全限定名, 父類名為:"java/lang/reflect/Proxy" cp.getClass(superclassName); //驗證常量池存在代理類接口的全限定名 for ( int i = 0 ; i < interfaces.length; i++) { cp.getClass(dotToSlash(interfaces[i].getName())); } //接下來要開始寫入文件了,設置常量池只讀 cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout); try { //1.寫入魔數 dout.writeInt( 0xCAFEBABE ); //2.寫入次版本號 dout.writeShort(CLASSFILE_MINOR_VERSION); //3.寫入主版本號 dout.writeShort(CLASSFILE_MAJOR_VERSION); //4.寫入常量池 cp.write(dout); //5.寫入訪問修飾符 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER); //6.寫入類索引 dout.writeShort(cp.getClass(dotToSlash(className))); //7.寫入父類索引, 生成的代理類都繼承自Proxy dout.writeShort(cp.getClass(superclassName)); //8.寫入接口計數值 dout.writeShort(interfaces.length); //9.寫入接口集合 for ( int i = 0 ; i < interfaces.length; i++) { dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName()))); } //10.寫入字段計數值 dout.writeShort(fields.size()); //11.寫入字段集合 for (FieldInfo f : fields) { f.write(dout); } //12.寫入方法計數值 dout.writeShort(methods.size()); //13.寫入方法集合 for (MethodInfo m : methods) { m.write(dout); } //14.寫入屬性計數值, 代理類class文件沒有屬性所以為0 dout.writeShort( 0 ); } catch (IOException e) { throw new InternalError( "unexpected I/O Exception" ); } //轉換成二進制數組輸出 return bout.toByteArray(); } |
可以看到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文件。
我們知道一個類的核心部分就是它的字段和方法。我們重點聚焦第二步,看看它為代理類生成了哪些字段和方法。在第二步中,按順序做了下面四件事。
1.為代理類生成一個帶參構造器,傳入InvocationHandler實例的引用并調用父類的帶參構造器。
2.遍歷代理方法Map集合,為每個代理方法生成對應的Method類型靜態域,并將其添加到fields集合中。
3.遍歷代理方法Map集合,為每個代理方法生成對應的MethodInfo對象,并將其添加到methods集合中。
4.為代理類生成靜態初始化方法,該靜態初始化方法主要是將每個代理方法的引用賦值給對應的靜態字段。
通過以上分析,我們可以大致知道JDK動態代理最終會為我們生成如下結構的代理類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
public class Proxy0 extends Proxy implements UserDao { //第一步, 生成構造器 protected Proxy0(InvocationHandler h) { super (h); } //第二步, 生成靜態域 private static Method m1; //hashCode方法 private static Method m2; //equals方法 private static Method m3; //toString方法 private static Method m4; //... //第三步, 生成代理方法 @Override public int hashCode() { try { return ( int ) h.invoke( this , m1, null ); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public boolean equals(Object obj) { try { Object[] args = new Object[] {obj}; return ( boolean ) h.invoke( this , m2, args); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public String toString() { try { return (String) h.invoke( this , m3, null ); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public void save(User user) { try { //構造參數數組, 如果有多個參數往后面添加就行了 Object[] args = new Object[] {user}; h.invoke( this , m4, args); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } //第四步, 生成靜態初始化方法 static { try { Class c1 = Class.forName(Object. class .getName()); Class c2 = Class.forName(UserDao. class .getName()); m1 = c1.getMethod( "hashCode" , null ); m2 = c1.getMethod( "equals" , new Class[]{Object. class }); m3 = c1.getMethod( "toString" , null ); m4 = c2.getMethod( "save" , new Class[]{User. class }); //... } catch (Exception e) { e.printStackTrace(); } } } |
至此,經過層層分析,深入探究JDK源碼,我們還原了動態生成的代理類的本來面目,之前心中存在的一些疑問也隨之得到了很好的解釋
1.代理類默認繼承Porxy類,因為Java中只支持單繼承,所以JDK動態代理只能去實現接口。
2.代理方法都會去調用InvocationHandler的invoke()方法,因此我們需要重寫InvocationHandler的invoke()方法。
3.調用invoke()方法時會傳入代理實例本身,目標方法和目標方法參數。解釋了invoke()方法的參數是怎樣來的。
使用剛剛構造出來的Proxy0作為代理類再次進行測試,可以看到最終的結果與使用JDK動態生成的代理類的效果是一樣的。再次驗證了我們的分析是可靠且準確的。至此,JDK動態代理系列文章宣告結束。通過本系列的分析,筆者解決了心中長久以來的疑惑,相信讀者們對JDK動態代理的理解也更深了一步。但是紙上得來終覺淺,想要更好的掌握JDK動態代理技術,讀者可參照本系列文章自行查閱JDK源碼,也可與筆者交流學習心得,指出筆者分析不當的地方,共同學習,共同進步。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/liuyun1995/p/8144706.html