摘要:與靜態代理對比,動態代理是在動態生成代理類,由代理類完成對具體方法的封裝,實現的功能。本文將分析中兩種動態代理的實現方式,和,比較它們的異同。那如何動態編譯呢你可以使用,這是一個封裝了的庫,幫助你方便地實現動態編譯源代碼。
發現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,也純粹是實驗而已,感興趣的童鞋也可以自己試一下。寫字節碼還是挺酸爽的,它類似匯編但其實比匯編容易的多。它不像匯編那樣一會兒寄存器一會兒內存地址,一會兒堆一會兒棧,各種變量和地址繞來繞去。字節碼的執行方式是很清晰的,變量都存儲在本地變量表里,棧只是用來做函數調用,所以非常直觀。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/76511.html
摘要:要明白,動態代理類的存在意義是為了攔截方法并修改邏輯而動態代理的局限性之一就是只能攔截接口所聲明的方法。因為動態代理類是繼承自業務類,所以該類和方法不能聲明成無法繼承或重寫。者最終都是生成了一個新的動態代理類對象。 動態代理 1、先談靜態代理 對于靜態代理,我們已經很熟悉了。我們擁有一個抽象類,真實類繼承自抽象類并重寫其業務方法,代理類持有真實類的對象實例,在重寫業務方法中通過調用真實...
摘要:動態代理又被稱為代理或接口代理。靜態代理在編譯時產生字節碼文件,可以直接使用,效率高。代理無需實現接口,通過生成類字節碼實現代理,比反射稍快,不存在性能問題,但會繼承目標對象,需要重寫方法,所以目標對象不能為類。 一、代理模式介紹 代理模式是一種設計模式,提供了對目標對象額外的訪問方式,即通過代理對象訪問目標對象,這樣可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功...
摘要:場景描述病從口入這句成語告訴我們注意飲食健康,小六同學想吃蘋果,在吃蘋果之前需要清洗一下蘋果和洗一下手,吃完蘋果后,需要洗一下手保持個人衛生十分鐘后。。。動態代理小六委托管家來代理洗食物和洗手,小六屬于委托對象,管家屬于代理對象。 前言 為了更好的理解代理模式,首先根據生活中實際場景進行模擬,讓我們在生活中去體驗設計思想的美妙。 場景描述 病從口入這句成語告訴我們注意飲食健康,小六同學...
摘要:是一種特殊的增強切面切面由切點和增強通知組成,它既包括了橫切邏輯的定義也包括了連接點的定義。實際上,一個的實現被拆分到多個類中在中聲明切面我們知道注解很方便,但是,要想使用注解的方式使用就必須要有源碼因為我們要 前言 只有光頭才能變強 上一篇已經講解了Spring IOC知識點一網打盡!,這篇主要是講解Spring的AOP模塊~ 之前我已經寫過一篇關于AOP的文章了,那篇把比較重要的知...
摘要:代理模式的實現靜態代理優缺點優點只對對需要的方法加代理邏輯。通過繼承的方式進行代理,無論目標對象有沒有實現接口都可以代理,但是無法處理的情況。 注意:本文所有的class使用的static修飾主要是為了能在一個類里面測試。實際項目中不應該這樣做的,應該分包分class。文字描述不是很多,還是看代碼比較好理解吧... 1. Java代理的理解 代理模式是一種設計模式,簡單說即是在不改變源...
閱讀 2533·2023-04-26 02:47
閱讀 3014·2023-04-26 00:42
閱讀 879·2021-10-12 10:12
閱讀 1386·2021-09-29 09:35
閱讀 1702·2021-09-26 09:55
閱讀 488·2019-08-30 14:00
閱讀 1543·2019-08-29 12:57
閱讀 2365·2019-08-28 18:00