1. 程式人生 > >秒懂Java代理與動態代理模式

秒懂Java代理與動態代理模式

概述

什麼是代理模式?解決什麼問題(即為什麼需要)?什麼是靜態代理?什麼是動態代理模式?二者什麼關係?具體如何實現?什麼原理?如何改進?這即為我們學習一項新知識的正確開啟方式,我們接下來會以此展開,讓你秒懂。

概念

什麼是代理模式

定義:為其他物件提供一種代理以控制對這個物件的訪問

定義總是抽象而晦澀難懂的,讓我們回到生活中來吧。

例項:王二狗公司(天津線上回聲科技發展有限公司)老闆突然在發工資的前一天帶著小姨子跑路了,可憐二狗一身房貸,被迫提起勞動仲裁,勞動局就會為其指派一位代理律師全權負責二狗的仲裁事宜。那這裡面就是使用了代理模式,因為在勞動仲裁這個活動中,代理律師會全權代理王二狗。

解決什麼問題

下面是一些使用場景,不過太抽象,暫時可以不要在意,隨著你的不斷進步你終究會明白的。

  • 遠端代理 :為位於兩個不同地址空間物件的訪問提供了一種實現機制,可以將一些消耗資源較多的物件和操作移至效能更好的計算機上,提高系統的整體執行效率。
  • 虛擬代理:通過一個消耗資源較少的物件來代表一個消耗資源較多的物件,可以在一定程度上節省系統的執行開銷。
  • 緩衝代理:為某一個操作的結果提供臨時的快取儲存空間,以便在後續使用中能夠共享這些結果,優化系統性能,縮短執行時間。
  • 保護代理:可以控制對一個物件的訪問許可權,為不同使用者提供不同級別的使用許可權。
  • 智慧引用:要為一個物件的訪問(引用)提供一些額外的操作時可以使用

什麼是靜態代理

靜態代理是指預先確定了代理與被代理者的關係,例如王二狗的代理律師方文鏡是在開庭前就確定的了。那對映到程式設計領域的話,就是指代理類與被代理類的依賴關係在編譯期間就確定了。下面就是王二狗勞動仲裁的程式碼實現:

首先定義一個代表訴訟的介面

public interface ILawSuit {
    void submit(String proof);//提起訴訟
    void defend();//法庭辯護
}

王二狗訴訟型別,實現ILawSuit介面

public class SecondDogWang implements ILawSuit {
    @Override
public void submit(String proof) { System.out.println(String.format("老闆欠薪跑路,證據如下:%s",proof)); } @Override public void defend() { System.out.println(String.format("鐵證如山,%s還錢","馬旭")); } }

代理律師訴訟類,實現ILawSuit介面

public class ProxyLawyer implements ILawSuit {

    ILawSuit plaintiff;//持有要代理的那個物件
    public ProxyLawyer(ILawSuit plaintiff) {
        this.plaintiff=plaintiff;
    }

    @Override
    public void submit(String proof) {
        plaintiff.submit(proof);
    }

    @Override
    public void defend() {
        plaintiff.defend();
    }
}

產生代理物件的靜態代理工廠類

public class ProxyFactory {
    public static ILawSuit getProxy(){
        return new ProxyLawyer(new SecondDogWang());
    }
}

這樣就基本構建了靜態代理關係了,然後在客戶端就可以使用代理物件來進行操作了。

    public static void main(String[] args) {
        ProxyFactory.getProxy().submit("工資流水在此");
        ProxyFactory.getProxy().defend();
    }

輸出結果如下:

老闆欠薪跑路,證據如下:工資流水在此
鐵證如山,馬旭還錢

可以看到,代理律師全權代理了王二狗的本次訴訟活動。那使用這種代理模式有什麼好處呢,我們為什麼不直接讓王二狗直接完成本次訴訟呢?現實中的情況比較複雜,但是我可以簡單列出幾條:這樣代理律師就可以在提起訴訟等操作之前做一些校驗工作,或者記錄工作。例如二狗提供的資料,律師可以選擇的移交給法庭而不是全部等等操作,就是說可以對代理的對做一些控制。例如二狗不能出席法庭,代理律師可以代為出席。。。

