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


一.什么是代理模式?

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

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

二.代理模式有什么用?

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

遠程代理:最經典的代理模式之一,遠程代理負責與遠程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;

/* 參考資料:
 * 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");

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

  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)上指定對象的方法時,使用遠程代理是一個不錯的選擇。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM