帶你學 Java 技術之動態代理機制

這一次我們來講解一個有意思的機制:動態代理。學習下Java裡為什麼出現這樣一個機制,什麼場合下會使用這個機制。

靜態代理

常規的代理模式有以下三個部分組成: 功能接口

interface IFunction {
void doAThing();
}

功能提供者

class FunctionProvider implement IFunction {
public void doAThing {
System.out.print("do A");
}
}

功能代理者

class Proxy implement IFunction {
private FunctionProvider provider;
Proxy(FunctionProvider provider) {
this.provider = provider;
}
public void doAThing {
provider.doAThing();
}
}

前兩者就是普通的接口和實現類,而第三個就是所謂的代理類。對於使用者而言,他會讓代理類去完成某件任務,並不關心這件任務具體的跑腿者。

這就是靜態代理,好處是方便調整變換具體實現類,而使用者不會受到任何影響。

不過這種方式也存在弊端:比如有多個接口需要進行代理,那麼就要為每一個功能提供者創建對應的一個代理類,那就會越來越龐大。而且,所謂的“靜態”代理,意味著必須提前知道被代理的委託類。

通過下面一個例子來說明下:

統計函數耗時--靜態代理實現

現在希望通過一個代理類,對我感興趣的方法進行耗時統計,利用靜態代理有如下實現:

interface IAFunc {
void doA();
}
interface IBFunc {
void doB();
}

class TimeConsumeProxy implement IAFunc, IBFunc {
private AFunc a;
private BFunc b;
public(AFunc a, BFunc b) {
this.a = a;
this.b = b;
}
void doA() {
long start = System.currentMillions();
a.doA();

System.out.println("耗時:" + (System.currentMillions() - start));
}
void doB() {

long start = System.currentMillions();
b.doB();

System.out.println("耗時:" + (System.currentMillions() - start));
}
}

弊端很明顯,如果接口越多,每新增一個函數都要去修改這個TimeConsumeProxy代理類:把委託類對象傳進去,實現接口,在函數執行前後統計耗時。

這種方式顯然不是可持續性的,下面來看下使用動態代理的實現方式,進行對比。

動態代理

動態代理的核心思想是通過Java Proxy類,為傳入進來的任意對象動態生成一個代理對象,這個代理對象默認實現了原始對象的所有接口。

還是通過統計函數耗時例子來說明更加直接。

統計函數耗時--動態代理實現

interface IAFunc {
void doA();
}
interface IBFunc {
void doB();
}

class A implement IAFunc { ... }
class B implement IBFunc { ... }

class TimeConsumeProxy implements InvocationHandler {
private Object realObject;

public Object bind(Object realObject) {
this.realObject = realObject;
Object proxyObject = Proxy.newInstance(
realObject.getClass().getClassLoader(),
realObject.getClass().getInterfaces(),
this
);
return proxyObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentMillions();
Object result = method.invoke(target, args);
System.out.println("耗時:" + (System.currentMillions() - start));
return result;
}
}

具體使用時:

public static void main(String[] args) {
A a = new A();
IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();
B b = new B();
IBFunc bProxy = (IBFunc) new TimeConsumeProxy().bind(b);
bProxy.doB();
}

這裡最大的區別就是:代理類和委託類互相透明獨立,邏輯沒有任何耦合,在運行時才綁定在一起。這也就是靜態代理與動態代理最大的不同,帶來的好處就是:無論委託類有多少個,代理類不受到任何影響,而且在編譯時無需知道具體委託類。

回到動態代理本身,上面代碼中最重要的就是:

Object proxyObject = Proxy.newInstance(
realObject.getClass().getClassLoader(),
realObject.getClass().getInterfaces(),
this
);

通過Proxy工具,把真實委託類轉換成了一個代理類,最開始提到了一個代理模式的三要素:功能接口、功能提供者、功能代理者;在這裡對應的就是:realObject.getClass().getInterfaces(),realObject,TimeConsumeProxy。

其實動態代理並不複雜,通過一個Proxy工具,為委託類的接口自動生成一個代理對象,後續的函數調用都通過這個代理對象進行發起,最終會執行到InvocationHandler#invoke方法,在這個方法裡除了調用真實委託類對應的方法,還可以做一些其他自定義的邏輯,比如上面的運行耗時統計等。

探索動態代理實現機制

好了,上面我們已經把動態代理的基本用法及為什麼要用動態代理進行了講解,很多文章到這裡也差不多了,不過我們還準備進一步探索一下給感興趣的讀者。

拋出幾個問題:

  1. 上面生成的代理對象Object proxyObject究竟是個什麼東西?為什麼它可以轉型成IAFunc,還能調用doA()方法?
  2. 這個proxyObject是怎麼生成出來的?它是一個class嗎?

