一起學設計模式 - 代理模式

代理模式(Proxy Pattern)屬於結構型模式的一種,給某個對象提供一個代理對象,並由代理對象控制對於原對象的訪問,即客戶不直接操控原對象,而是通過代理對象間接地操控原對象。

概述

身處華夏大地的碼農都知道,因爲國內有個牛逼的GFW,所以導致無法訪問 Google,不能訪問Google怎麼查資料呢?不能查資料怎麼提升(裝逼)呢,古語有云(扯犢子的):上有政策,下有對策,我們可以藉助梯子(設置代理)的方法訪問。

代理

  • 用戶向代理服務器發起請求
  • 代理把HTTP請求發給目標服務器
  • 目標服務器接收響應並返回給代理
  • 代理服務器把HTTP響應發回給用戶

在軟件開發中,也有一種設計模式可以提供與之類似的功能。由於某些原因,客戶端不想或不能直接訪問一個對象,此時可以通過一個稱之爲代理的第三者來實現間接訪問,該方案對應的設計模式被稱爲代理模式

代理模式是一種廣泛應用的結構型設計模式,常見的代理形式包括遠程代理、安全代理、虛擬代理、緩衝代理、智能引用代理等。

結構圖

代理模式結構圖

存在的角色

  • Subject(抽象主題角色):聲明瞭RealSubjectProxySubject的共同接口,客戶端通常需要針對接口角色進行編程
  • ProxySubject(代理主題角色):包含了對真實(委託)對象(RealSubject)的引用,可以控制對RealSubject的使用,負責在需要的時候創建和刪除,並對RealSubject的使用加以約束
  • RealSubject(真實主題角色):代理對象所代表的真實對象,也是最終引用的對象

案例

實現方式

  • 靜態代理:代理類是在編譯時就實現好的。Java 編譯完成後代理類是一個實際的 class 文件。
  • 動態代理:代理類是在運行時生成的。編譯完之後並沒有實際的 class 文件,而是在運行時動態生成的類字節碼,並加載到JVM中。

靜態代理

UML圖如下:

靜態代理

1.定義Subject(抽象主題角色)

interface Subject {
    void request();
}

2.創建RealSubject(真實主題角色),代理類中引用的對象

class RealSubject implements Subject {

    @Override
    public void request() {
        System.out.println("Google 搜索 battcn ");
    }
}

3.創建ProxySubject(代理主題角色),採用延遲加載的方式初始化RealSubject

class ProxySubject implements Subject {

    private Subject realSubject;

    @Override
    public void request() {
        System.out.println("向代理服務器發起請求");
        //用到時候才加載
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        realSubject.request();
        System.out.println("代理服務器響應請求");
    }
}

4.創建StaticProxyClient(測試類)

public class StaticProxyClient {

    public static void main(String[] args) {
        // 使用代理
        Subject subject = new ProxySubject();
        subject.request();
    }
}

5.運行結果

向代理服務器發起請求
Google 搜索 battcn 
代理服務器響應請求

RealSubject(真實主題角色)必須是事先創建好在的,並將其作爲ProxySubject(代理主題角色)的內部屬性。但是實際使用時,一個RealSubject(真實主題角色)色必須對應一個ProxySubject(代理主題角色),大量使用會導致類的急劇膨脹;此外,如果事先並不知道RealSubject角色,該如何使用ProxySubject呢?這個問題可以通過Java的動態代理類來解決。

動態代理

動態代理是指在運行時動態生成代理類。即,代理類的字節碼將在運行時生成並載入當前代理的ClassLoader

常見的幾種動態代理

  • JDK自帶:內置在JDK中,無需要引入第三方 Jar 包,使用簡單但相對功能較弱
  • CGLIB/Javassist:都是高級的字節碼生成庫,總體性能比 JDK 自帶的動態代理好,且功能十分強大
  • ASM:低級的字節碼生成工具,幾乎在使用 Java bytecode 編程,對開發人員要求及高,但也是性能最好的一種動態代理生成工具。不過由於它使用繁瑣,且性能也沒有數量級的提升,與 CGLIB/Javassist 等高級字節碼生成工具相比,ASM 程序的維護性較差,如果不是在對性能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist。

對比靜態代理的好處

  • 不需要爲RealSubject寫一個形式上完全一樣的封裝類,利於維護
  • 使用動態代理的生成方法甚至可以在運行時制定代理類的執行邏輯,從而提升系統的靈活性。

JDK動態代理

以上述所示代碼中的 ProxySubject 爲例,使用JDK動態代理生成動態類,替換上例中的 ProxySubject

UML圖如下:

JDK動態代理

1.JDK 的動態代理需要實現一個處理方法調用的Handler,用於實現代理方法的內部邏輯。

class SubjectHandler implements InvocationHandler {
    private Subject subject;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("向代理服務器發起請求");
        //如果第一次調用,生成真實主題
        if (subject == null) {
            subject = new RealSubject();
        }
        subject.request();
        //返回真實主題完成實際的操作
        System.out.println("代理服務器響應請求");
        //如果返回值可以直接 return subject.request();
        return null;
    }

    public static Subject createProxy() {
        return (Subject) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(), new Class[]{Subject.class}, new SubjectHandler()
        );
    }
}

