去年買的Head First Pattern英文版,看了一點點,看起來還是比較吃力。。今年開始一點點的看,慢慢的看進去了,真是好書啊,一點點的從實際例子入手,一步步的、循序漸進的說明每一個設計模式,真是足夠的深入淺出!以前也看過閻宏的《Java與模式》,結合中國的傳統道家文化、儒家思想,甚至西游記、紅樓夢、女媧造人都用上了,說的是也算夠透徹了的,但是總感覺還是有些東西理解的不太深。
個人理解,代理模式在現實例子里,可以有非常多的變種,關鍵在于代理對象如何實現對真實對象的訪問控制。變化在于訪問控制的方式。著重說明下書中的3個例子,就是3種代理模式的使用場合。。
遠程代理
遠程代理的例子是java中的RMI。真是足夠深入淺出的,讓我以前對RMI非常模糊的印象也漸漸清晰起來。咱們一步步細細道來。。
第一步:定義遠程接口
1.繼承java.rmi.Remote接口
定義服務接口,服務接口必須繼承自Remote接口。Remote接口是一個標記接口,就是這個接口,沒有任何要實現的方法,僅僅是用來標識其實現類具有某種功能(個人理解),就像Serializable接口,僅僅表示實現這個接口的類能被序列化。
public interface MyRemote extends Remote {
2.服務接口中所有方法拋出RemoteException異常
RMI客戶端的方法調用其實是調用實現Remote接口的Stub(樁),樁的實現是基于網絡和IO的(底層就是socket),客戶端在調用方法過程中,任何錯誤都有可能發生,所以必須讓客戶端知道所發生的異常,并能捕捉。
import java.rmi.*;
public interface MyRemote extends Remote {
?? ?public String sayHello() throws RemoteException;
}
3.保證返回值和參數必須是可序列化的
遠程方法的參數要通過網絡傳輸,因此必須是可序列化的,返回值也是同樣。如果用原生類型(int、float等)、String、集合等,就沒問題,如果用自己的類型,必須實現Serializable接口(和Remote接口一樣,都是標記接口)。
第二步:實現遠程服務
1.實現遠程接口
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
?? ?public String sayHello(){
?? ??? ?return "Hello, I'm server.";
?? ?}
}
2.繼承UnicastRemoteObject?
要想成為一個遠程服務對象,需要有遠程的功能。最簡單的方法就是實現UnicastRemoteObject方法了。
3.聲明一個無參數的構造函數,且拋出RemoteException
public MyRemoteImpl() throws RemoteException{}
4.用RMI registry注冊服務
實現遠程服務后,要布遠程服務供客戶端使用。要實例化一個遠程服務,放入RMI注冊表中。注冊了服務實現對象后,RMI會把Stub(樁)放入注冊表,讓客戶端使用。
try{
?? ?MyRemote service = new MyRemoteImpl();
?? ?Naming.rebind("RemoteHello",service);
}catch(Exception e){
?? ?// ...
}
第三步:生成Stub和Skeletons(樁和骨架)
1.在遠程實現類上運行rmic(不是遠程接口)
rmic MyRemoteImpl(類名,不帶.class)
會生成樁和骨架代碼:MyRemoteImpl_Stub.class、MyRemoteImpl_Skel.class
rmic是jdk bin目錄下的工具
第四步:運行rmiregistry
1.rmiregistry
必須讓rmiregistry能訪問到你的服務相關類,要么把類放入classpath,要么在classes目錄下直接運行rmiregistry
第五步:啟動服務
1.另一個dos窗口里啟動服務類
java MyRemoteImpl
客戶端調用方法:
MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/RemoteHello");
String msg = service.sayHello();// 調用樁的方法
通過RMI registry查找服務后,返回樁,客戶端必須用MyRemoteImpl_Stub.class和MyRemote.class。樁MyRemoteImpl_Stub.class、骨架MyRemoteImpl_Skel.class、MyRemote.class、MyRemoteImpl.class必須在服務端。
這么多亂七八糟的跟代理模式有什么關系?
其實客戶端返回的MyRemote,其實是MyRemote_Stub,就是代理對象了,服務端的MyRemoteImpl及時實際對象,通過RMI,來獲得遠程對象的代理,再通過代理,來訪問實際對象(遠程服務實現類MyRemoteImpl)所實現的遠程服務方法(MyRemote定義)。對應類圖,每個類在代理模式中的角色分別是:
Subject:MyRemote接口
RealObject:MyRemoteImpl服務實現類
Proxy:MyRemote_Stub樁
在RMI中,找到服務后,拿到的MyRemote service其實是一個代理對象(MyRemote_Stub),對代理對象的方法調用,實際是通過RMI來訪問遠程服務實現對象的方法。也就是說代理對象MyRemote service(實際是MyRemote_Stub)通過RMI機制對遠程服務對象來做訪問控制,也就實現了代理模式。
虛擬代理
虛擬代理舉的是一個Swing的例子。
我是這么理解的:一個對象的創建非常耗時,通過代理對象去調用,在真實對象創建前,返回一個假的調用,等真實對象創建好了,這時候返回給客戶端的就是一個真實對象的相應方法調用。
也就是延遲加載的問題,Swing例子中,要顯示一個Icon,但是要通過網絡加載一個圖片,在圖片通過網絡加載成功前,先顯示一個“加載中,請稍候...”(如果是真實對象的調用,應該顯示一個圖片),在代理對象中通過后臺線程去加載圖片,加載完了后,再偷偷的把“加載中,請稍候...”的字樣偷偷換成加載成功后的圖片。
沒想到這也算代理模式的一種應用場景。以前有這么在Swing中用過,需要從數據庫中查找數據,但是比較耗時,就先顯示“加載數據中,請稍候...”,等加載完了,再在JTable中顯示出來。如果用代理模式的方式來思考,好像比較的好吧。。
同樣在jsp頁面里,通過ajax來加載數據好像也是這樣的道理,數據沒加載之前就是“加載中...”,加載完了再通過innerHTML來改變顯示,也是同樣的延遲加載問題。
如果用代理模式的方式來考慮,可以定義一個JavaScript類(這個類其實是個代理),這個類有個方法要顯示一些從Server取出的數據,但是調用顯示方法時,后臺數據還沒有加載,就先顯示加載中請稍候之類的文本,這時候通過ajax從Server取數據(創建真實對象),取出來之后在回調函數中更新顯示HTML元素的innerHTML。跟那個Swing的例子一模一樣吧。不過好像JavaScript中好像沒有誰會定義接口、實現、代理對象吧,但是思路其實是一樣的。
不知道這樣理解代理模式,算不算曲解。。。
JDK動態代理
jdk里的動態代理支持,主要是通過java.lang.reflect包中Proxy、InvocationHandler等幾個類來實現的。具體如何實現可參考JDK中文文檔。
使用場合:
好像在在一本Hibernate的書上,對數據庫Connection的close方法調用,用動態代理的方式來攔截,并不真正關閉連接,而是返回到數據庫連接池中。
在Spring中的攔截貌似有些是用動態代理實現的?不過動態代理使用時要基于接口,但是Spring是使用動態生成字節碼的方式?對Spring內部實現機制不熟。。不敢妄自猜測。。等有時間好好研究再來說明。。
動態代理,我覺得最好的使用場合是給方法調用增加預處理和后處理,更加靈活了,可以做一些額外的事,同時也做到無侵入的解耦合,因為代理對象和實際對象的接口是一樣的,唯一需要注意的地方是,客戶端調用者是拿的接口,接口到底是使用代理對象還是實際對象,調用者并不知道,這就需要對代理對象的創建用類似工廠的方式來封裝創建。比如一下代碼:
PersonBean getOwnerProxy(PersonBean person){
?? ?return (PersonBean)Proxy.newProxyInstance(
?? ??? ? ? ? person.getClass().getClassLoader(),
?? ??? ??? ? person.getClass().getInterfaces(),
?? ??? ??? ? new OwnerInvocationHandler(person));
}
PersonBean為Subject接口,OwnerInvocationHandler實現InvocationHandler接口。
和Decorator的比較
Decorator模式在jdk的java.io包中使用非常廣泛。主要用來為一個類添加新的行為。
而Proxy模式中,代理對象并不對實際對象添加新的行為,只是對實際對象做訪問控制。