1. 程式人生 > 其它 >一文徹底搞懂代理模式(Proxy)

一文徹底搞懂代理模式(Proxy)

代理模式

引言

代理模式是非常常見的模式,在生活中的例子也非常多,例如你不好意思向你關係不太好朋友幫個忙,這時需要找一個和它關係好的應一個朋友幫忙轉達,這個中間朋友就是代理物件。例如購買火車票不一定要去火車站買,可以通過12306網站或者去火車票代售點買。又如找女朋友、找保姆、找工作等都可以通過找中介完成。

代理模式的定義與特點

代理模式的定義:由於某些原因需要給某物件提供一個代理以控制對該物件的訪問。這時,訪問物件不適合或者不能直接引用目標物件,代理物件作為訪問物件和目標物件之間的中介。

考慮生活中一個常見的例子,客戶想買房,房東有很多房,提供賣房服務,但房東不會帶客戶看房,於是客戶通過中介買房。

你可能無法理解這裡中介是代替客戶買房還是代替房東賣房,其實這是很好理解的。我們程式編寫程式碼是為客戶服務的,中介是代替一名服務商處理業務,這種服務可能被定義為賣房,也可能被定義為幫助客戶買房,但中介唯獨不可能去實現買房的功能,在程式碼中,我們定義的是服務於客戶的業務介面,而不是客戶的需求介面,如果讓客戶和中介都去實現買房介面,那麼這裡的買房就是一種業務,服務於賣房的客戶,這樣房東就是客戶端,買房的一方就是服務端。

但在生活中,買房的一方往往是客戶端,賣房的才是服務端,因此這裡中介和房東都要實現賣房的介面方法,換句話說,中介是代替房東賣房而不是代替客戶買房。

客戶將中介抽象看成房東,直接從中介手中買房(中介==房東,提供賣房服務)。這裡中介就是代理物件,客戶是訪問物件,房東是目標物件,實際由代理完全操控與目標物件的訪問,訪問物件客戶僅與代理物件交流。

代理模式的結構

代理模式的結構比較簡單,主要是通過定義一個繼承抽象主題的代理來包含真實主題,從而實現對真實主題的訪問,下面來分析其基本結構。

代理模式的主要角色如下。

  1. 抽象主題(Subject)類(業務介面類):通過介面或抽象類宣告真實主題和代理物件實現的業務方法,服務端需要實現該方法。
  2. 真實主題(Real Subject)類(業務實現類):實現了抽象主題中的具體業務,是代理物件所代表的真實物件,是最終要引用的物件。
  3. 代理(Proxy)類:提供了與真實主題相同的介面,其內部含有對真實主題的引用,它可以訪問、控制或擴充套件真實主題的功能。

其結構圖如圖 1 所示。


圖1 代理模式的結構圖

在程式碼中,一般代理會被理解為程式碼增強,實際上就是在原始碼邏輯前後增加一些程式碼邏輯,而使呼叫者無感知

模式實現

根據代理的建立時期,代理模式分為靜態代理和動態代理。

  • 靜態:由程式設計師建立代理類或特定工具自動生成原始碼再對其編譯,在程式執行前代理類的 .class 檔案就已經存在了。
  • 動態:在程式執行時,運用反射機制動態建立而成。

靜態代理

靜態代理服務於單個介面,我們來考慮實際工程中的一個例子,現在已經有業務程式碼實現一個增刪功能,原有的業務程式碼由於仍有大量程式無法改變,現在新增需求,即以後每執行一個方法輸出一個日誌。

我們不改變原有程式碼而新增一個代理來實現:

//業務介面
interface DateService {
    void add();
    void del();
}

class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("成功新增!");
    }

    @Override
    public void del() {
        System.out.println("成功刪除!");
    }
}

class DateServiceProxy implements DateService {
    DateServiceImplA server = new DateServiceImplA();

    @Override
    public void add() {
        server.add();
        System.out.println("程式執行add方法,記錄日誌.");
    }
    @Override
    public void del() {
        server.del();
        System.out.println("程式執行del方法,記錄日誌.");
    }
}

//客戶端
public class Test {
    public static void main(String[] args) {
        DateService service = new DateServiceProxy();
        service.add();
        service.del();
    }
}

現在,我麼成功的在不改變程式原有程式碼的情況下,擴充套件了一些功能!

我們來思考一下這種情況,當原有的業務處理由於某種原因無法改變,而目前又需要擴充套件一些功能,此時可以通過代理模式實現:

如上圖所示,我們原有的業務十分龐大,牽一髮而動全身,難以修改,而現在需要擴充套件一些功能,這裡就需要代理模式實現,在縱向程式碼之間,橫向擴充套件一些功能,這也是所謂的面向切面程式設計。

如果你設計思想比較良好的話,你很快就能發現上面程式碼的不足:一個代理只能服務於一個特定的業務實現類,假設我們又另外一個類也實現了業務介面,即class DateServiceImplB implements DateService,發現想要擴充套件該類必須要為其也編寫一個代理,擴充套件性極低。想要解決這個問題也是很簡單的,我們面向介面程式設計而不是面向實現,我們給代理類持有介面而不是持有具體的類:

class DateServiceProxy implements DateService {
    DateService server;

    public DateServiceProxy(DateService server) {
        this.server = server;
    }
}

這樣一個代理就可以同時代理多個實現了同一個業務介面的業務,但這種方式必須要求客戶端傳入一個具體的實現類,這樣客戶就必須要獲得具體目標物件例項,目標物件就直接暴露在訪問物件面前了,對於某些情況這是不可接受的,例如你想獲得某資源,但需要一定的許可權,這時由代理控制你對目標資源物件的訪問,不能由你直接區去訪問,這是代理就必須將目標資源物件牢牢的控制在自己手中,後面會講到這其實就是保護代理。但在這裡,這種方法是可以接受的,並且帶給程式較高的靈活性。

動態代理

我們為什麼需要動態代理?要理解這一點,我們必須要知道靜態代理有什麼不好,要實現靜態代理,我們必須要提前將代理類硬編碼在程式中,這是固定死的,上面也提到過,有一些代理一個代理就必須要負責一個類,這種情況下代理類的數量可能是非常多的,但我們真的每個代理都會用上嗎?例如,在普通的專案中,可能99%的時間都僅僅只是簡單的查詢,而不會設計到增刪功能,此時是不需要我們的增刪代理類的,但在靜態代理中,我們仍然必須硬編碼代理類,這就造成了不必要的資源浪費並且增加了程式碼量。

動態代理可以幫助我們僅僅在需要的時候再建立代理類,減少資源浪費,此外由於動態代理是一個模板的形式,也可以減少程式的程式碼量,例如在靜態程式碼示例中,我們在每個方法中加入System.out.println("程式執行***方法,記錄日誌.");,當業務方法非常多時,我們也得為每個業務方法加上記錄日誌的語句,而動態代理中將方法統一管理,無論幾個業務方法都只需要一條記錄語句即可實現,具體請看程式碼。

動態代理採用反射的機制,在執行時建立一個介面類的例項。在JDK的實現中,我們需要藉助Proxy類和InvocationHandler介面類。

在執行期動態建立一個interface例項的方法如下:

  1. 定義一個類去實現InvocationHandler介面,這個介面下有一個invoke(Object proxy, Method method, Object[] args) 方法,它負責呼叫對應介面的介面方法;

    呼叫代理類的方法時,處理程式會利用反射,將代理類、代理類的方法、要呼叫代理類的引數傳入這個函式,並執行這個函式,這個函式是實際執行的,我們在這裡編寫代理的核心程式碼。

  2. 通過Proxy.newProxyInstance()建立某個interface例項,它需要3個引數:

    1. 使用的ClassLoader,通常就是介面類的ClassLoader
    2. 需要實現的介面陣列,至少需要傳入一個介面進去;
    3. 一個處理程式的介面。

    這個方法返回一個代理類$Proxy0,它有三個引數,第一個通常是類本身的ClassLoader,第二個是該類要實現的介面,例如這裡我們要實現增刪介面,第三個是一個處理程式介面,即呼叫這個類的方法時,這個類的方法會被委託給該處理程式,該處理程式做一些處理,這裡對應了上面這個方法,通常設定為this。

  3. 將返回的Object強制轉型為介面。

來看一下具體實現:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//業務介面
interface DateService {
    void add();
    void del();
}

class DateServiceImplA implements DateService {
    @Override
    public void add() {
        System.out.println("成功新增!");
    }

