Java中的兩種動態代理分別是什麼?它們是jdk proxy和cglib,今天來討論Java中的兩種動態代理最後產生的代理類別應該是什麼樣的以及如何實現代理? apache php mysql
參加過Java面試的夥伴可能都會知道,面試官很喜歡問Spring AOP怎麼實現的之類的問題,因此我結合問題整理了一下寫出這篇文章。關於AOP和代理模式的概念這裡並不做詳解,就直接說主題了,即AOP的實現方式:動態代理。與靜態代理程式對比,動態代理程式是在runtime動態產生Java代理程式類,由代理程式類別完成對具體方法的封裝,實作AOP的功能。
這些是我個人的整理和想法,和真正的jdk,cglib的產生的結果可能不太一樣,但從原理上來講是基本一致的。
文章的最後也會探討如何自己實作一個簡單的動態代理,並提供我自己實作的簡單版本,當然僅供參考。
這是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
是不会去产生源代码的,而是直接生成类的原始数据,它具体是怎么实现我们暂时不讨论,我们目前只需要关心这个类是什么样的,以及它实现代理的原理。
这是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
方法进行转接的,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
方法的签名都差不多,第一个参数是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資料的。
前面了解了代理類別是什麼,接下來就要介紹如何產生代理類,我結合資料整理了兩個方案:
第一種方法是動態產生ProxyClass
原始碼,然後動態編譯,就能得到Class了。這裡就需要利用反射,加上一系列字串拼接,產生原始碼。如果你充分理解代理類別應該長什麼樣,其實並不是很難做到。那如何動態編譯呢?你可以使用JOOR,這是一個封裝了javax.tools.JavaCompiler
的函式庫,幫助你方便地實作動態編譯Java原始碼。我試著寫了一個Demo,純粹是實驗性質的。而且它有個重大問題,我不知道如何修改它編譯使用的classpath,在預設情況下它無法引用到你自己定義的任何類,因為它們不在編譯的classpath裡,編譯就不會通過,這實際上就使得這個程式碼產生器沒有任何卵用。 。 。我強行透過修改System.setProperty
的classpath
來加入我的class路徑繞開了這個問題,然而這顯然不是解決根本問題的方法。
第二種方法比較直接,就是產生類別的字節碼。這也是cglib
使用的方法,它封裝了ASM,這是一個可以用來直接操縱Class資料的函式庫,透過它你就可以任意產生或修改你想要的Class,當然這需要你對虛擬機器的字節碼比較了解,才能玩得通這種比較黑科技的套路。這裡我也寫了一個Demo,也純粹是實驗而已,有興趣的童鞋也可以自己試試看。寫字節碼還挺酸爽的,它類似彙編但其實比彙編容易的多。它不像彙編那樣一會兒暫存器一會兒記憶體位址,一會兒堆一會兒棧,各種變數和位址繞來繞去。字節碼的執行方式是很清晰的,變數都儲存在本地變數表裡,而棧只是用來做函數調用,所以非常直觀。
相關文章:
#相關影片:
#以上是Java兩種動態代理 jdk和cglib產生的代理類型以及如何實現的的詳細內容。更多資訊請關注PHP中文網其他相關文章!