代理模式為另一個(gè)對(duì)象提供一個(gè)替身以控制對(duì)這個(gè)對(duì)象的訪問(wèn)。從定義可以看出,1. 代理模式提供了一個(gè)替身,即代理對(duì)象 2.
代理對(duì)象是為了控制對(duì)另一個(gè)對(duì)象(真實(shí)對(duì)象)的訪問(wèn),控制可以理解為做權(quán)限檢查、可行性判斷等。舉個(gè)例子,代理對(duì)象 = 經(jīng)紀(jì)人,真實(shí)對(duì)象 =
明星,如果某劇組想邀請(qǐng)明星出演電影,先將劇本給經(jīng)紀(jì)人,經(jīng)紀(jì)人先判斷劇組的真實(shí)性以及劇本的價(jià)值,如果是無(wú)良劇組或者垃圾劇本直接懟回去,這便是控制。如果劇組和劇本靠譜便轉(zhuǎn)交給明星處理,明星確定演不演把結(jié)果反饋給經(jīng)紀(jì)人,經(jīng)紀(jì)人再反饋結(jié)果給劇組,這個(gè)過(guò)程就可以理解為代理模式。代理模式有很多種,包括動(dòng)態(tài)代理、遠(yuǎn)程代理、虛擬代理等等,本章我們?cè)敿?xì)介紹動(dòng)態(tài)代理(Java版),簡(jiǎn)單介紹遠(yuǎn)程代理和虛擬代理。
動(dòng)態(tài)代理
之所以叫動(dòng)態(tài)是因?yàn)檫\(yùn)行時(shí)才將代理類創(chuàng)建出來(lái)。我們先由一個(gè)簡(jiǎn)單的需求引入動(dòng)態(tài)代理技術(shù),同時(shí)也會(huì)介紹面向?qū)ο笤O(shè)計(jì)原則。需求如下:在我們的業(yè)務(wù)當(dāng)中,需要將某些數(shù)據(jù)寫入本地磁盤做持久化,因此程序中需要封裝一個(gè)寫文件的類來(lái)滿足業(yè)務(wù)需求。前期的業(yè)務(wù)很簡(jiǎn)單,我們只需要定義能夠提供寫入文件的方法即可。因此,首先定義一個(gè)
Writer 接口,包含不同的寫方法,其次定義一個(gè)該接口的實(shí)現(xiàn)類,實(shí)現(xiàn)該接口定義的方法。
package com.cnblogs.duma.dp.proxy.dynamic; public interface Writer { public
void write(String fileName, String str); public void write(String fileName, byte
[] bs); } package com.cnblogs.duma.dp.proxy.dynamic; public class FileWriter
implements Writer { @Override public void write(String fileName, String str) {
System.out.println("call write str in FileWriter"); } @Override public void
write(String fileName,byte[] bs) { System.out.println("call write bytes in
FileWriter"); } }
之后我們用 Writer writer = new FileWriter();?
就可以完成向本地文件寫數(shù)據(jù)的功能了。這里其實(shí)不定義接口也能實(shí)現(xiàn)這個(gè)功能,至于為什么要定義接口下文會(huì)有解釋。至此,我們的小需求完成了,也上線了并能正常運(yùn)行。突然有一天運(yùn)維小哥說(shuō)了,為了保證
xxx ,需要在服務(wù)器預(yù)留 100G 磁盤空間, 也就是說(shuō)我們的應(yīng)用程序?qū)懘疟P的時(shí)候要判斷已有的磁盤空間,如果快到了 100G
臨界值,就不能再寫了。因此,我們需要改代碼,寫之前加上一個(gè)判斷當(dāng)前可用的磁盤空間的邏輯,本來(lái)我們可以直接改 FileWriter 的代碼。但存在兩個(gè)問(wèn)題 1.
改現(xiàn)有代碼風(fēng)險(xiǎn)高,可能改動(dòng)過(guò)程中影響原有邏輯,并且要重新進(jìn)行單元測(cè)試 2.
這個(gè)需求比較牽強(qiáng),跟我們的實(shí)際業(yè)務(wù)無(wú)關(guān),直接放在業(yè)務(wù)代碼里面導(dǎo)致耦合度比較大,不利于維護(hù)。因此,我們可以考慮使用代理模式解決這個(gè)問(wèn)題,即可以保證現(xiàn)有代碼不動(dòng),又可以低耦合地實(shí)現(xiàn)目前的需求。
package com.cnblogs.duma.dp.proxy.dynamic; import
java.lang.reflect.InvocationHandler;import java.lang.reflect.Method; public
class FileWriterInvocationHandler implements InvocationHandler { Writer writer =
null; public FileWriterInvocationHandler(Writer writer) { this.writer = writer;
} @Overridepublic Object invoke(Object proxy, Method method, Object[] args)
throws Exception { boolean localNoSpace = false; System.out.println("check
local filesystem space.");//檢測(cè)磁盤空間代碼,返回值可以更新 localNoSpace 變量 if (localNoSpace) {
throw new Exception("no space."); //如果空間不足,拋出空間不足的異常 } return
method.invoke(writer, args);//調(diào)用真實(shí)對(duì)象(FileWriter)的方法 } }
可以看到只增加了一個(gè)類,這個(gè)類有個(gè)特點(diǎn) 1. 它實(shí)現(xiàn)了 InvocationHandler 接口 2. 它的 invoke
方法實(shí)現(xiàn)了我們的需求并控制是否要調(diào)用真實(shí)對(duì)象。InvocationHandler 是 Java 動(dòng)態(tài)代理定義的一個(gè)接口,接口中定義了一個(gè) invoke
方法,我們調(diào)用代理對(duì)象的任何方法都會(huì)變成對(duì) FileWriterInvocationHandler 對(duì)象的 invoke 方法的調(diào)用, invoke
方法就是代理要做的事情。如果看到你覺(jué)得一頭霧水,沒(méi)關(guān)系繼續(xù)向下看將豁然開(kāi)朗。
到目前為止我們只看到新增了一個(gè) InvocationHandler
接口的實(shí)現(xiàn)類,并沒(méi)有看到代理對(duì)象。之前說(shuō)過(guò)之所以是動(dòng)態(tài)代理是因?yàn)樵谶\(yùn)行時(shí)才創(chuàng)建代理類,因此我們需要編寫一個(gè)驅(qū)動(dòng)程序,動(dòng)態(tài)創(chuàng)建代理對(duì)象,完成動(dòng)態(tài)代理的后半部分。
package com.cnblogs.duma.dp.proxy.dynamic; import java.lang.reflect.Proxy;
public class DynamicProxyDriver { public static void main(String[] args) { /**
* Proxy.newProxyInstance 包括三個(gè)參數(shù) * 第一個(gè)參數(shù):定義代理類的 classloader,一般用被代理接口的
classloader * 第二個(gè)參數(shù):需要被代理的接口列表 * 第三個(gè)參數(shù):實(shí)現(xiàn)了 InvocationHandler 接口的對(duì)象 * 返回值:代理對(duì)象*/
Writer writer= (Writer) Proxy.newProxyInstance( Writer.class.getClassLoader(),
new Class[]{Writer.class}, new FileWriterInvocationHandler(new FileWriter()));
//這就是動(dòng)態(tài)的原因,運(yùn)行時(shí)才創(chuàng)建代理類 try { writer.write("file1.txt", "text"); //調(diào)用代理對(duì)象的write方法 }
catch (Exception e) { e.printStackTrace(); } writer.write("file2.txt", new byte
[]{});//調(diào)用代理對(duì)象的write方法 } }
最關(guān)的一步是 Proxy.newProxyInstance ,該調(diào)用會(huì)創(chuàng)建代理對(duì)象,該代理對(duì)象會(huì)將我們需要代理的接口(Writer)和
InvocationHandler 實(shí)現(xiàn)類關(guān)聯(lián)起來(lái)。這樣代理對(duì)象就會(huì)有 Writer 接口的 2 個(gè)方法,針對(duì)我們的業(yè)務(wù)邏輯調(diào)用過(guò)程為:調(diào)用代理對(duì)象
writer 的 write 方法寫數(shù)據(jù) -> 轉(zhuǎn)到 FileWriterInvocationHandler 對(duì)象的 invoke 方法,判斷磁盤空間是否夠用
-> 拋出磁盤空間不足異常或調(diào)用 FileWriter 對(duì)象的 write 方法寫數(shù)據(jù)。在這里動(dòng)態(tài)代理涉及到了 Writer
接口及其實(shí)現(xiàn)類、InvocationHandler 接口及其實(shí)現(xiàn)類、代理類。動(dòng)態(tài)代理 UML 類圖如下:
可以看到代理類 Proxy 實(shí)現(xiàn)了 Writer 接口,因此可以調(diào)用 write 方法,同時(shí)代理類關(guān)聯(lián)
FileWriterInvocationHandler ,因此對(duì) write 方法的調(diào)用會(huì)變成對(duì) invoke 方法的調(diào)用。
至此,新的需求就完成了,我們結(jié)合代理模式談?wù)劥舜涡枨笞兏覀冇玫搅四男┖玫脑O(shè)計(jì)原則。
1. 我們沒(méi)有在原有 FileWriter 實(shí)現(xiàn)類中修改代碼, 而是新增了 FileInvocationHandler
實(shí)現(xiàn)新需求,這符合設(shè)計(jì)原則中的開(kāi)閉原則,即:對(duì)擴(kuò)展開(kāi)發(fā)對(duì)修改封閉。改動(dòng)現(xiàn)有代碼容易影響已有的正常代碼
2. 我們?cè)黾哟碇笾皇前?Writer writer = new FileWriter() 改為 Writer writer =
Proxy.newProxyInstance(...),由于都繼承了 Writer 接口,因此不需要修改 writer 的類型,
這符合面向接口的設(shè)計(jì)原則,讓我們盡量少的改動(dòng)現(xiàn)有代碼
動(dòng)態(tài)代理還有一個(gè)重要的應(yīng)用場(chǎng)景,我們可以在 invoke
方法中把待調(diào)用的方法名(method)和參數(shù)(args)發(fā)送到遠(yuǎn)程服務(wù)器,在遠(yuǎn)程服務(wù)器中完成調(diào)用并返回一個(gè)結(jié)果,這其實(shí)就是 RPC (remote
procedure call),即:遠(yuǎn)程過(guò)程調(diào)用。我在閱讀 Hadoop 源碼過(guò)程中發(fā)現(xiàn) Hadoop RPC 將動(dòng)態(tài)代理技術(shù)應(yīng)用在上述場(chǎng)景中。
遠(yuǎn)程代理?
個(gè)人覺(jué)得上述動(dòng)態(tài)代理第二個(gè)應(yīng)用場(chǎng)景算是遠(yuǎn)程代理的一個(gè)特例,因?yàn)檫h(yuǎn)程代理不一定非要?jiǎng)討B(tài)創(chuàng)建代理對(duì)象。接下來(lái)我們以 Java RMI 為例,
簡(jiǎn)單看下遠(yuǎn)程代理。RMI(remote method invocation)即:遠(yuǎn)程方法調(diào)用,與 RPC 類似,可以讓我們像調(diào)用 Java
本地方法一樣,調(diào)用遠(yuǎn)程的方法。這里就需要一個(gè)代理對(duì)象,它實(shí)現(xiàn)了本地的接口,其中序列化/反序列化以及網(wǎng)絡(luò)傳輸都在代理對(duì)象中實(shí)現(xiàn),
對(duì)我們透明,這也是控制了我們對(duì)遠(yuǎn)程對(duì)象的訪問(wèn)。代碼如下:
import java.rmi.Remote; import java.rmi.RemoteException; /** *
定義一個(gè)接口,接口中的方法要在遠(yuǎn)程調(diào)用*/ public interface MyRemote extends Remote { public String
sayHello()throws RemoteException; } import java.net.MalformedURLException;
import java.rmi.Naming; import java.rmi.RemoteException; import
java.rmi.server.UnicastRemoteObject;/** * 定義一個(gè)接口的遠(yuǎn)程實(shí)現(xiàn)類 * 為了讓遠(yuǎn)程對(duì)象擁有 “遠(yuǎn)程的”
功能,需要繼承 UnicastRemoteObject 類*/ public class MyRemoteImpl extends
UnicastRemoteObjectimplements MyRemote { protected MyRemoteImpl() throws
RemoteException { }/** * 客戶端通過(guò) rmi 代理對(duì)象調(diào)用 sayHello 方法,將會(huì)進(jìn)入到此方法 * @return *
@throws RemoteException */ @Override public String sayHello() throws
RemoteException { System.out.println("req from client."); return "Server says,
'Hey'"; } /** * 啟動(dòng)遠(yuǎn)程進(jìn)程的 main 方法 * @param args */ public static void
main(String[] args) {try { MyRemote service = new MyRemoteImpl(); Naming.rebind(
"RemoteHello", service);//將服務(wù)名和對(duì)應(yīng)的服務(wù)進(jìn)行綁定,客戶端會(huì)根據(jù) RemoteHello 找到遠(yuǎn)程服務(wù) } catch
(RemoteException e) { e.printStackTrace(); }catch (MalformedURLException e) {
e.printStackTrace(); } } }
這樣我們的遠(yuǎn)程服務(wù)已經(jīng)寫好了,還需要做以下 3 個(gè)工作來(lái)啟動(dòng)遠(yuǎn)程服務(wù)
1. 生成客戶端代理類,需要在 MyRemoteImpl.class 所在的目錄中執(zhí)行 rmic MyRemoteImpl 命令,將會(huì)生成
MyRemoteImpl_Stub.class 類。首先,rmic 命令是 jdk 自帶命令,所在的目錄與 java 和 javac
所在的目錄一樣;其次,我用的 Idea 創(chuàng)建的普通 Java 工程,我的 MyRemoteImpl.class
文件在“E:\backends\java-backends\java-ex\out\production\java-ex”目錄中,以我的工程為例,路徑以及命令執(zhí)行如下:
E:\backends\java-backends\java-ex\out\production\java-ex>rmic MyRemoteImpl
2. 啟動(dòng) rmiregistry,為了遠(yuǎn)程服務(wù)可以注冊(cè)服務(wù)名,在我們的 class
所在的目錄(“項(xiàng)目目錄\out\production\java-ex”)中執(zhí)行 rmiregistry 命令
E:\backends\java-backends\java-ex\out\production\java-ex>rmiregistry
3. 運(yùn)行 MyRemoteImpl 類,啟動(dòng)遠(yuǎn)程服務(wù)進(jìn)程
繼續(xù)編寫客戶端訪問(wèn)代碼,客戶端代碼主要是找到剛剛注冊(cè)的 RemoteHello 遠(yuǎn)程服務(wù),并獲得代理對(duì)象,調(diào)用代理對(duì)象上的方法。我們可以在同一個(gè)工程下,創(chuàng)建
MyRemoteClient 類
import java.net.MalformedURLException; import java.rmi.Naming; import
java.rmi.NotBoundException;import java.rmi.RemoteException; public class
MyRemoteClient {public static void main(String[] args) { try { /** *
找到遠(yuǎn)程服務(wù),并返回代理對(duì)象 * 該代理對(duì)象就是 MyRemoteImpl_Stub 且實(shí)現(xiàn)了 MyRemote 接口*/ MyRemote service
= (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello"); /** * 調(diào)用代理對(duì)象的
sayHello 方法,便會(huì)通過(guò)代理將調(diào)用發(fā)送到遠(yuǎn)程服務(wù)進(jìn)程并返回結(jié)果*/ String ret = service.sayHello();
System.out.println(ret); }catch (RemoteException e) { e.printStackTrace(); }
catch (NotBoundException e) { e.printStackTrace(); } catch
(MalformedURLException e) { e.printStackTrace(); } } }
我們可以直接運(yùn)行 MyRemoteClient 類,可以看到在剛啟動(dòng)的 MyRemoteImpl 進(jìn)程中,控制臺(tái)打印了?
req from client.
在 MyRemoteClient 進(jìn)程的控制臺(tái)中打印了
Server says, 'Hey'
至此我們的遠(yuǎn)程代理已經(jīng)介紹完畢。
虛擬代理
虛擬代理是作為創(chuàng)建開(kāi)銷大的對(duì)象的替身。舉一個(gè)我們常見(jiàn)的例子,在 Web 開(kāi)發(fā)或者移動(dòng)端開(kāi)發(fā)的時(shí)候經(jīng)常會(huì)用到 Image 組件,Image 組件一般要傳入一個(gè)
URL
參數(shù),從網(wǎng)絡(luò)上下載圖片到本地展示。假設(shè)這個(gè)組件要等到圖片下載完成才有顯示,那如果圖片較大或者網(wǎng)絡(luò)較慢,給用戶造成不好的體驗(yàn)。解決方法是我們可以先顯示一個(gè)
loading 狀態(tài)的默認(rèn)的本地圖片,當(dāng)遠(yuǎn)程圖片下載完成后重新渲染,替換掉當(dāng)前的 laoding 狀態(tài)的圖片。用虛擬代理來(lái)實(shí)現(xiàn)這個(gè)技術(shù)就可以定義一個(gè)
ImageProxy 類型,在該類中初始時(shí)候先展示一個(gè)默認(rèn)圖片,啟動(dòng)線程創(chuàng)建 Image 對(duì)象,Image
對(duì)象創(chuàng)建完畢,再重新渲染,替換默認(rèn)圖片。虛擬代理也是控制了對(duì) Image 對(duì)象的訪問(wèn)。
總結(jié)
本章主要介紹了代理模式,并且我們看到了代理模式常用的幾種變形,同時(shí)也接觸了面向?qū)ο蟮幕镜脑O(shè)計(jì)原則
動(dòng)態(tài)代理 - 程序運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建代理對(duì)象,所有的對(duì)代理對(duì)象方法的調(diào)用都會(huì)變成對(duì) InvocationHandler 的 invoke 方法的調(diào)用
遠(yuǎn)程代理 - 本地調(diào)用代理對(duì)象訪問(wèn)遠(yuǎn)程的方法,無(wú)需關(guān)心網(wǎng)絡(luò)通信細(xì)節(jié),跟調(diào)用本地方法一樣
虛擬代理 - 為了創(chuàng)建開(kāi)銷大的對(duì)象而存在
可以看到代理模式最核心就是控制,代理對(duì)象的目的就是控制對(duì)真實(shí)對(duì)象的訪問(wèn)。
本章主要參考《Head First 設(shè)計(jì)模式》
熱門工具 換一換