    @Override
    public void del() {
        System.out.println("成功刪除!");
    }
}

class ProxyInvocationHandler implements InvocationHandler {
    private DateService service;

    public ProxyInvocationHandler(DateService service) {
        this.service = service;
    }

    public Object getDateServiceProxy() {
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), service.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var result = method.invoke(service, args); // 讓service呼叫方法,方法返回值
        System.out.println(proxy.getClass().getName() + "代理類執行" + method.getName() + "方法,返回" + result +  ",記錄日誌!");
        return result;
    }
}

//客戶端
public class Test {
    public static void main(String[] args) {
        DateService serviceA = new DateServiceImplA();
        DateService serviceProxy = (DateService) new ProxyInvocationHandler(serviceA).getDateServiceProxy();
        serviceProxy.add();
        serviceProxy.del();
    }
}
/*
成功新增!
$Proxy0代理類執行add方法,返回null,記錄日誌!
成功刪除!
$Proxy0代理類執行del方法,返回null,記錄日誌!
*/

我們代理類是通過Proxy.newProxyInstance(this.getClass().getClassLoader(),service.getClass().getInterfaces(), this);方法得到的,這個方法中,第二個引數我們傳入了類service的介面部分,即DateService,在底層通過該介面的位元組碼幫我們建立一個新類$Proxy0,該類具有介面的全部方法。第三個引數是一個處理程式介面,此處傳入this即表明將方法交給ProxyInvocationHandler 的介面即InvocationHandler的invoke方法執行。

$Proxy並不具備真正處理的能力,當我們呼叫$$Proxy0.add()時,會陷入invoke處理程式,這是我們編寫核心程式碼的地方,在這裡var result = method.invoke(service, args);呼叫目標物件的方法,我們可以編寫代理的核心程式碼。

總結

代理模式通常有如下幾種用途:

  • 遠端代理,這種方式通常是為了隱藏目標物件存在於不同地址空間的事實,方便客戶端訪問。例如,使用者申請某些網盤空間時,會在使用者的檔案系統中建立一個虛擬的硬碟,使用者訪問虛擬硬碟時實際訪問的是網盤空間。

  • 虛擬代理,這種方式通常用於要建立的目標物件開銷很大時。例如,下載一幅很大的影象需要很長時間,因某種計算比較複雜而短時間無法完成,這時可以先用小比例的虛擬代理替換真實的物件,消除使用者對伺服器慢的感覺。

  • 保護代理,當對目標物件訪問需要某種許可權時,保護代理提供對目標物件的受控保護,例如,它可以拒絕服務許可權不夠的客戶。

  • 智慧指引,主要用於呼叫目標物件時,代理附加一些額外的處理功能。例如,增加計算真實物件的引用次數的功能,這樣當該物件沒有被引用時,就可以自動釋放它(C++智慧指標);例如上面的房產中介代理就是一種智慧指引代理,代理附加了一些額外的功能,例如帶看房等。

代理模式的主要優點有:

  • 代理模式在客戶端與目標物件之間起到一箇中介作用和保護目標物件的作用;
  • 代理物件可以擴充套件目標物件的功能;
  • 代理模式能將客戶端與目標物件分離,在一定程度上降低了系統的耦合度,增加了程式的可擴充套件性;

其主要缺點是:

  • 靜態代理模式會造成系統設計中類的數量增加,但動態代理可以解決這個問題;
  • 在客戶端和目標物件之間增加一個代理物件,會造成請求處理速度變慢;
  • 增加了系統的複雜度;

與裝飾者模式

我們實現的代理模式和裝飾者模式十分相像,但他們的目的不同。在上面我們提到過,某些代理會嚴格將訪問物件和受控物件分離開來,一個代理僅僅只負責一個類,這與裝飾器模式是不同的,對於裝飾器模式來說,目標物件就是訪問物件所持有的。此外虛擬代理的實現與裝飾者模式實現是不同的,虛擬代理一開始並不持有遠端伺服器的資源物件,而是對域名和檔名進行解析才得到該物件,這與我們上面的程式碼都是不同的,在我們的程式碼中我們要麼傳入一個例項,要麼讓代理持有一個例項,但在虛擬代理中,我麼傳入一個虛擬的檔案資源,虛擬代理對遠端伺服器進行解析才會獲得真實的物件例項,這一點也是不同的。