深入 JDK 動態代理實現
基本使用
沒有花裡胡哨的,就下面的程式碼:
UserService userService = new UserServiceImpl();
UserService o = (UserService) Proxy。newProxyInstance(UserService。class。getClassLoader(), new Class[]{UserService。class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System。out。println(“——before——”);
Object invoke = method。invoke(userService, args);
System。out。println(“——after——”);
return invoke;
}
});
可以看出,關鍵在於代理的建立:Proxy。newProxyInstance。後面我們也以這裡為入口深入。
關鍵步驟解析
生成代理類
生成代理類是動態代理中的最複雜的步驟,而真正生成代理類位元組碼的方法位於:java。lang。reflect。Proxy。ProxyClassFactory#apply。
精簡一下方法,主要有以下幾個步驟:
//前置校驗
interfaceClass = Class。forName(intf。getName(), false, loader);
if(xxx){
throw new IllegalArgumentException(xxx);
}
。。。
//資料準備
int accessFlags = Modifier。PUBLIC | Modifier。FINAL;
。。。
String proxyName = proxyPkg + proxyClassNamePrefix + num;
。。。
//生成代理類 bytes
byte[] proxyClassFile = ProxyGenerator。generateProxyClass(
proxyName, interfaces, accessFlags);
。。。
//載入
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile。length);
生成代理類與載入是動態代理的核心。
組裝 ProxyMethod
Step 1: Assemble ProxyMethod objects for all methods to generate proxy dispatching code for。
這裡只是將收集到的方法元資訊封裝為sun。misc。ProxyGenerator。ProxyMethod 類儲存,並沒有真正的生成 code。
這裡還特地也儲存了了hashCode,equals,toString方法。
addProxyMethod(hashCodeMethod, Object。class);
addProxyMethod(equalsMethod, Object。class);
addProxyMethod(toStringMethod, Object。class);
。。。
sigmethods。add(new ProxyMethod(name, parameterTypes, returnType,
exceptionTypes, fromClass));
組裝 FieldInfo / MethodInfo
Step 2: Assemble FieldInfo and MethodInfo structs for all of fields and methods in the class we are generating。
新增構造方法,並根據上一步組裝好的ProxyMethod新增成員屬性。
在這一步中,會呼叫sun。misc。ProxyGenerator。ProxyMethod#generateMethod方法,為其生成code。
//生成構造方法
this。methods。add(this。generateConstructor());
。。。
//生成成員屬性
fields。add(new FieldInfo(pm。methodFieldName,
“Ljava/lang/reflect/Method;”,
ACC_PRIVATE | ACC_STATIC));
//新增方法
methods。add(pm。generateMethod());
。。。
//新增靜態初始化程式碼塊
methods。add(generateStaticInitializer());
構造最終類
Step 3: Write the final class file。
根據上面幾步收集到的資訊,生成最終 byte 陣列。
// u4 magic;
dout。writeInt(0xCAFEBABE);
// u2 minor_version;
dout。writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout。writeShort(CLASSFILE_MAJOR_VERSION);
cp。write(dout); // (write constant pool)
// u2 access_flags;
dout。writeShort(accessFlags);
// u2 this_class;
dout。writeShort(cp。getClass(dotToSlash(className)));
// u2 super_class;
dout。writeShort(cp。getClass(superclassName));
。。。
最終結果
透過配置 sun。misc。ProxyGenerator。saveGeneratedFiles=true 可以將生成的代理類位元組碼儲存下來。
刪除了我們無需關心 hashCode,toString等方法後,反編譯結果如下:
package com。sun。proxy;
import git。frank。proxy。UserService;
import java。lang。reflect。InvocationHandler;
import java。lang。reflect。Method;
import java。lang。reflect。Proxy;
import java。lang。reflect。UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final String getUserInfo() throws {
try {
return (String)super。h。invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class。forName(“java。lang。Object”)。getMethod(“equals”, Class。forName(“java。lang。Object”));
m2 = Class。forName(“java。lang。Object”)。getMethod(“toString”);
m3 = Class。forName(“git。frank。proxy。UserService”)。getMethod(“getUserInfo”);
m0 = Class。forName(“java。lang。Object”)。getMethod(“hashCode”);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2。getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3。getMessage());
}
}
}
載入代理類
我們以 openJdk 中的程式碼為參考,這裡實際呼叫的方法是:JavaLangAccess#defineClass。
Defines a class with the given name to a class loader。
它真正的程式碼位於 java。lang。System 中,以匿名內部類的形式。
// Allow privileged classes outside of java。lang
private static void setJavaLangAccess() {
SharedSecrets。setJavaLangAccess(new JavaLangAccess() {
。。。
public Class<?> defineClass(ClassLoader loader, String name, byte[] b, ProtectionDomain pd, String source) {
return ClassLoader。defineClass1(loader, name, b, 0, b。length, pd, source);
}
。。。
}
所以,這裡就是使用給定的類載入器去載入剛才生成的class。當被代理方法getUserInfo被呼叫時,便會被代理類轉至傳入的 InvocationHandler#invoke 方法中。
那麼,為什麼在程式碼中沒有直接呼叫 ClassLoader.defineClass1,而是這樣繞了一圈呢?
我們再仔細看看,因為 ClassLoader 中的 defineClass 相關程式碼都是 protected 的呀~,根本沒辦法直接呼叫。
所以 openJdk 才創造了一個 JavaLangAccess 類,專門用於讓外界呼叫 java。lang 包內的內容。
構造物件
構造物件就簡單很多,單純地使用反射呼叫構造方法去建立剛才生成的代理類例項。
使用入參為 InvocationHandler 的構造方法,並傳入使用者編寫的 InvocationHandler 物件。
final Constructor<?> cons = cl。getConstructor(constructorParams);
。。。
cons。newInstance(new Object[]{h})
這裡構造的實際上就是上面生成的 com。sun。proxy。$Proxy0 類例項。
動態代理中的快取機制
使用動態代理時並不是每次都要經過上面那一大串程式碼的,只要同 classLoader + interfaces 相同,都可以使用同一個 proxy 類。
所以 jdk 為動態代理生成的 class 也提供了快取機制,下次使用的時候,直接透過反射建立 proxy 例項就可以,無需再重新生成 clazz 。
這裡的快取主要透過 java。lang。reflect。WeakCache 實現。
使用 key + sub-key 唯一確定一個 value。
其中,key 為傳入的 classLoader,sub-key 為透過 key + 傳入的介面 class 計算得出。
快取構成如下圖:
懶載入
起初 valuesMap 中快取的並不是最中的代理類,而是一個 ProxyClassFactory。
當呼叫 valueFactory。apply(key, parameter) 生成真正的代理類後,會將快取由 factory 物件更換為真正的代理。
if (!valuesMap。replace(subKey, this, cacheValue)) {
throw new AssertionError(“Should not reach here”);
}
完整地獲取快取流程如下: