1. 程式人生 > >Java動態代理 深度詳解

Java動態代理 深度詳解

實現 接下來 href 新建 結構 str 如果 cat 子類

代理模式是設計模式中非常重要的一種類型,而設計模式又是編程中非常重要的知識點,特別是在業務系統的重構中,更是有舉足輕重的地位。代理模式從類型上來說,可以分為靜態代理和動態代理兩種類型。

今天我將用非常簡單易懂的例子向大家介紹動態代理的兩種類型,接著重點介紹動態代理的兩種實現方式(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)。

希望這篇文章幫助大家更好地理解動態代理。

以上。

轉載地址:作者:陳樹義 出處:http://www.cnblogs.com/chanshuyi/

Java動態代理 深度詳解