什麼是動態代理

動態代理本質上仍然是代理,情況與上面介紹的完全一樣,只是代理與被代理人的關係是動態確定的,例如王二狗的同事牛翠花開庭前沒有確定她的代理律師,而是在開庭當天當庭選擇了一個律師,對映到程式設計領域為這個關係是在執行時確定的。

那既然動態代理沒有為我們增強代理方面的任何功能,那我們為什麼還要用動態代理呢,靜態代理不是挺好的嗎?凡是動態確定的東西大概都具有靈活性,強擴充套件的優勢。上面的例子中如果牛翠花也使用靜態代理的話,那麼就需要再新增兩個類。一個是牛翠花訴訟類,一個是牛翠花的代理律師類,還的在代理靜態工廠中新增一個方法。而如果使用動態代理的話,就只需要生成一個訴訟類就可以了,全程只需要一個代理律師類,因為我們可以動態的將很多人的案子交給這個律師來處理。

Jdk動態代理實現

在java的動態代理機制中,有兩個重要的類或介面,一個是InvocationHandler介面、另一個則是 Proxy類,這個類和介面是實現我們動態代理所必須用到的。

InvocationHandler介面是給動態代理類實現的,負責處理被代理物件的操作的,而Proxy是用來建立動態代理類例項物件的,因為只有得到了這個物件我們才能呼叫那些需要代理的方法。

接下來我們看下例項,牛翠花動態指定代理律師是如何實現的。
1.構建一個牛翠花訴訟類

public class CuiHuaNiu implements ILawSuit {
    @Override
    public void submit(String proof) {
        System.out.println(String.format("老闆欠薪跑路,證據如下:%s",proof));
    }
    @Override
    public void defend() {
        System.out.println(String.format("鐵證如山,%s還牛翠花血汗錢","馬旭"));
    }
}

2.構建一個動態代理類

public class DynProxyLawyer implements InvocationHandler {
    private Object target;//被代理的物件
    public DynProxyLawyer(Object obj){
        this.target=obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("案件進展:"+method.getName());
        Object result=method.invoke(target,args);
        return result;
    }
}

3.修改靜態工廠方法

public class ProxyFactory {
    ...

    public static Object getDynProxy(Object target) {
        InvocationHandler handler = new DynProxyLawyer(target);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
    }
}

4.客戶端使用

    public static void main(String[] args) {
        ILawSuit proxy= (ILawSuit) ProxyFactory.getDynProxy(new CuiHuaNiu());
        proxy.submit("工資流水在此");
        proxy.defend();
    }

輸出結果為:

案件進展:submit
老闆欠薪跑路,證據如下:工資流水在此
案件進展:defend
鐵證如山,馬旭還牛翠花血汗錢

JDK動態代理實現的原理

首先Jdk的動態代理實現方法是依賴於介面的,首先使用介面來定義好操作的規範。然後通過Proxy類產生的代理物件呼叫被代理物件的操作,而這個操作又被分發給InvocationHandler介面的 invoke方法具體執行

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

此方法的引數含義如下
proxy:代表動態代理物件
method:代表正在執行的方法
args:代表當前執行方法傳入的實參
返回值:表示當前執行方法的返回值

例如上面牛翠花案例中,我們使用Proxy類的newProxyInstance()方法生成的代理物件proxy去呼叫了proxy.submit("工資流水在此");操作,那麼系統就會將此方法分發給invoke().其中proxy物件的類是系統幫我們動態生產的,其實現了我們的業務介面ILawSuit

cgLib的動態代理實現

由於JDK只能針對實現了介面的類做動態代理,而不能對沒有實現介面的類做動態代理,所以cgLib橫空出世!CGLib(Code Generation Library)是一個強大、高效能的Code生成類庫,它可以在程式執行期間動態擴充套件類或介面,它的底層是使用java位元組碼操作框架ASM實現。

1 引入cgLib 庫
cglib-nodep-3.2.6.jar:使用nodep包不需要關聯asm的jar包,jar包內部包含asm的類.

2 定義業務類,被代理的類沒有實現任何介面

