跳到主要內容
黯羽輕揚每天積累一點點

設計模式之代理模式(Proxy Pattern)_遠程代理解析

免費2015-03-07#Design_Pattern#代理模式#Proxy Pattern

代理模式通過插入第三方(代理對象)來分離調用者和被調用者(不同於執行者),而遠程代理是最經典的代理之一,被調用者不在本地(處於另一個 JVM 中),無法直接調用它,此時就需要一個遠程代理,調用者把調用請求發送給遠程代理,代理對象和被調用者通信,再把調用結果傳遞給調用者。

no_mkd

一.什麼是代理模式?

顧名思義,代理就是第三方,比如明星的經紀人,明星的事務都交給經紀人來處理,明星只要告訴經紀人去做什麼,經紀人自然會想辦法去做,做完之後再把結果告訴明星就好了

本來是調用者與被調用者之間的直接交互,現在把調用者與被調用者分離開,由代理負責傳遞信息來完成調用

二.代理模式有什麼用?

代理模式是一個很大的模式,所以應用很廣泛,從代理的種類就能看出來了:

遠程代理:最經典的代理模式之一,遠程代理負責與遠程 JVM 通信,以實現本地調用者與遠程被調用者之間的正常交互

虛擬代理:用來代替巨大對象,確保它在需要的時候才被創建

保護代理:給被調用者提供訪問控制,確認調用者的權限

此外還有防火牆代理,智能引用代理,緩存代理,同步代理,複雜隱藏代理,寫入時複製代理等等,都有各自特殊的用途

P.S.遠程代理是最基礎的代理模式,有必要單獨拿出來說說,所以本文對其作以詳細解釋,其餘代理會在補充的博文中詳述

三.遠程代理

有些事情不用代理也能輕鬆解決,但有些事情必須得依靠代理來完成,比如要調用另一台機器上的一個方法,我們可能就不得不用代理

遠程代理的內部機制是這樣的:

解釋一下,Stub 是「樁」也有人稱之為「存根」,代表了 Server 對象

Skeleton 是「骨架」(不知道為什麼叫「樁」和「骨架」,當然,也沒必要知道),代表了 Client

Stub 明明在客戶那邊,為什麼不是客戶的代理而是服務的代理?因為客戶是要與服務器交互,現在服務在遠程 JVM 中,無法交互,所以用 Stub 來代表 Server,調用 Stub 就等同於調用 Server(內部通信機制對 Client 透明,對 Client 來說,調用 Stub 和直接調用 Server 沒什麼區別,而這正是代理模式的優點之一)

具體流程是這樣的:

  1. Client 向 Stub 發送方法調用請求(Client 以為 Stub 就是 Server)
  2. Stub 接到請求,通過 Socket 與服務端的 Skeleton 通信,把調用請求傳遞給 Skeleton
  3. Skeleton 接到請求,調用本地 Server(聽起來有點奇怪,這裡 Server 相當於 Service)
  4. Server 作出對應動作,把結果返回給調用者 Skeleton
  5. Skeleton 接到結果之後通過 Socket 發送給 Stub
  6. 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;

/* 參考資料:

/**

  • 實現客戶類
  • @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");

雖然只是一句話,但隱藏了兩個細節:

  1. 客戶端必須知道服務接口 Service,這裡由於是在本地同一個 package 下,所以不用關心,在真正應用中 Client 與 Server 是分離的,所以 Client 需要拿到一份 Service 接口的 Copy,否則無法調用
  2. 客戶端必須知道服務器的 IP 和端口號(通信嘛,沒有這個可不行)

忙活了半天了,看看運行結果(先運行 Server,再運行 Client):

P.S.利用 Java 支持的 RMI 來實現遠程代理部分,參考的資料是別人的一篇博文,裡面解釋的更詳細一些

P.S.至於命令行方式啟動 RMI 註冊服務,太麻煩了,而且需要先生成 Stub 類,不建議用這種方式,具體操作細節,上面的鏈接博文中也有詳細介紹,不再贅述

注意:直到我們親眼看到測試結果為止,始終沒有自定義 Stub 與 Skeleton 類,不是嗎?對,沒錯,它們確實存在,只是被 RMI 隱藏起來了(據說 Java1.5 之後 RMI 中沒有了 Skeleton,甚至更高的版本中連 Stub 都沒有了。。不過這都沒關係,我們已經清楚了最原始的遠程代理)

五.總結

回過頭去想一想遠程代理做了些什麼:

  1. 攔截並控制方法調用(這也是代理模式最大的特點,最典型的,防火牆代理。。)
  2. 遠程對象的存在對客戶是透明的(客戶完全把 Stub 代理對象當做遠程對象了,雖然客戶有點好奇為什麼可能會出現異常。。)
  3. 遠程代理隱藏了通信細節

當我們需要調用另一台機器(JVM)上指定對象的方法時,使用遠程代理是一個不錯的選擇。。

評論

暫無評論,快來發表你的看法吧

提交評論