2.創建JdkProxyClient(測試類)

public class JdkProxyClient {

    public static void main(String[] args) {
        Subject proxy = SubjectHandler.createProxy();
        proxy.request();
    }
}

3.運行結果

向代理服務器發起請求
動態代理 Google 搜索 battcn 
代理服務器響應請求

以上代碼生成了一個實現了Subject接口的代理類,代理類的內部邏輯由 SubjectHandler決定。生成代理類後,由 newProxyInstance()方法返回該代理類的一個實例。至此,一個完整的動態代理完成了。

CGLIB動態代理

在Java中,動態代理類的生成主要涉及對 ClassLoader 的使用。以 CGLIB 爲例,使用 CGLIB 生成動態代理,首先需要生成 Enhancer類實例,並指定用於處理代理業務的回調類。在 Enhancer.create() 方法中,會使用 DefaultGeneratorStrategy.Generate() 方法生成動態代理類的字節碼,並保存在 byte 數組中。接着使用 ReflectUtils.defineClass() 方法,通過反射,調用ClassLoader.defineClass() 方法,將字節碼裝載到 ClassLoader 中,完成類的加載。最後使用 ReflectUtils.newInstance() 方法,通過反射,生成動態類的實例,並返回該實例。基本流程是根據指定的回調類生成 Class 字節碼—通過 defineClass() 將字節碼定義爲類—使用反射機制生成該類的實例。

UML圖如下:

CGLIB動態代理

1.依賴CGLIB的包

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.1</version>
</dependency>

2.定義反射類及重載方法

class SubjectProxy implements MethodInterceptor {

    private Object target;

    /**
     * 創建代理對象
     *
     * @param target 目標對象
     * @return
     */
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 回調方法
        enhancer.setCallback(this);
        // 創建代理對象
        return enhancer.create();
    }


    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("向代理服務器發起請求");
        methodProxy.invokeSuper(o, objects);
        System.out.println("代理服務器響應請求");
        return null;
    }
}

3.創建CglibProxyClient(測試類)

public class CglibProxyClient {

    public static void main(String[] args) {
        SubjectProxy proxy = new SubjectProxy();
        Subject subject = (RealSubject) proxy.getInstance(new RealSubject());
        subject.request();
    }
}

4.運行結果

向代理服務器發起請求
CGLIB 動態代理 Google 搜索 battcn 
代理服務器響應請求

總結

應用場合

  • 遠程代理:爲對象在不同的地址空間提供局部代表,這樣可以隱藏一個對象存在於不同地址空間的事實。比如說 WebService,當我們在應用程序的項目中加入一個 Web 引用,引用一個 WebService,此時會在項目中聲稱一個 WebReference 的文件夾和一些文件,這個就是起代理作用的,這樣可以讓那個客戶端程序調用代理解決遠程訪問的問題;
  • 虛擬代理:對於一些佔用系統資源較多或者加載時間較長的對象,可以給這些對象提供一個虛擬代理,這樣就可以達到性能的最優化。比如打開一個網頁,這個網頁裏面包含了大量的文字和圖片,但我們可以很快看到文字,但是圖片卻是一張一張地下載後才能看到,那些未打開的圖片框,就是通過虛擬代裏來替換了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸;
  • 安全代理:控制真實對象訪問時的權限。一般用於對象應該有不同的訪問權限的時候;
  • 緩衝代理:爲某一個操作的結果提供臨時的緩存存儲空間,以便在後續使用中能夠共享這些結果,從而可以避免某些方法的重複執行,優化
    系統性能。
  • 智能引用:指當調用真實的對象時,代理處理另外一些事。比如計算真實對象的引用次數,這樣當該對象沒有引用時,可以自動釋放它,或當第一次引用一個持久對象時,將它裝入內存,或是在訪問一個實際對象前,檢查是否已經釋放它,以確保其他對象不能改變它。這些都是通過代理在訪問一個對象時附加一些內務處理;

相關模式

  • 適配器模式主要改變所考慮對象的接口,而代理模式不能改變所代理類的接口
  • 裝飾器模式爲了增強功能,而代理模式是爲了加以控制。

結束語

設計模式是前人工作的總結和提煉。通常,被人們廣泛流傳的設計模式都是對某一特定問題的成熟的解決方案。如果能合理地使用設計模式,不僅能使系統更容易地被他人理解,同時也能使系統擁有更加合理的結構。

參考文獻:https://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/

參考文獻:https://www.zybuluo.com/pastqing/note/174679

- 說點什麼

全文代碼:https://gitee.com/battcn/design-pattern/tree/master/Chapter10/battcn-proxy

  • 個人QQ:1837307557
  • battcn開源羣(適合新手):391619659

微信公衆號:battcn(歡迎調戲)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章