下面我先給出答案,再一步步探究這個答案是如何來的。

問題一: proxyObject究竟是個什麼 -> 動態生成的$Proxy0.class文件

在調用Proxy.newInstance後,Java最終會為委託類A生成一個真實的class文件:$Proxy0.class,而proxyObject就是這個class的一個實例。

猜一下,這個$Proxy0.class類長什麼樣呢,包含了什麼方法呢?回看下剛剛的代碼:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();

推理下,顯然這個$Proxy0.class實現了 IAFunc 接口,同時它內部也實現了doA()方法,而且重點是:這個doA()方法在運行時會執行到TimeConsumeProxy#invoke()方法裡。

重點來了!下面我們來看下這個$Proxy0.class文件,把它放進IDE反編譯下,可以看到如下內容,來驗證下剛剛的猜想:

final class $Proxy0 extends Proxy implements IAFunc {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {

// 省略
}
public final void doA() throws {
try {
// 劃重點
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
// 省略
}
public final int hashCode() throws {
// 省略
}
static {
try {
// 劃重點
m3 = Class.forName("proxy.IAFunc").getMethod("doA", new Class[0]);
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

沒錯,剛剛的猜想都中了!實現了IAFunc接口和doA()方法,不過,doA()裡是什麼鬼?

super.h.invoke(this, m3, (Object[])null);

回看下,TimeConsumeProxy裡面的invoke方法,它的函數簽名是啥?

public Object invoke(Object proxy, Method method, Object[] args);

沒錯,doA()裡做的就是調用TimeConsumeProxy#invoke()方法。

那麼也就是說,下面這段代碼執行流程如下:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();
  1. 基於傳入的委託類A,生成一個$Proxy0.class文件;
  2. 創建一個$Proxy0.class對象,轉型為IAFunc接口;
  3. 調用aProxy.doA()時,自動調用TimeConsumeProxy內部的invoke方法。

問題二:proxyObject 是怎麼一步步生成出來的 -> $Proxy0.class文件生成流程

剛剛從末尾看了結果,現在我們回到代碼的起始端來看:

Object proxyObject = Proxy.newInstance(
realObject.getClass().getClassLoader(),
realObject.getClass().getInterfaces(),
this
);

準備好,開始發車讀源碼了。我會截取重要的代碼並加上註釋。

先看Proxy.newInstance():

public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h) {
//複製要代理的接口

final Class>[] intfs = interfaces.clone();
//重點:生成 $Proxy0.class 文件並通過 ClassLoader 加載進來
Class> cl = getProxyClass0(loader, intfs);
//對$Proxy0.class生成一個實例,就是`proxyObject`
final Constructor> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});
}

再來看 getProxyClass0 的具體實現:ProxyClassFactory工廠類:

@Override
public Class> apply(ClassLoader loader, Class>[] interfaces) {
// 參數為ClassLoader和要代理的接口
Map<class>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
// 1. 驗證ClassLoader和接口有效性
for (Class> intf : interfaces) {
// 驗證classLoader正確性
Class> interfaceClass = Class.forName(intf.getName(), false, loader);
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
// 驗證傳入的接口class有效
if (!interfaceClass.isInterface()) { ... }
// 驗證接口是否重複
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { ... }
}
// 2. 創建包名及類名 $Proxy0.class
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
// 3. 創建class字節碼內容
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
// 4. 基於字節碼和類名,生成Class>對象
return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
}
/<class>

再看下第三步生成class內容 ProxyGenerator.generateProxyClass:

// 添加 hashCode equals toString方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
// 添加委託類的接口實現
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]);
}
}
// 添加構造函數
methods.add(this.generateConstructor());

這裡構造好了類的內容:添加必要的函數,實現接口,構造函數等,下面就是要寫入上一步看到的 $Proxy0.class 了。

 ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xCAFEBABE);
...
dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
...
return bout.toByteArray();

到這裡就生成了第一步看到的$Proxy0.class文件了,完成閉環,講解完成!

動態代理小結

通過上面的講解可以看出,動態代理可以隨時為任意的委託類進行代理,並可以在InvocationHandler#invoke拿到運行時的信息,並可以做一些切面處理。

在動態代理背後,其實是為一個委託類動態生成了一個$Proxy0.class的代理類,該代理類會實現委託類的接口,並把接口調用轉發到InvocationHandler#invoke上,最終調用到真實委託類的對應方法。

動態代理機制把委託類和代理類進行了隔離,提高了擴展性。

關注我:私信回覆“555”獲取往期Java高級架構資料、源碼、筆記、視頻Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術往期架構視頻


分享到:


相關文章: