1. 程式人生 > >設計模式:這是一份全面 & 清晰的動態代理模式(Proxy Pattern)學習指南

設計模式:這是一份全面 & 清晰的動態代理模式(Proxy Pattern)學習指南

前言

今天我來全面講解Android開發中最常用的設計模式 - 代理模式中的動態代理模式

目錄

示意圖

1. 為什麼要使用動態代理

1.1 背景

代理模式中的靜態代理模式存在一些特點:


  • 1個靜態代理 只服務1種類型的目標物件
  • 若要服務多型別的目標物件,則需要為每種目標物件都實現一個靜態代理物件

1.2 衝突

在目標物件較多的情況下,若採用靜態代理,則會出現 靜態代理物件量多、程式碼量大,從而導致程式碼複雜的問題

1.3 解決方案

採用 動態代理模式

2. 動態代理模式介紹

2.1 實現原理

  • 設計動態代理類(DynamicProxy
    )時,不需要顯式實現與目標物件類(RealSubject)相同的介面,而是將這種實現推遲到程式執行時由 JVM來實現
    1. 即:在使用時再建立動態代理類 & 例項
    2. 靜態代理則是在代理類實現時就指定與目標物件類(RealSubject)相同的介面
    3. 通過Java 反射機制的method.invoke(),通過呼叫動態代理類物件方法,從而自動呼叫目標物件的方法

2.2 優點

  • 只需要1個動態代理類就可以解決建立多個靜態代理的問題,避免重複、多餘程式碼
  • 更強的靈活性
    1. 設計動態代理類(DynamicProxy)時,不需要顯式實現與目標物件類(RealSubject
      )相同的介面
      ,而是將這種實現推遲到程式執行時由 JVM來實現
    2. 在使用時(呼叫目標物件方法時)才會動態建立動態代理類 & 例項,不需要事先例項化

2.3 缺點

  • 效率低
    相比靜態代理中 直接呼叫目標物件方法,動態代理則需要先通過Java反射機制 從而 間接呼叫目標物件方法
  • 應用場景侷限
    因為 Java 的單繼承特性(每個代理類都繼承了 Proxy 類),即只能針對介面 建立 代理類,不能針對類 建立代理類
    即只能動態代理 實現了介面的類

2.4 應用場景

  • 基於靜態代理應用場景下,需要代理物件數量較多的情況下使用動態代理
  • AOP
    領域
    1. 定義:即 Aspect Oriented Programming = 面向切面程式設計,是OOP的延續、函數語言程式設計的一種衍生範型
    2. 作用:通過預編譯方式和執行期動態代理實現程式功能的統一維護。
    3. 優點:降低業務邏輯各部分之間的耦合度 、 提高程式的可重用性 & 提高了開發的效率
    4. 具體應用場景:日誌記錄、效能統計、安全控制、異常處理等

2.5 與靜態代理模式的區別

示意圖

3. 具體應用

接下來,我將用1個具體例項來對 動態代理模式 進行更深一步的介紹

3.1 例項概況

  • 背景:小成 希望買一臺最新的頂配 Mac 電腦;小何希望買一臺 iPhone
  • 衝突:國內還沒上,只有美國才有
  • 解決方案:尋找一個代購一起進行購買
  1. 即1個代購(動態代理物件)同時 代替 小成 & 小何(目標物件) 去買Mac(間接訪問的操作)
  2. 該代購是代購任何商品 = 什麼人有什麼需求就會去代購任何東西(動態代理)

3.2 使用步驟

  1. 宣告 呼叫處理器類
  2. 宣告目標物件類的抽象介面
  3. 宣告目標物件類
  4. 通過動態代理物件,呼叫目標物件的方法

3.3 步驟詳解

步驟1: 宣告 呼叫處理器類

DynamicProxy.java

<-- 作用 -->
// 1.  生成 動態代理物件
// 2.  指定 代理物件執行目標物件方法時需要完成的 具體任務
// 注:需實現InvocationHandler介面 = 呼叫處理器 介面
// 所以稱為 呼叫處理器類

 public class DynamicProxy implements InvocationHandler {

    // 宣告代理物件
    // 作用:繫結關係,即關聯到哪個介面(與具體的實現類繫結)的哪些方法將被呼叫時,執行invoke()
    private Object ProxyObject;

    public Object newProxyInstance(Object ProxyObject){
        this.ProxyObject =ProxyObject;
        return Proxy.newProxyInstance(ProxyObject.getClass().getClassLoader(),
                ProxyObject.getClass().getInterfaces(),this);
        // Proxy類 = 動態代理類的主類 
        // Proxy.newProxyInstance()作用:根據指定的類裝載器、一組介面 & 呼叫處理器 生成動態代理類例項,並最終返回
        // 引數說明:
        // 引數1:指定產生代理物件的類載入器,需要將其指定為和目標物件同一個類載入器
        // 引數2:指定目標物件的實現介面
        // 即要給目標物件提供一組什麼介面。若提供了一組介面給它,那麼該代理物件就預設實現了該介面,這樣就能呼叫這組介面中的方法
        // 引數3:指定InvocationHandler物件。即動態代理物件在呼叫方法時,會關聯到哪個InvocationHandler物件

    }

    //  複寫InvocationHandler介面的invoke()
    //  動態代理物件呼叫目標物件的任何方法前,都會呼叫呼叫處理器類的invoke()
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            // 引數說明:
            // 引數1:動態代理物件(即哪個動態代理物件呼叫了method()
            // 引數2:目標物件被呼叫的方法
            // 引數3:指定被呼叫方法的引數
            throws Throwable {
                System.out.println("代購出門了");
                Object result = null;
                // 通過Java反射機制呼叫目標物件方法
                result = method.invoke(ProxyObject, args);
        return result;
    }

}

步驟2: 宣告目標物件的抽象介面

Subject.java

public interface Subject {
    // 定義目標物件的介面方法
    // 代購物品
    public  void buybuybuy();

}

步驟3: 宣告目標物件類

Buyer1.java


// 小成,真正的想買Mac的物件 = 目標物件 = 被代理的物件
// 實現抽象目標物件的介面
public class Buyer1 implements Subject  {

    @Override
    public void buybuybuy() {
        System.out.println("小成要買Mac");
    }

}

Buyer2.java

// 小何,真正的想買iPhone的物件 = 目標物件 = 被代理的物件
// 實現抽象目標物件的介面
public class Buyer2 implements Subject  {

    @Override
    public void buybuybuy() {
        System.out.println("小何要買iPhone");
    }

}

步驟4: 通過動態代理物件,呼叫目標物件的方法
MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 建立呼叫處理器類物件
        DynamicProxy DynamicProxy = new DynamicProxy();

        // 2. 建立目標物件物件
        Buyer1 mBuyer1 = new Buyer1();

        // 3. 建立動態代理類 & 物件:通過呼叫處理器類物件newProxyInstance()
        // 傳入上述目標物件物件
        Subject Buyer1_DynamicProxy = (Subject) DynamicProxy.newProxyInstance(mBuyer1);

        // 4. 通過呼叫動態代理物件方法從而呼叫目標物件方法
        // 實際上是呼叫了invoke(),再通過invoke()裡的反射機制呼叫目標物件的方法
        Buyer1_DynamicProxy.buybuybuy();
        // 以上代購為小成代購Mac

        // 以下是代購為小何代購iPhone
        Buyer2 mBuyer2 = new Buyer2();
        Subject Buyer2_DynamicProxy = (Subject) DynamicProxy.newProxyInstance(mBuyer2);
        Buyer2_DynamicProxy.buybuybuy();
    }
}

3.4 測試結果

示意圖

3.5 Demo地址

4. 原始碼分析

  • 在經過上面的例項後,你是否會對以下問題好奇:

    1. 動態代理類 及其物件例項是如何生成的?
    2. 如何通過呼叫動態代理物件方法,從而呼叫目標物件方法?
  • 下面,我們順著 步驟4:目標物件 通過 動態代理物件呼叫方法的使用 來進行動態代理模式的原始碼分析

// 步驟4:通過動態代理物件,呼叫目標物件的方法
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1. 建立 呼叫處理器類 物件
        DynamicProxy DynamicProxy = new DynamicProxy();

        // 2. 建立 目標類 物件
        Buyer1 mBuyer1 = new Buyer1();

        // 3. 建立動態代理類 & 物件:通過呼叫處理器類物件newProxyInstance()
        // 傳入上述目標類物件
        Subject Buyer1_DynamicProxy = (Subject) DynamicProxy.newProxyInstance(mBuyer1);
        // ->>關注1

        // 4. 通過呼叫動態代理物件方法從而呼叫目標物件方法
        // ->>關注2
        Buyer1_DynamicProxy.buybuybuy();
        // 以上代購為小成代購Mac

        // 以下是代購為小何代購iPhone
        Buyer2 mBuyer2 = new Buyer2();
        Subject Buyer2_DynamicProxy = (Subject) DynamicProxy.newProxyInstance(mBuyer2);
        Buyer2_DynamicProxy.buybuybuy();
    }
}

此處有兩個重要的原始碼分析點:

  • 關注1:建立動態代理類 & 物件:通過呼叫處理器類物件newProxyInstance()

    解決的問題是:動態代理類 及其物件例項是如何生成的?

  • 關注2:通過呼叫動態代理物件方法從而呼叫目標物件方法

    解決的問題是:如何通過呼叫動態代理物件方法,從而呼叫目標物件方法?

下面,我們將主要分析這兩處原始碼。

4.1 (關注1)建立動態代理類 & 物件:通過呼叫處理器類物件newProxyInstance()

// 使用程式碼
Subject Buyer1_DynamicProxy = (Subject) DynamicProxy.newProxyInstance(mBuyer1);
  • 即,動態代理類 及其物件例項是如何生成的?
  • 下面,我們直接進入DynamicProxy.newProxyInstance()
<-- 關注1:呼叫處理器 類的newProxyInstance() -->
// 即步驟1中實現的類:DynamicProxy

// 作用:
// 1.  生成 動態代理物件
// 2.  指定 代理物件執行目標物件方法時需要完成的 具體任務
// 注:需實現InvocationHandler介面 = 呼叫處理器 介面

 public class DynamicProxy implements InvocationHandler {

    // 宣告代理物件
    private Object ProxyObject;

    public Object newProxyInstance(Object ProxyObject){
        this.ProxyObject =ProxyObject;
        return Proxy.newProxyInstance(ProxyObject.getClass().getClassLoader(),
                ProxyObject.getClass().getInterfaces(),this);
        // Proxy.newProxyInstance()作用:根據指定的類裝載器、一組介面 & 呼叫處理器 生成動態代理類例項,並最終返回
        // ->>關注2

    }

     // 以下暫時忽略,下文會詳細介紹
    //  複寫InvocationHandler介面的invoke()
    //  動態代理物件呼叫目標物件的任何方法前,都會呼叫呼叫處理器類的invoke()
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            // 引數說明:
            // 引數1:動態代理物件(即哪個動態代理物件呼叫了method()
            // 引數2:目標物件被呼叫的方法
            // 引數3:指定被呼叫方法的引數
            throws Throwable {
                System.out.println("代購出門了");
                Object result = null;
                // 通過Java反射機制呼叫目標物件方法
                result = method.invoke(ProxyObject, args);
        return result;
    }

// 至此,關注1分析完畢,跳出
}

<-- 關注2:newProxyInstance()原始碼解析-->
// 作用:根據指定的類裝載器、一組介面 & 呼叫處理器 生成動態代理類及其物件例項,並最終返回

public static Object newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler h) throws IllegalArgumentException { 
  // 引數說明:
        // 引數1:指定產生代理物件的類載入器,需要將其指定為和目標物件同一個類載入器
        // 引數2:指定目標物件的實現介面
        // 即要給目標物件提供一組什麼介面。若提供了一組介面給它,那麼該代理物件就預設實現了該介面,這樣就能呼叫這組介面中的方法
        // 引數3:指定InvocationHandler物件。即動態代理物件在呼叫方法時,會關聯到哪個InvocationHandler物件

    ...  
    // 僅貼出核心程式碼

    // 1. 通過 為Proxy類指定類載入器物件 & 一組interface,從而建立動態代理類
    // >>關注3
    Class cl = getProxyClass(loader, interfaces); 

    // 2. 通過反射機制獲取動態代理類的建構函式,其引數型別是呼叫處理器介面型別
    Constructor cons = cl.getConstructor(constructorParams); 

    // 3. 通過動態代理類的建構函式 建立 代理類例項(傳入呼叫處理器物件
    return (Object) cons.newInstance(new Object[] { h }); 

// 特別注意 
// 1. 動態代理類繼承 Proxy 類 & 並實現了在Proxy.newProxyInstance()中提供的一系列介面(介面陣列)
// 2. Proxy 類中有一個對映表
  // 對映關係為:(<ClassLoader>,(<Interfaces>,<ProxyClass>) )
  // 即:1級key = 類載入器,根據1級key 得到 2級key = 介面陣列
  // 因此:1類載入器物件 + 1介面陣列 = 確定了一個代理類例項
...

// 回到呼叫關注2的原處
}


<-- 關注3:getProxyClass()原始碼分析 -->
// 作用:建立動態代理類

public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)  
throws IllegalArgumentException { 

  ...
  // 僅貼出關鍵程式碼

<-- 1. 將目標類所實現的介面載入到記憶體中 -->
    // 遍歷目標類所實現的介面  
    for (int i = 0; i < interfaces.length; i++) {  

        // 獲取目標類實現的介面名稱 
        String interfaceName = interfaces[i].getName();  
        Class interfaceClass = null;  
        try {  
        // 載入目標類實現的介面到記憶體中  
        interfaceClass = Class.forName(interfaceName, false, loader);  
        } catch (ClassNotFoundException e) {  
        }  
        if (interfaceClass != interfaces[i]) {  
        throw new IllegalArgumentException(  
            interfaces[i] + " is not visible from class loader");  
        }  
    }  

<-- 2. 生成動態代理類 -->       
        // 根據傳入的介面 & 代理物件 建立動態代理類的位元組碼
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(  
            proxyName, interfaces);  

            // 根據動態代理類的位元組碼 生成 動態代理類
            proxyClass = defineClass0(loader, proxyName,  
            proxyClassFile, 0, proxyClassFile.length);  

        }  
    // 最終返回動態代理類
    return proxyClass;  
    }  

