標籤:cglib動態代理 分享圖片 總結 col 日誌系統 需要 複雜 應該 his
文章首發於【部落格園-陳樹義】,點擊跳轉到原文深入淺出Java動態代理
代理模式是設計模式中非常重要的一種類型,而設計模式又是編程中非常重要的知識點,特別是在業務系統的重構中,更是有舉足輕重的地位。代理模式從類型上來說,可以分為靜態代理和動態代理兩種類型。
今天我將用非常簡單易懂的例子向大家介紹動態代理的兩種類型,接著重點介紹動態代理的兩種實現方式(Java 動態代理和 CGLib 動態代理),最後深入剖析這兩種實現方式的異同,最後說說動態代理在我們周邊架構中的應用。
在開始之前,我們先假設這樣一個情境:有一個蛋糕店,它們都是使用蛋糕機來做蛋糕的,而且不同種類的蛋糕由不同的蛋糕機來做,這樣就有:水果蛋糕機、巧克力蛋糕機等。這個情境用 Java 語言描述就是下面這樣:
//做蛋糕的機器public interface CakeMachine{ void makeCake();}//專門做水果蛋糕的機器class FruitCakeMachine implements CakeMachine{ public void makeCake() { System.out.println("Making a fruit cake..."); }}//專門做巧克力蛋糕的機器public class ChocolateCakeMachine implements CakeMachine{ public void makeCake() { System.out.printf("making a Chocolate Cake..."); }}//蛋糕店public class CakeShop { public static void main(String[] args) { new FruitCakeMachine().makeCake(); //making a Fruit Cake... new ChocolateCakeMachine().makeCake(); //making a Chocolate Cake... }}
上面的代碼抽象出了一個 CakeMachine 介面,有各種蛋糕機(FruitCakeMachine、ChocolateCakeMachine 等)實現了該介面,最後蛋糕店(CakeShop)直接利用這些蛋糕機做蛋糕。
這樣的一個例子真實地描述了實際生活中的情境。但生活中的情境往往是複雜多變的,假設這個時候來了一個顧客,他想要一個水果蛋糕,但他特別喜歡杏仁,希望在水果蛋糕上加上一層杏仁。這時候我們應該怎麼做呢?
因為我們的蛋糕機只能做水果蛋糕(程式設定好了),沒辦法做杏仁水果蛋糕。最簡單的辦法是直接修改水果蛋糕機的程式,做一台能做杏仁水果蛋糕的蛋糕機。這種方式對應的代碼修改也很簡單,直接在原來的代碼上進行修改,產生一台專門做杏仁水果蛋糕的機器就好了,修改後的 FruitCakeMachien 類應該是這樣子:
//專門做水果蛋糕的機器,並且加上一層杏仁class FruitCakeMachine implements CakeMachine{ public void makeCake() { System.out.println("making a Fruit Cake..."); System.out.println("adding apricot..."); }}
雖然上面這種方式實現了我們的業務需求。但是仔細想一想,在現實生活中如果我們遇到這樣的一個需求,我們不可能因為一個顧客的特殊需求就去修改一台蛋糕機的硬體程式,這樣成本太高!而且從代碼實現角度上來說,這種方式從代碼上不是很優雅,修改了原來的代碼。根據代碼圈中「對修改封閉、對擴充開放」的思想,我們在嘗試滿足新的業務需求的時候應該盡量少修改原來的代碼,而是在原來的代碼上進行拓展。
那我們究竟應該怎麼做更加合適一些呢?我們肯定是直接用水果蛋糕機做一個蛋糕,然後再人工撒上一層杏仁啦。這其實就對應了即使模式中的代理模式,在這個業務情境中,服務員(代理人)跟顧客說沒問題,可以做水果杏仁蛋糕,於是服務員充當了一個代理的角色,先讓水果蛋糕機做出了水果蛋糕,之後再往上面撒了一層杏仁。在這個例子中,實際做事情的還是水果蛋糕機,服務員(撒杏仁的人)只是充當了一個代理的角色。
下面我們就來試著實現這樣一個代理模式的設計。我們需要做的,其實就是設計一個代理類(FruitCakeMachineProxy),這個代理類就相當於那個撒上一層杏仁的人,之後讓蛋糕店直接調用即可代理類去實現即可。
//水果蛋糕機代理public class FruitCakeMachineProxy implements CakeMachine{ private CakeMachine cakeMachine; public FruitCakeMachineProxy(CakeMachine cakeMachine) { this.cakeMachine = cakeMachine; } public void makeCake() { cakeMachine.makeCake(); System.out.println("adding apricot..."); }}//蛋糕店public class CakeShop { public static void main(String[] args) { FruitCakeMachine fruitCakeMachine = new FruitCakeMachine(); FruitCakeMachineProxy fruitCakeMachineProxy = new FruitCakeMachineProxy(fruitCakeMachine); fruitCakeMachineProxy.makeCake(); //making a Fruit Cake... adding apricot... }}
通過代理實現這樣的業務情境,這樣我們就不需要在原來的類上進行修改,從而使得代碼更加優雅,拓展性更強。如果下次客人喜歡葡萄乾水果蛋糕了了,那可以再寫一個 CurrantCakeMachineProxy 類來撒上一層葡萄乾,原來的代碼也不會被修改。上面說的這種業務情境就是代理模式的實際應用,準確地說這種是靜態代理。
業務情境的複雜度往往千變萬化,如果有另外一個客人,他也想在巧克力蛋糕上撒一層杏仁,那我們豈不是也要再寫一個代理類讓他做同樣的一件事情。如果有客人想在抹茶蛋糕上撒一層杏仁,有客人想在五仁蛋糕上撒一層杏仁……那我們豈不是要寫無數個代理類?
其實在 Java 中早已經有了針對這種情況而設計的一個介面,專門用來解決類似的問題,它就是動態代理 —— InvocationHandler。
動態代理與靜態代理的區別是靜態代理只能針對特定一種類型(某種蛋糕機)做某種代理動作(撒杏仁),而動態代理則可以對所有類型(所有蛋糕機)做某種代理動作(撒杏仁)。
接下來我們針對這個業務情境做一個代碼的抽象實現。首先我們分析一下可以知道這種情境的共同點是希望在各種蛋糕上都做「撒一層杏仁」的動作,所以我們就做一個杏仁動態代理(ApricotHandler)。
//杏仁動態代理public class ApricotHandler implements InvocationHandler{ private Object object; public ApricotHandler(Object object) { this.object = object; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(object, args); //調用真正的蛋糕機做蛋糕 System.out.println("adding apricot..."); return result; }}
撒杏仁的代理寫完之後,我們直接讓蛋糕店開工:
public class CakeShop { public static void main(String[] args) { //水果蛋糕撒一層杏仁 CakeMachine fruitCakeMachine = new FruitCakeMachine(); ApricotHandler fruitCakeApricotHandler = new ApricotHandler(fruitCakeMachine); CakeMachine fruitCakeProxy = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(), fruitCakeMachine.getClass().getInterfaces(), fruitCakeApricotHandler); fruitCakeProxy.makeCake(); //巧克力蛋糕撒一層杏仁 CakeMachine chocolateCakeMachine = new ChocolateCakeMachine(); ApricotHandler chocolateCakeApricotHandler = new ApricotHandler(chocolateCakeMachine); CakeMachine chocolateCakeProxy = (CakeMachine) Proxy.newProxyInstance(chocolateCakeMachine.getClass().getClassLoader(), chocolateCakeMachine.getClass().getInterfaces(), chocolateCakeApricotHandler); chocolateCakeProxy.makeCake(); }}
輸出結果為:
making a Fruit Cake...adding apricot...making a Chocolate Cake...adding apricot...
從輸出結果可以知道,這與我們想要的結果是一致的。與靜態代理相比,動態代理具有更加的普適性,能減少更多重複的代碼。試想這個情境如果使用靜態代理的話,我們需要對每一種類型的蛋糕機都寫一個代理類(FruitCakeMachineProxy、ChocolateCakeMachineProxy、MatchaCakeMachineProxy等)。但是如果使用動態代理的話,我們只需要寫一個通用的撒杏仁代理類(ApricotHandler)就可以直接完成所有操作了。直接省去了寫 FruitCakeMachineProxy、ChocolateCakeMachineProxy、MatchaCakeMachineProxy 的功夫,極大地提高了效率。
看到這裡,大家應該清楚為什麼有了靜態代理之後,還需要有動態代理了吧。靜態代理只能針對某一種類型的實現(蛋糕機)進行操作,如果要針對所有類型的實現(所有蛋糕機)都進行同樣的操作,那就必須要動態代理出馬了。
如何使用動態代理?
參照上面的例子,我們可以知道要實現動態代理需要做兩方面的工作。
- 首先需要建立一個類,並且這個類必須實現 InvocationHandler 介面。
//杏仁動態代理public class ApricotHandler implements InvocationHandler{ private Object object; public ApricotHandler(Object object) { this.object = object; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(object, args); //調用真正的蛋糕機做蛋糕 System.out.println("adding apricot..."); return result; }}
- 在調用的時候使用 Proxy.newProxyInstance() 方法組建代理程式類。
public class CakeShop { public static void main(String[] args) { //水果蛋糕撒一層杏仁 CakeMachine fruitCakeMachine = new FruitCakeMachine(); ApricotHandler fruitCakeApricotHandler = new ApricotHandler(fruitCakeMachine); CakeMachine fruitCakeProxy = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(), fruitCakeMachine.getClass().getInterfaces(), fruitCakeApricotHandler); fruitCakeProxy.makeCake(); }
動態代理的幾種實現方式
動態代理其實指的是一種設計模式概念,指的是通過代理來做一些通用的事情,常見的應用有許可權系統、日誌系統等,都用到了動態代理。
而 Java 動態代理只是動態代理的一種實現方式而已,動態代理還有另外一種實現方式,即 CGLib(Code Generation Library)。
Java 動態代理只能針對實現了介面的類進行拓展,所以細心的朋友會發現我們的代碼裡有一個叫 MachineCake 的介面。而 CGLib 則沒有這個限制,因為 CGLib 是使用繼承原有類的方式來實現代理的。
我們還是舉個例子來說明 CGLib 是如何?動態代理的吧。還是前面的例子:我們要做杏仁水果蛋糕、巧克力水果蛋糕、五仁巧克力蛋糕,這時候用代碼描述是這樣的。
首先我們需要寫一個杏仁攔截器類,這個攔截器可以給做好的蛋糕加上杏仁。
public class ApricotInterceptor implements MethodInterceptor { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { methodProxy.invokeSuper(o, objects); System.out.println("adding apricot..."); return o; }}
接著直接讓蛋糕店使用 CGLib 提供的工具類做杏仁水果蛋糕:
public class CakeShop { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(FruitCakeMachine.class); enhancer.setCallback(new ApricotInterceptor()); FruitCakeMachine fruitCakeMachine = (FruitCakeMachine) enhancer.create(); fruitCakeMachine.makeCake(); }}
上面的 enhancer.setSuperClass() 設定需要增強類,而 enhancer.setCallback() 則設定需要回調的攔截器,即實現了 MethodInterceptor 介面的類。最後最後使用 enhancer.create() 產生了對應的增強類,最後輸出結果為:
making a Fruit Cake...adding apricot...
和我們預期的一樣。如果要做一個杏仁巧克力蛋糕,那麼直接讓蛋糕店利用ApricotHandler 再做一個就可以了,它們的區別只是傳入的增強類不同。
public class CakeShop { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(ChocolateCakeMachine.class); enhancer.setCallback(new ApricotInterceptor()); ChocolateCakeMachine chocolateCakeMachine = (ChocolateCakeMachine) enhancer.create(); chocolateCakeMachine.makeCake(); }}
可以看到,這裡傳入的增強類是 ChocolateCakeMachine,而不是之前的 FruitCakeMachine。
對比 Java 動態代理和 CGLib 動態代理兩種實現方式,你會發現 Java 動態代理適合於那些有介面抽象的類代理,而 CGLib 則適合那些沒有介面抽象的類代理。
Java動態代理的原理
從上面的例子我們可以知道,Java 動態代理的入口是從 Proxy.newInstance() 方法中開始的,那麼我們就從這個方法開始邊剖析源碼邊理解其原理。
其實通過這個方法,Java 替我們產生了一個繼承了指定介面(CakeMachine)的代理類(ApricotHandler)執行個體。從 Proxy.newInstance() 的源碼我們可以看到首先調用了 getProxyClass0 方法,該方法返回了一個 Class 執行個體對象,該執行個體對象其實就是 ApricotHandler 的 Class 對象。接著擷取其構造方法對象,最後產生該 Class 對象的執行個體。其實這裡最主要的是 getProxyClass0() 方法,這裡面動態產生了 ApricotHandler 的 Class 對象。下面我們就深入到 getProxyClass0() 方法中去瞭解這裡面做了什麼操作。
getProxyClass0() 方法首先是做了一些參數校正,之後從 proxyClassCache 參數中取出 Class 對象。其實 proxyClassCache 是一個 Map 對象,緩衝了所有動態建立的 Class 對象。從源碼中的注釋可以知道,如果從 Map 中取出的對象為空白,那麼其會調用 ProxyClassFactory 產生對應的 Class 對象。
在 ProxyClassFactory 類的源碼中,最終是調用了 ProxyGenerator.genrateProxyClass() 方法產生了對應的 class 位元組碼檔案。
到這裡,我們已經把動態代理的 Java 原始碼都解析完了,現在思路就很清晰了。Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法簡單來說執行了以下操作:
- 1、產生一個實現了參數 interfaces 裡所有介面且繼承了 Proxy 的代理類的位元組碼,然後用參數裡的 classLoader 載入這個代理類。
- 2、使用代理類父類的建構函式 Proxy(InvocationHandler h) 來創造一個代理類的執行個體,將我們自訂的 InvocationHandler 的子類傳入。
- 3、返回這個代理類執行個體,因為我們構造的代理類實現了 interfaces(也就是我們程式中傳入的 fruitCakeMachine.getClass().getInterfaces() 裡的所有介面,因此返回的代理類可以強轉成 MachineCake 類型來調用介面中定義的方法。
CGLib動態代理的原理
因為 JVM 並不允許在運行時修改原有類,所以所有的動態性都是通過建立類來實現的,上面說到的 Java 動態代理也不例外。所以對於 CGLib 動態代理的原理,其實也是通過動態組建代理程式類,最後由代理類來完成操作實現的。
對於 CGLib 動態代理的實現,我並沒有深入到源碼中,而是通過查閱資料瞭解了其大概的實現原理。
- 首先,我們在使用的時候通過 enhancer.setSuperclass(FruitCakeMachine.class) 傳入了需要增加的類,CGLib 便會產生一個繼承了改類的代理類。
- 接著,我們通過 enhancer.setCallback(new ApricotInterceptor()) 傳入了代理類對象,CGLib 通過組裝兩個類的結構實現一個靜態代理,從而達到具體的目的。
而在 CGLib 產生新類的過程中,其使用的是一個名為 ASM 的東西,它對 Java 的 class 檔案進行操作、產生新的 class 檔案。如果你對 CGLib 的原理感興趣,不妨看看這篇文章:從兄弟到父子:動態代理在民間是怎麼玩的?
動態代理的應用
動態代理在代碼界可是有非常重要的意義,我們開發用到的許多架構都使用到了這個概念。我所知道的就有:Spring AOP、Hibernate、Struts 使用到了動態代理。
- Spring AOP。Spring 最重要的一個特性是 AOP(Aspect Oriented Programming 面向切面編程),利用 Spring AOP 可以快速地實現許可權校正、安全校正等公用操作。而 Spring AOP 的原理則是通過動態代理實現的,預設情況下 Spring AOP 會採用 Java 動態代理實現,而當該類沒有對應介面時才會使用 CGLib 動態代理實現。
- Hibernate。Hibernate 是一個常用的 ORM 層架構,在擷取資料時常用的操作有:get() 和 load() 方法,它們的區別是:get() 方法會直接擷取資料,而 load() 方法則會消極式載入,等到使用者真的去取資料的時候才利用代理類去讀資料庫。
- Struts。Struts 現在雖然因為其太多 bug 已經被拋棄,但是曾經用過 Struts 的人都知道 Struts 中的攔截器。攔截器有非常強的 AOP 特性,仔細瞭解之後你會發現 Struts 攔截器其實也是用動態代理實現的。
總結
我們通過蛋糕店的不同業務情境介紹了靜態代理和動態代理的應用,接著重點介紹了動態代理兩種實現方式(Java 動態代理、CGLib 動態代理)的使用方法及其實現原理,其中還針對 Java 動態代理的源碼進行了簡單的分析。最後,我們介紹了動態代理在實際上編程中的應用(Spring AOP、Hibernate、Struts)。
希望這篇文章協助大家更好地理解動態代理。
以上。
文章首發於【部落格園-陳樹義】,點擊跳轉到原文深入淺出Java動態代理
深入淺出Java動態代理