發現Java面試很喜歡問Spring AOP怎麼實現的之類的問題,因此寫一篇文章來整理一下。關於AOP和代理模式的概念這裏並不作贅述,而是直奔主題,即AOP的實現方式:動態代理。與靜態代理對比,動態代理是在runtime動態生成Java代理類,由代理類完成對具體方法的封裝,實現AOP的功能。java
本文將分析Java中兩種動態代理的實現方式,jdk proxy
和cglib
,比較它們的異同。本文並不會過多地分析jdk和cglib的源碼去探究底層的實現細節,而只關注最後生成的代理類應該是什麼樣的,如何實現代理。只是我我的的整理和思考,和真正的jdk,cglib的產生的結果可能不盡相同,但從原理上來說是一致的。git
文章的最後也會探討如何本身實現一個簡單的動態代理,並提供我本身實現的簡單版本,固然僅供參考。github
這是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實例來代理。ide
因此咱們應該能大概刻畫出這個代理類的模樣:函數
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
,它完成方法的攔截與代理。this
代理模式通常要求代理類中有一個真正類(被代理類)的實例,在這裏也就是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
是不會去產生源代碼的,而是直接生成類的原始數據,它具體是怎麼實現咱們暫時不討論,咱們目前只須要關心這個類是什麼樣的,以及它實現代理的原理。
這是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,也純粹是實驗而已,感興趣的童鞋也能夠本身試一下。寫字節碼仍是挺酸爽的,它相似彙編但其實比彙編容易的多。它不像彙編那樣一下子寄存器一下子內存地址,一下子堆一下子棧,各類變量和地址繞來繞去。字節碼的執行方式是很清晰的,變量都存儲在本地變量表裏,棧只是用來作函數調用,因此很是直觀。