no_mkd
一.什麼是代理模式?
顧名思義,代理就是第三方,比如明星的經紀人,明星的事務都交給經紀人來處理,明星只要告訴經紀人去做什麼,經紀人自然會想辦法去做,做完之後再把結果告訴明星就好了
本來是調用者與被調用者之間的直接交互,現在把調用者與被調用者分離開,由代理負責傳遞信息來完成調用
二.代理模式有什麼用?
代理模式是一個很大的模式,所以應用很廣泛,從代理的種類就能看出來了:
遠程代理:最經典的代理模式之一,遠程代理負責與遠程 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)上指定對象的方法時,使用遠程代理是一個不錯的選擇。。
暫無評論,快來發表你的看法吧