發現Java面試很喜歡問Spring AOP怎麼實現的之類的問題,所以寫一篇文章來整理一下。關於AOP和代理模式的概念這裡並不做贅述,而是直奔主題,即AOP的實現方式:動態代理。與靜態代理對比,動態代理是在runtime動態生成Java代理類,由代理類完成對具體方法的封裝,實現AOP的功能。
本文將分析Java中兩種動態代理的實現方式,jdk proxy
和cglib
,比較它們的異同。本文並不會過多地分析jdk和cglib的原始碼去探究底層的實現細節,而只關注最後生成的代理類應該是什麼樣的,如何實現代理。只是我個人的整理和思考,和真正的jdk,cglib的產生的結果可能不盡相同,但從原理上來講是一致的。
文章的最後也會探討如何自己實現一個簡單的動態代理,並提供我自己實現的簡單版本,當然僅供參考。
JDK Proxy
這是Java反射包java.lang.reflect
提供的動態代理的方式,這種代理方式是完全基於介面的。這裡先給出一個簡單的例子。
定義介面:
interface ifc {
int add(int, int);
}
然後是介面ifc
的實現類Real
:
class Real implements ifc {
@Override
public int add(int x, int y) {
return x + y;
}
Real
就是我們需要代理的類,比如我們希望在呼叫add
的前後列印一些log,這實際上就是AOP了。我們需要最終產生一個代理類,實現同樣的介面ifc
,執行Real.add
的功能,但需要增加一行新的列印語句。這一切對使用者是透明的,使用者只需要關心介面的呼叫。為了能在Real.add
的周圍新增額外程式碼,動態代理都是通過一種類似方法攔截器的東西來實現的,在Java Proxy裡這就是InvocationHandler
.
class Handler implements InvocationHandler {
private final Real real;
public Handler(Real real) {
this.real = real;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
System.out.println("=== BEFORE ===");
Object re = method.invoke(real, args);
System.out.println("=== AFTER ===");
return re;
}
}
這裡最關鍵的就是invoke
方法,實際上代理類的add
方法,以及其它方法(如果介面還定義了其它方法),最終都只是呼叫這個Handler
的invoke
方法,由你來具體定義在invoke裡需要做什麼,通常就是呼叫真正實體類Real
的方法,這裡就是add
,以及額外的AOP行為(列印 BEFORE 和 AFTER)。所以可想而知,代理類裡必然是有一個InvocationHandler
的例項的,所有的介面方法呼叫都會由這個handler例項來代理。
所以我們應該能大概刻畫出這個代理類的模樣:
public ProxyClass implements ifc {
private static Method mAdd;
private InvocationHandler handler;
static {
Class clazz = Class.forName("ifc");
mAdd = clazz.getMethod("add", int.class, int.class);
}
@Override
public int add(int x, int y) {
return (Integer)handler.invoke(this, mAdd, new Object[] {x, y});
}
}
這個版本非常簡單,但已足夠實現我們的要求。我們來觀察這個類,首先毋庸置疑它實現了ifc
介面,這是代理模式的根本。它的add
方法直接呼叫InvocationHandler
例項的invoke
方法,傳入三個引數,第一個是代理類本身this指標,第二個是add
方法的反射類,第三個是引數列表。所以在invoke
方法裡,使用者就能自由定義它的行為實現AOP,所有這一切的橋樑就是InvocationHandler
,它完成方法的攔截與代理。
代理模式一般要求代理類中有一個真正類(被代理類)的例項,在這裡也就是Real
的例項,這樣代理類才能去呼叫Real
中原本的add
方法。那Real
在哪裡呢?答案也是在InvocationHandler
裡。這與標準的代理模式相比,似乎多了一層巢狀,不過這並沒有關係,只要這個代理的鏈條能夠搭建起來,它就符合代理模式的要求。
注意到這裡add
方法的反射例項mAdd
的初始化方式,我們使用靜態塊static {...}
來完成,只會被設定一次,並且不會有多執行緒問題。當然你也可以用懶載入等方式,不過就得考慮併發的安全性。
最後看一下JDK Proxy
的具體使用:
Handler handler = new Handler(new Real());
ifc p = (ifc)Proxy.newProxyInstance(ifc.class.getClassLoader(),
new Class[] {ifc},
handler);
p.add(1, 2);
方法newProxyInstance
就會動態產生代理類,並且返回給我們一個例項,實現了ifc
介面。這個方法需要三個引數,第一個ClassLoader並不重要;第二個是介面列表,即這個代理類需要實現那些介面,因為JDK的Proxy是完全基於介面的,它封裝的是介面的方法而不是實體類;第三個引數就是InvocationHandler
的例項,它會被放置在最終的代理類中,作為方法攔截和代理的橋樑。注意到這裡的handler
包含了一個Real
例項,這在上面已經說過是代理模式的必然要求。
總結一下JDK Proxy
的原理,首先它是完全面向介面的,其實這才是符合代理模式的標準定義的。我們有兩個類,被代理類Real
和需要動態生成的代理類ProxyClass
,都實現了介面ifc
。類ProxyClass
需要攔截介面ifc
上所有方法的呼叫,並且最終轉發到實體類Real
上,這兩者之間的橋樑就是方法攔截器InvocatioHandler
的invoke
方法。
上面的例子裡我給出類ProxyClass
的原始碼,當然實際上JDK Proxy
是不會去產生原始碼的,而是直接生成類的原始資料,它具體是怎麼實現我們暫時不討論,我們目前只需要關心這個類是什麼樣的,以及它實現代理的原理。
cglib實現動態代理
這是Spring
使用的方式,與JDK Proxy
不同之處在於它不是面向介面的,而是基於類的繼承。這似乎是有點違背代理模式的標準格式,不過這沒有關係,所謂的代理模式只是一種思想而不是嚴格的規範。我們直接看它是如何使用的。
現在沒有介面,我們直接有實體類:
class Real {
public int add(int x, int y) {
return x + y;
}
}
類似於InvocationHandler
,這裡cglib
直接使用一個叫MethodInterceptor
的類,顧名思義。
public class Interceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj,
Method method,
Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("=== BEFORE ===");
Object re = proxy.invokeSuper(obj, args);
System.out.println("=== AFTER ===");
return re;
}
}
使用方法:
public static void main(String[] args) {
Enhancer eh = new Enhancer();
eh.setSuperclass(Real.class);
eh.setCallback(new Interceptor());
Real r = (Real)eh.create();
int result = r.add(1, 2);
}
如果你仔細和JDK Proxy
比較,會發現它們其實是類似的:
- 首先
JDK Proxy
提供interface列表,而cglib
提供superclass供代理類繼承,本質上都是一樣的,就是提供這個代理類的簽名,也就是對外表現為什麼型別。 - 然後是一個方法攔截器,
JDK Proxy
裡是InvocationHandler
,而cglib
裡一般就是MethodInterceptor
,所有被代理的方法的呼叫是通過它們的invoke
和intercept
方法進行轉接的,AOP的邏輯也是在這一層實現。
它們不同之處上面已經說了,就在於cglib
生成的動態代理類是直接繼承原始類的,所以我們這裡也可以大概刻畫出這個代理類長什麼樣子:
public ProxyClass extends Real {
private static Method mAdd;
private static MethodProxy mAddProxy;
private MethodInterceptor interceptor;
static {
Class clazz = Class.forName("ifc");
mAdd = clazz.getMethod("add", int.class, int.class);
// Some logic to generate mAddProxy.
// ...
}
@Override
public int add(int x, int y) {
return (Integer)interceptor.invoke(
this, mAdd, new Object[] {x, y}, mAddProxy);
}
}
因為直接繼承了Real
,那自然就包含了Real
的所有public方法,都通過interceptor.invoke
進行攔截代理。這其實和上面JDK Proxy
的原理是類似的,連invoke
和intercept
方法的簽名都差不多,第一個引數是this指標代理類本身,第二個引數是方法的反射,第三個引數是方法呼叫的引數列表。唯一不同的是,這裡多出一個MethodProxy
,它是做什麼用的?
如果你仔細看這裡invoke
方法內部的寫法,當使用者想呼叫原始類(這裡是Real
)定義的方法時,它必須使用:
Object re = proxy.invokeSuper(obj, args);
這裡就用到了那個MethodProxy
,那我們為什麼不直接寫:
Object re = method.invoke(obj, args);
答案當然是不可以,你不妨試一下,程式會進入一個無限遞迴呼叫。這裡的原因恰恰就是因為代理類是繼承了原始類的,obj
指向的就是代理類物件的例項,所以如果你對它使用method.invoke
,由於多型性,就會又去呼叫代理類的add
方法,繼而又進入invoke
方法,進入一個無限遞迴:
obj.add() {
interceptor.invoke() {
obj.add() {
interceptor.invoke() {
...
}
}
}
}
那我如何才能在interceptor.invoke()
裡去呼叫基類Real
的add
方法呢?當然通常做法是super.add()
,然而這是在MethodInterceptor
的方法裡,而且這裡的method呼叫必須通過反射完成,你並不能在語法層面上做到這一點。所以cglib
封裝了一個類叫MethodProxy
幫助你,這也是為什麼那個方法的名字叫invokeSuper
,表明它呼叫的是原始基類的真正方法。它究竟是怎麼辦到的呢?你可以簡單理解為,動態代理類裡會生成這樣一個方法:
int super_add(int x, int y) {
return super.add(x, y);
}
當然你並不知道有這麼一個方法,但invokeSuper
會最終找到這個方法並呼叫,這都是在生成代理類時通過一系列反射的機制實現的,這裡就不細展開了。
小結
以上我對比了JDK Proxy
和cglib
動態代理的使用方法和實現上的區別,它們本質上是類似的,都是提供兩個最重要的東西:
- 介面列表或者基類,定義了代理類(當然也包括原始類)的簽名。
- 一個方法攔截器,完成方法的攔截和代理,是所有呼叫鏈的橋樑。
需要說明的一點是,以上我給出的代理類ProxyClass
的原始碼,僅是參考性的最精簡版本,只是為了說明原理,而不是JDK Proxy
和cglib
真正生成的代理類的樣子,真正的代理類的邏輯要複雜的多,但是原理上基本是一致的。另外之前也說到過,事實上它們也不會生成原始碼,而是直接產生類的位元組碼,例如cglib
是封裝了ASM
來直接生成Class資料的。
如何生成代理類
接下來的部分純粹是實驗性質的。既然知道了代理類長什麼樣,可能還是有人會關心底層究竟如何在runtime動態生成這個類,這裡我個人想了兩種方案。
第一種方法是動態生成ProxyClass
原始碼,然後動態編譯,就能得到Class了。這裡就需要利用反射,加上一系列字串拼接,生成原始碼。如果你充分理解代理類應該長什麼樣,其實並不是很難做到。那如何動態編譯呢?你可以使用JOOR,這是一個封裝了javax.tools.JavaCompiler
的庫,幫助你方便地實現動態編譯Java原始碼。我試著寫了一個Demo,純粹是實驗性質的。而且它有個重大問題,我不知道如何修改它編譯使用的classpath,在預設情況下它無法引用到你自己定義的任何類,因為它們不在編譯的classpath裡,編譯就不會通過,這實際上就使得這個程式碼生成器沒有任何卵用。。。我強行通過修改System.setProperty
的classpath
來新增我的class路徑繞開了這個問題,然而這顯然不是個解決根本問題的方法。
第二種方法更直接,就是生成類的位元組碼。這也是cglib
使用的方法,它封裝了ASM,這是一個可以用來直接操縱Class資料的庫,通過它你就可以任意生成或修改你想要的Class,當然這需要你對虛擬機器的位元組碼比較瞭解,才能玩得通這種比較黑科技的套路。這裡我也寫了一個Demo,也純粹是實驗而已,感興趣的童鞋也可以自己試一下。寫位元組碼還是挺酸爽的,它類似彙編但其實比彙編容易的多。它不像彙編那樣一會兒暫存器一會兒記憶體地址,一會兒堆一會兒棧,各種變數和地址繞來繞去。位元組碼的執行方式是很清晰的,變數都儲存在本地變數表裡,棧只是用來做函式呼叫,所以非常直觀。