一.什么是代理模式?
顧名思義,代理就是第三方,比如明星的經紀人,明星的事務都交給經紀人來處理,明星只要告訴經紀人去做什么,經紀人自然會想辦法去做,做完之后再把結果告訴明星就好了
本來是調用者與被調用者之間的直接交互,現在把調用者與被調用者分離開,由代理負責傳遞信息來完成調用
二.代理模式有什么用?
代理模式是一個很大的模式,所以應用很廣泛,從代理的種類就能看出來了:
遠程代理:最經典的代理模式之一,遠程代理負責與遠程JVM通信,以實現本地調用者與遠程被調用者之間的正常交互
虛擬代理:用來代替巨大對象,確保它在需要的時候才被創建
保護代理:給被調用者提供訪問控制,確認調用者的權限
此外還有防火牆代理,智能引用代理,緩存代理,同步代理,復雜隱藏代理,寫入時復制代理等等,都有各自特殊的用途
P.S.遠程代理是最基礎的代理模式,有必要單獨拿出來說說,所以本文對其作以詳細解釋,其余代理會在補充的博文中詳述
三.遠程代理
有些事情不用代理也能輕松解決,但有些事情必須得依靠代理來完成,比如要調用另一台機器上的一個方法,我們可能就不得不用代理
遠程代理的內部機制是這樣的:
解釋一下,Stub是“樁”也有人稱之為“存根”,代表了Server對象
Skeleton是“骨架”(不知道為什么叫“樁”和“骨架”,當然,也沒必要知道),代表了Client
Stub明明在客戶那邊,為什么不是客戶的代理而是服務的代理?因為客戶是要與服務器交互,現在服務在遠程JVM中,無法交互,所以用Stub來代表Server,調用Stub就等同於調用Server(內部通信機制對Client透明,對Client來說,調用Stub和直接調用Server沒什么區別,而這正是代理模式的優點之一)
具體流程是這樣的:
- Client向Stub發送方法調用請求(Client以為Stub就是Server)
- Stub接到請求,通過Socket與服務端的Skeleton通信,把調用請求傳遞給Skeleton
- Skeleton接到請求,調用本地Server(聽起來有點奇怪,這里Server相當於Service)
- Server作出對應動作,把結果返回給調用者Skeleton
- Skeleton接到結果之后通過Socket發送給Stub
- Stub把結果傳遞給Client
P.S.第2步與第5步都需要通過Socket通信,相互傳遞的東西都必須在發送前序列化,接收后反序列化,這也就解釋了為什么Server中的public方法返回值都必須是可序列化的
四.遠程代理的實現
有兩種方式可以實現遠程代理:
- 自定義Stub與Skeleton(實現其內部通信)
- 利用Java支持的RMI(Remote Method Invocation)來實現,可以省去很多麻煩,但不容易弄明白內部原理
首先給出自定義方式的例子,有一篇關於這個的博文很不錯,就擅自記錄下了鏈接,點我跳轉>>
原文給出了一個完整的例子,因此這里不再贅述,給原文補充一個偽類圖,方便理解:
(不要問我類圖為什么這么畫,說了是“偽”類圖。。)
仔細看看原文的話不難理解遠程代理,Stub和Skeleton負責通信,類似於用Socket編寫的聊天程序,除此之外沒什么特別的
-------
下面給出利用Java支持的RMI來實現代理模式,能夠明顯的感受到隱藏了很多細節
首先要定義遠程接口:
package ProxyPattern; import java.rmi.RemoteException; /** * 定義服務接口(擴展自java.rmi.Remote接口) * @author ayqy */ public interface Service extends java.rmi.Remote{ /* 1.方法返回類型必須是可序列化的Serializable * 2.每一個方法都要聲明異常throws RemoteException(因為是RMI方式) * */ /** * @return 完整的問候語句 * @throws RemoteException */ public String greet(String name) throws RemoteException; }
注意:服務接口中public方法的返回類型必須是可序列化的(換言之,自定義的返回類型必須實現Serializable接口),而String類型已經實現了Serializable接口
為什么要定義這樣一個擴展自java.rmi.Remote的接口?API文檔中給出了清晰的解釋:
The Remote
interface serves to identify interfaces whose methods may be invoked from a non-local virtual machine. Any object that is a remote object must directly or indirectly implement this interface. Only those methods specified in a "remote interface", an interface that extends java.rmi.Remote
are available remotely.
說白了就是為了告訴編譯器:我們的Service對象可以被遠程調用,僅此而已
-------
定義好了遠程接口,當然還需要一個具體實現:
package ProxyPattern; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; /** * 實現遠程服務(擴展自UnicastRemoteObject並實現自定義遠程接口) * @author ayqy */ public class MyService extends UnicastRemoteObject implements Service{ /** * 用來校驗程序版本(接收端在反序列化是會驗證UID,不符則引發異常) */ private static final long serialVersionUID = 1L; /** * 空的構造方法,只是為了聲明異常(默認的構造方法不會聲明異常) * @throws RemoteException */ protected MyService() throws RemoteException { } @Override public String greet(String name) throws RemoteException { return "Hey, " + name; } }
P.S.服務繼承UnicastRemoteObject類是為了自動生成Stub類(UnicastRemoteObject封裝了具體生成細節,我們省去了一個類的工作量)
-------
服務端有了服務還不夠,我們需要一個Server幫助我們啟動RMI注冊服務,並注冊遠程對象,供客戶端調用:
package ProxyPattern; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; /** * 實現服務器類,負責開啟服務並注冊服務對象 * @author ayqy */ public class Server { public static void main(String[] args){ try { //啟動RMI注冊服務,指定端口為1099 (1099為默認端口) LocateRegistry.createRegistry(1099); //創建服務對象 MyService service = new MyService(); //把service注冊到RMI注冊服務器上,命名為MyService Naming.rebind("MyService", service); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
注意,除了使用LocateRegistry.createRegistry()方式開啟服務外,還可以用命令行方式開啟(rmiregistry命令),效果一樣
服務端代碼到這里就完成了,就像Socket聊天程序一樣,我們需要寫兩部分代碼,Server與Client
-------
下面開始做客戶端的實現,非常簡單,就像一個測試類:
package ProxyPattern; /* 參考資料: * 1.JAVA RMI怎么用 * http://blog.csdn.net/afterrain/article/details/1819659 * 2.RMI內部原理 * http://www.cnblogs.com/yin-jingyu/archive/2012/06/14/2549361.html * */ import java.rmi.Naming; /** * 實現客戶類 * @author ayqy */ public class Client { /** * 查找遠程對象並調用遠程方法 */ public static void main(String[] argv) { try { //如果要從另一台啟動了RMI注冊服務的機器上查找MyService對象,修改IP地址即可 Service service = (Service) Naming.lookup("//127.0.0.1:1099/MyService"); //調用遠程方法 System.out.println(service.greet("SmileStone")); } catch (Exception e) { System.out.println("Client exception: " + e); } } }
P.S.我們直接用Naming.lookup()來獲取Stub對象(沒錯,是Stub,真正的對象還在另一台機器上呢,當然拿不到,這里得到的只是Service的Stub代理),再調用代理的方法獲取結果
注意這個細節:
//如果要從另一台啟動了RMI注冊服務的機器上查找MyService對象,修改IP地址即可 Service service = (Service) Naming.lookup("//127.0.0.1:1099/MyService");
雖然只是一句話,但隱藏了兩個細節:
- 客戶端必須知道服務接口Service,這里由於是在本地同一個package下,所以不用關心,在真正應用中Client與Server是分離的,所以Client需要拿到一份Service接口的Copy,否則無法調用
- 客戶端必須知道服務器的IP和端口號(通信嘛,沒有這個可不行)
-------
忙活了半天了,看看運行結果(先運行Server,再運行Client):
P.S.利用Java支持的RMI來實現遠程代理部分,參考的資料是別人的一篇博文,里面解釋的更詳細一些
P.S.至於命令行方式啟動RMI注冊服務,太麻煩了,而且需要先生成Stub類,不建議用這種方式,具體操作細節,上面的鏈接博文中也有詳細介紹,不再贅述
注意:直到我們親眼看到測試結果為止,始終沒有自定義Stub與Skeleton類,不是嗎?對,沒錯,它們確實存在,只是被RMI隱藏起來了(據說Java1.5之后RMI中沒有了Skeleton,甚至更高的版本中連Stub都沒有了。。不過這都沒關系,我們已經清楚了最原始的遠程代理)
五.總結
回過頭去想一想遠程代理做了些什么:
- 攔截並控制方法調用(這也是代理模式最大的特點,最典型的,防火牆代理。。)
- 遠程對象的存在對客戶是透明的(客戶完全把Stub代理對象當做遠程對象了,雖然客戶有點好奇為什么可能會出現異常。。)
- 遠程代理隱藏了通信細節
當我們需要調用另一台機器(JVM)上指定對象的方法時,使用遠程代理是一個不錯的選擇。。