JDK動態(tài)代理之ProxyGenerator生成代理類的字節(jié)碼文件解析
通過前面幾篇的分析,我們知道代理類是通過Proxy類的ProxyClassFactory工廠生成的,這個工廠類會去調(diào)用ProxyGenerator類的generateProxyClass()方法來生成代理類的字節(jié)碼。ProxyGenerator這個類存放在sun.misc包下,我們可以通過OpenJDK源碼來找到這個類,該類的generateProxyClass()靜態(tài)方法的核心內(nèi)容就是去調(diào)用generateClassFile()實(shí)例方法來生成Class文件。我們直接來看generateClassFile()這個方法內(nèi)部做了些什么。
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]);
}
}
//對于具有相同簽名的代理方法, 檢驗(yàn)方法的返回值是否兼容
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
//第二步, 組裝要生成的class文件的所有的字段信息和方法信息
try {
//添加構(gòu)造器方法
methods.add(generateConstructor());
//遍歷緩存中的代理方法
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
//添加代理類的靜態(tài)字段, 例如:private static Method m1;
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
//添加代理類的代理方法
methods.add(pm.generateMethod());
}
}
//添加代理類的靜態(tài)字段初始化方法
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
}
//驗(yàn)證方法和字段集合不能大于65535
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
//第三步, 寫入最終的class文件
//驗(yàn)證常量池中存在代理類的全限定名
cp.getClass(dotToSlash(className));
//驗(yàn)證常量池中存在代理類父類的全限定名, 父類名為:"java/lang/reflect/Proxy"
cp.getClass(superclassName);
//驗(yàn)證常量池存在代理類接口的全限定名
for (int i = 0; i < interfaces.length; i++) {
cp.getClass(dotToSlash(interfaces[i].getName()));
}
//接下來要開始寫入文件了,設(shè)置常量池只讀
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
//1.寫入魔數(shù)
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.寫入接口計(jì)數(shù)值
dout.writeShort(interfaces.length);
//9.寫入接口集合
for (int i = 0; i < interfaces.length; i++) {
dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));
}
//10.寫入字段計(jì)數(shù)值
dout.writeShort(fields.size());
//11.寫入字段集合
for (FieldInfo f : fields) {
f.write(dout);
}
//12.寫入方法計(jì)數(shù)值
dout.writeShort(methods.size());
//13.寫入方法集合
for (MethodInfo m : methods) {
m.write(dout);
}
//14.寫入屬性計(jì)數(shù)值, 代理類class文件沒有屬性所以為0
dout.writeShort(0);
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
}
//轉(zhuǎn)換成二進(jìn)制數(shù)組輸出
return bout.toByteArray();
}
可以看到generateClassFile()方法是按照Class文件結(jié)構(gòu)進(jìn)行動態(tài)拼接的。什么是Class文件呢?在這里我們先要說明下,我們平時編寫的Java文件是以.java結(jié)尾的,在編寫好了之后通過編譯器進(jìn)行編譯會生成.class文件,這個.class文件就是Class文件。Java程序的執(zhí)行只依賴于Class文件,和Java文件是沒有關(guān)系的。這個Class文件描述了一個類的信息,當(dāng)我們需要使用到一個類時,Java虛擬機(jī)就會提前去加載這個類的Class文件并進(jìn)行初始化和相關(guān)的檢驗(yàn)工作,Java虛擬機(jī)能夠保證在你使用到這個類之前就會完成這些工作,我們只需要安心的去使用它就好了,而不必關(guān)心Java虛擬機(jī)是怎樣加載它的。當(dāng)然,Class文件并不一定非得通過編譯Java文件而來,你甚至可以直接通過文本編輯器來編寫Class文件。在這里,JDK動態(tài)代理就是通過程序來動態(tài)生成Class文件的。我們再次回到上面的代碼中,可以看到,生成Class文件主要分為三步:
第一步:收集所有要生成的代理方法,將其包裝成ProxyMethod對象并注冊到Map集合中。
第二步:收集所有要為Class文件生成的字段信息和方法信息。
第三步:完成了上面的工作后,開始組裝Class文件。
我們知道一個類的核心部分就是它的字段和方法。我們重點(diǎn)聚焦第二步,看看它為代理類生成了哪些字段和方法。在第二步中,按順序做了下面四件事。
1.為代理類生成一個帶參構(gòu)造器,傳入InvocationHandler實(shí)例的引用并調(diào)用父類的帶參構(gòu)造器。
2.遍歷代理方法Map集合,為每個代理方法生成對應(yīng)的Method類型靜態(tài)域,并將其添加到fields集合中。
3.遍歷代理方法Map集合,為每個代理方法生成對應(yīng)的MethodInfo對象,并將其添加到methods集合中。
4.為代理類生成靜態(tài)初始化方法,該靜態(tài)初始化方法主要是將每個代理方法的引用賦值給對應(yīng)的靜態(tài)字段。
通過以上分析,我們可以大致知道JDK動態(tài)代理最終會為我們生成如下結(jié)構(gòu)的代理類:
public class Proxy0 extends Proxy implements UserDao {
//第一步, 生成構(gòu)造器
protected Proxy0(InvocationHandler h) {
super(h);
}
//第二步, 生成靜態(tài)域
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 {
//構(gòu)造參數(shù)數(shù)組, 如果有多個參數(shù)往后面添加就行了
Object[] args = new Object[] {user};
h.invoke(this, m4, args);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
//第四步, 生成靜態(tài)初始化方法
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();
}
}
}
至此,經(jīng)過層層分析,深入探究JDK源碼,我們還原了動態(tài)生成的代理類的本來面目,之前心中存在的一些疑問也隨之得到了很好的解釋
1.代理類默認(rèn)繼承Porxy類,因?yàn)镴ava中只支持單繼承,所以JDK動態(tài)代理只能去實(shí)現(xiàn)接口。
2.代理方法都會去調(diào)用InvocationHandler的invoke()方法,因此我們需要重寫InvocationHandler的invoke()方法。
3.調(diào)用invoke()方法時會傳入代理實(shí)例本身,目標(biāo)方法和目標(biāo)方法參數(shù)。解釋了invoke()方法的參數(shù)是怎樣來的。