// 回到呼叫關注3的原處

總結

  • 通過呼叫處理器類物件的.newProxyInstance()建立動態代理類 及其例項物件(需傳入目標類物件)
  • 具體過程如下:
    1. 通過 為Proy類指定類載入器物件 & 一組介面,從而建立動態代理類的位元組碼;再根據類位元組碼建立動態代理類
    2. 通過反射機制獲取動態代理類的建構函式(引數型別 = 呼叫處理器介面型別
    3. 通過動態代理類的建構函式 建立 代理類例項(傳入呼叫處理器物件

4.2 (關注2)通過呼叫動態代理物件方法從而呼叫目標物件方法

即,如何通過呼叫動態代理物件方法,從而呼叫目標物件方法?

// 使用程式碼
Buyer1_DynamicProxy.buybuybuy();
  • 在關注1中的 DynamicProxy.newProxyInstance()生成了一個動態代理類 及其例項

    該動態代理類記為 :$Proxy0


下面我們直接看該類實現及其 buybuybuy()
  • 該方法的邏輯如下:
<-- 動態代理類 $Proxy0 實現-->
// 繼承:Java 動態代理機制的主類:java.lang.reflect.Proxy
// 實現:與目標物件一樣的介面(即上文例子的Subject介面)
public final class $Proxy0 extends Proxy implements Subject {

// 建構函式
public ProxySubject(InvocationHandler invocationhandler)   
    {   
        super(invocationhandler);   
    }  

 // buybuybuy()是目標物件實現介面(Subject)中的方法
 // 即$Proxy0類必須實現
 // 所以在使用動態代理類物件時,才可以呼叫目標物件的同名方法(即上文的buybuybuy())
 public final void buybuybuy() {
        try {
            super.h.invoke(this, m3, null); 
            // 該方法的邏輯實際上是呼叫了父類Proxy類的h引數的invoke()
            // h引數即在Proxy.newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler h)傳入的第3個引數InvocationHandler物件
            // 即呼叫了呼叫處理器的InvocationHandler.invoke()
            // 而複寫的invoke()利用反射機制:Object result=method.invoke(proxied,args)
            // 從而呼叫目標物件的的方法 ->>關注4
            return;
        } catch (Error e) {
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

<-- 關注4:呼叫處理器 類複寫的invoke() -->
// 即步驟1中實現的類:DynamicProxy
// 內容很多都分析過了,直接跳到複寫的invoke()中

 public class DynamicProxy implements InvocationHandler {

    private Object ProxyObject;

    public Object newProxyInstance(Object ProxyObject){
        this.ProxyObject =ProxyObject;
        return Proxy.newProxyInstance(ProxyObject.getClass().getClassLoader(),
                ProxyObject.getClass().getInterfaces(),this);


    }

    //  複寫InvocationHandler介面的invoke()
    //  動態代理物件呼叫目標物件的任何方法前,都會呼叫呼叫處理器類的invoke()
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            // 引數說明:
            // 引數1:動態代理物件(即哪個動態代理物件呼叫了method()
            // 引數2:目標物件被呼叫的方法
            // 引數3:指定被呼叫方法的引數
            throws Throwable {
                System.out.println("代購出門了");
                Object result = null;
                // 通過Java反射機制呼叫目標物件方法
                result = method.invoke(ProxyObject, args);
        return result;
    }

總結

  • 動態代理類實現了與目標類一樣的介面,並實現了需要目標類物件需要呼叫的方法
  • 該方法的實現邏輯 = 呼叫父類 Proxy類的 h.invoke()

其中h引數 = 在建立動態代理例項中newProxyInstance(ClassLoader loader, Class<?>[]interfaces,InvocationHandler h)傳入的第3個引數InvocationHandler物件

  • InvocationHandler.invoke() 中通過反射機制,從而呼叫目標類物件的方法

4.3 原理總結

我用一張圖總結第4節說的關於動態代理模式的原始碼分析。

示意圖

至此,關於代理模式中的動態代理模式的相關知識已經講解完畢。

5. 總結

  • 我用兩張圖總結整篇文章的內容

示意圖

示意圖2

  • 本文主要對動態代理模式進行了全面介紹,接下來將介紹 其他設計模式,有興趣可以繼續關注Carson_Ho的安卓開發筆記!!!!

請幫頂 / 評論點贊!因為你的鼓勵是我寫作的最大動力!