public class Frank {
   public void submit(String proof) {
       System.out.println(String.format("老闆欠薪跑路,證據如下:%s",proof));
   }
   public void defend() {
       System.out.println(String.format("鐵證如山,%s還Frank血汗錢","馬旭"));
   }
}

3 定義攔截器,在呼叫目標方法時,CGLib會回撥MethodInterceptor介面方法攔截,來實現你自己的代理邏輯,類似於JDK中的InvocationHandler介面。

public class cgLibDynProxyLawyer implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {
        if (method.getName().equals("submit"))
            System.out.println("案件提交成功,證據如下:"+ Arrays.asList(params));
        Object result = methodProxy.invokeSuper(o, params);
        return result;
    }
}

4定義動態代理工廠,生成動態代理

public class ProxyFactory {
    public static Object getGcLibDynProxy(Object target){
        Enhancer enhancer=new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(new cgLibDynProxyLawyer());
        Object targetProxy= enhancer.create();
        return targetProxy;
    }
}

5客戶端呼叫

  public static void main(String[] args) {
        Frank cProxy= (Frank) ProxyFactory.getGcLibDynProxy(new Frank());
        cProxy.submit("工資流水在此");
        cProxy.defend();
    }

輸出結果:

案件提交成功,證據如下:[工資流水在此]
老闆欠薪跑路,證據如下:工資流水在此
鐵證如山,馬旭還Frank血汗錢

可見,通過cgLib對沒有實現任何介面的類做了動態代理,達到了和前面一樣的效果。這裡只是簡單的講解了一些cgLib的使用方式,有興趣的可以進一步瞭解其比較高階的功能,例如回撥過濾器(CallbackFilter)等。

cgLib的動態代理原理

CGLIB原理:動態生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。它比使用java反射的JDK動態代理要快。

CGLIB底層:使用位元組碼處理框架ASM,來轉換位元組碼並生成新的類。不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。

CGLIB缺點:對於final方法,無法進行代理。

動態代理在AOP中的應用

什麼是AOP? 維基百科上如是說:

定義:In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.
AOP是一種程式設計正規化,其目標是通過隔離切面耦合來增加程式的模組化。

首先宣告,AOP是OOP的補充,其地位及其重要性遠不及OOP,總體來說OOP面向名詞領域而AOP面向動詞領域,例如對一個人的設計肯定是使用OOP,例如這個人有手,腳,眼睛瞪屬性。而對這個人上廁所這個動作就會涉及到AOP,例如上廁所前的先確定一下拿沒拿手紙等。要理解AOP就首先要理解什麼是切面耦合(cross-cutting concerns)。例如有這樣一個需求,要求為一個程式中所有方法名稱以test開頭的方法列印一句log,這個行為就是一個典型的cross-cutting場景。首先這個列印log和業務毫無關係,然後其處於分散在整個程式當中的各個模組,如果按照我們原始的方法開發,一旦後期需求變動將是及其繁瑣的。所以我們就需要將這個切面耦合封裝隔離,不要將其混入業務程式碼當中。

例如在王二狗的案子中,我們希望在案子起訴後列印一句成功的log,如果不使用代理的話,我們是需要將log寫在相應的業務邏輯裡面的,例如王二狗訴訟類SecondDogWang裡面的submit()方法中。使用了動態代理後,我們只需要在InvocationHandler 裡面的invoke()方法中寫就可以了,不會侵入業務程式碼當中,在以後的維護過程中對業務毫無影響,這是我們希望看到的。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (method.getName().equals("submit"))
           System.out.println("案件提交成功,證據如下:"+ Arrays.asList(args));
    Object result=method.invoke(target,args);
    return result;
}

輸出結果為:

案件提交成功,證據如下:[工資流水在此]
老闆欠薪跑路,證據如下:工資流水在此
鐵證如山,馬旭還牛翠花血汗錢

所以AOP主要可以用於:日誌記錄,效能統計,安全控制,事務處理,異常處理等場景下。

總結

靜態代理比動態代理更符合OOP原則,在日常開發中使用也較多。動態代理在開發框架時使用較多,例如大名鼎鼎的Spring

最後希望王二狗和牛翠花他們可以順利拿回自己的血汗錢。