使用剛剛構(gòu)造出來的Proxy0作為代理類再次進(jìn)行測試,可以看到最終的結(jié)果與使用JDK動態(tài)生成的代理類的效果是一樣的。再次驗(yàn)證了我們的分析是可靠且準(zhǔn)確的。至此,JDK動態(tài)代理系列文章宣告結(jié)束。通過本系列的分析,筆者解決了心中長久以來的疑惑,相信讀者們對JDK動態(tài)代理的理解也更深了一步。但是紙上得來終覺淺,想要更好的掌握J(rèn)DK動態(tài)代理技術(shù),讀者可參照本系列文章自行查閱JDK源碼,也可與筆者交流學(xué)習(xí)心得,指出筆者分析不當(dāng)?shù)牡胤?,共同學(xué)習(xí),共同進(jìn)步。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
spring cloud config 配置中心快速實(shí)現(xiàn)過程解析
這篇文章主要介紹了spring cloud config 配置中心快速實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08
開源項(xiàng)目ERM模型轉(zhuǎn)jpa實(shí)體maven插件使用
這篇文章主要為大家介紹了開源項(xiàng)目ERM模型轉(zhuǎn)jpa實(shí)體maven插件的使用說明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
淺談讓@Value更方便的Spring自定義轉(zhuǎn)換類
Spring為大家內(nèi)置了不少開箱即用的轉(zhuǎn)換類,如字符串轉(zhuǎn)數(shù)字、字符串轉(zhuǎn)時間等,但有時候需要使用自定義的屬性,則需要自定義轉(zhuǎn)換類了2021-06-06
SpringBoot整合Ldap的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot整合Ldap的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
基于Java實(shí)現(xiàn)圖形驗(yàn)證碼工具類
這篇文章主要為大家詳細(xì)介紹了如何基于Java實(shí)現(xiàn)圖形驗(yàn)證碼工具類,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11

