- 代理模式
- 引言
- 代理模式的定義與特點
- 代理模式的結構
- 模式實現
- 總結
- 與裝飾者模式
文章已收錄我的倉庫:Java學習筆記與免費書籍分享
代理模式
引言
代理模式是非常常見的模式,在生活中的例子也非常多,例如你不好意思向你關系不太好朋友幫個忙,這時需要找一個和它關系好的應一個朋友幫忙轉達,這個中間朋友就是代理對象。例如購買火車票不一定要去火車站買,可以通過12306網站或者去火車票代售點買。又如找女朋友、找保姆、找工作等都可以通過找中介完成。
代理模式的定義與特點
代理模式的定義:由於某些原因需要給某對象提供一個代理以控制對該對象的訪問。這時,訪問對象不適合或者不能直接引用目標對象,代理對象作為訪問對象和目標對象之間的中介。
考慮生活中一個常見的例子,客戶想買房,房東有很多房,提供賣房服務,但房東不會帶客戶看房,於是客戶通過中介買房。
你可能無法理解這里中介是代替客戶買房還是代替房東賣房,其實這是很好理解的。我們程序編寫代碼是為客戶服務的,中介是代替一名服務商處理業務,這種服務可能被定義為賣房
,也可能被定義為幫助客戶買房
,但中介唯獨不可能去實現買房的功能,在代碼中,我們定義的是服務於客戶的業務接口,而不是客戶的需求接口,如果讓客戶和中介都去實現買房接口,那么這里的買房就是一種業務,服務於賣房的客戶,這樣房東就是客戶端,買房的一方就是服務端。
但在生活中,買房的一方往往是客戶端,賣房的才是服務端,因此這里中介和房東都要實現賣房的接口方法,換句話說,中介是代替房東賣房而不是代替客戶買房。
客戶將中介抽象看成房東,直接從中介手中買房(中介==房東,提供賣房服務)。這里中介就是代理對象,客戶是訪問對象,房東是目標對象,實際由代理完全操控與目標對象的訪問,訪問對象客戶僅與代理對象交流。
,
代理模式的結構
代理模式的結構比較簡單,主要是通過定義一個繼承抽象主題的代理來包含真實主題,從而實現對真實主題的訪問,下面來分析其基本結構。
代理模式的主要角色如下。
- 抽象主題(Subject)類(業務接口類):通過接口或抽象類聲明真實主題和代理對象實現的業務方法,服務端需要實現該方法。
- 真實主題(Real Subject)類(業務實現類):實現了抽象主題中的具體業務,是代理對象所代表的真實對象,是最終要引用的對象。
- 代理(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
實例的方法如下:
-
定義一個類去實現
InvocationHandler
接口,這個接口下有一個invoke(Object proxy, Method method, Object[] args)
方法,它負責調用對應接口的接口方法;調用代理類的方法時,處理程序會利用反射,將代理類、代理類的方法、要調用代理類的參數傳入這個函數,並運行這個函數,這個函數是實際運行的,我們在這里編寫代理的核心代碼。
-
通過
Proxy.newProxyInstance()
創建某個interface
實例,它需要3個參數:- 使用的
ClassLoader
,通常就是接口類的ClassLoader
; - 需要實現的接口數組,至少需要傳入一個接口進去;
- 一個處理程序的接口。
這個方法返回一個代理類$Proxy0,它有三個參數,第一個通常是類本身的ClassLoader,第二個是該類要實現的接口,例如這里我們要實現增刪接口,第三個是一個處理程序接口,即調用這個類的方法時,這個類的方法會被委托給該處理程序,該處理程序做一些處理,這里對應了上面這個方法,通常設置為this。
- 使用的
-
將返回的
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++智能指針);例如上面的房產中介代理就是一種智能指引代理,代理附加了一些額外的功能,例如帶看房等。
代理模式的主要優點有:
- 代理模式在客戶端與目標對象之間起到一個中介作用和保護目標對象的作用;
- 代理對象可以擴展目標對象的功能;
- 代理模式能將客戶端與目標對象分離,在一定程度上降低了系統的耦合度,增加了程序的可擴展性;
其主要缺點是:
- 靜態代理模式會造成系統設計中類的數量增加,但動態代理可以解決這個問題;
- 在客戶端和目標對象之間增加一個代理對象,會造成請求處理速度變慢;
- 增加了系統的復雜度;
與裝飾者模式
我們實現的代理模式和裝飾者模式十分相像,但他們的目的不同。在上面我們提到過,某些代理會嚴格將訪問對象和受控對象分離開來,一個代理僅僅只負責一個類,這與裝飾器模式是不同的,對於裝飾器模式來說,目標對象就是訪問對象所持有的。此外虛擬代理的實現與裝飾者模式實現是不同的,虛擬代理一開始並不持有遠程服務器的資源對象,而是對域名和文件名進行解析才得到該對象,這與我們上面的代碼都是不同的,在我們的代碼中我們要么傳入一個實例,要么讓代理持有一個實例,但在虛擬代理中,我么傳入一個虛擬的文件資源,虛擬代理對遠程服務器進行解析才會獲得真實的對象實例,這一點也是不同的。