Java設計模式—代理模式之遠端代理
遠端代理
遠端代理是一種常用的代理模式,它使得客戶端程式可以訪問在遠端主機上的物件。為一個位於不同地址空間的物件提供一個本地的代理物件,因此,在客戶端完全可以認為被代理的遠端業務物件是在本地而不是遠端。下圖為遠端代理示意圖
Java語言中可以通過一種名為RMI(Remote Method Invocation,遠端方法呼叫)的方法來實現遠端代理,它能夠實現一個Java虛擬機器的物件呼叫另一個Java虛擬機器中的物件。
RMI將客戶輔助物件稱為Stub(樁)即本地代理,服務輔助物件稱為skeleton(骨架)
下面簡單介紹下RMI工作流程:
①客戶端發起請求,將請求轉交至RMI客戶端的Stub類。
②Stub將這些請求命令進行編碼(序列化,方便在網路上進行傳輸),並將他們通過Socket傳送到伺服器
③伺服器接收到流後將他們轉發到相應的Skeleton類,Skeleton類將這些請求資訊進行反序列化,然後找出真正被呼叫的方法,以及該方法所在的物件。
④伺服器處理完結果之後,將結果傳送給Skeleton類,Skeleton類將結果進行編碼(序列化),再次通過Socket傳送給客戶端。
⑤客戶端Stub接收到流後,再進行反序列化,然後將反序列化後的結果返還給客戶端呼叫者。
至此,一次完整的呼叫過程得以完成。下面將通過具體的例項,更加具體的描述實現過程。
RMI應用開發流程: 首先給出流程圖: PS:圖片插入不進來了(https://blog.csdn.net/QB2049_XG/article/details/3278672#p3)。這個部落格裡面有流程圖。
下面來一步一步的實現開發一個簡單的RMI應用:
①定義一個公共介面,我們大家都知道代理模式有一個Subject抽象主題角色,遠端代理也一樣。需要定義一個公共的介面。
package lib.complex.server; /** * @description:Subject抽象公共介面 */ import java.rmi.Remote; import java.rmi.RemoteException; public interface MyRmiInterface extends Remote { public String sayHello() throws RemoteException; }
②實現遠端介面:
客戶端呼叫伺服器的遠端物件來呼叫公共介面的方法,所以伺服器端必須得實現這個公共介面,並且丟擲一個遠端物件讓客戶端來進行呼叫。
package lib.complex.server; /** * @description:實現遠端介面 */ import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class MyRmiImpl extends UnicastRemoteObject implements MyRmiInterface { //在例項該類時,就匯出了一個遠端呼叫物件,該構造方法是必須的 public MyRmiImpl()throws RemoteException { super(); } public String sayHello(){ return "Hello,world"; } }
這裡發現實現遠端介面類繼承了UnicastRemoteObject類,這點是非常重要的。通常,遠端物件都繼承UnicastRemoteObject,該類提供了遠端物件的建立和匯出的一系列方法。這個類我們將在隨後的服務端例項化並把其註冊到RMI登錄檔中。
③實現服務端類:
package lib.complex.server;
/**
* @description:實現服務端類
*/
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class MyRmiServer {
public static void main(String args[]){
try{
//例項化遠端物件,同時匯出了該物件,使他可以接受來自客戶端的資訊
MyRmiImpl impl = new MyRmiImpl();
//獲取本地登錄檔物件
Registry registry = LocateRegistry.getRegistry();
//在登錄檔中繫結遠端物件,樁
registry.bind("Hello",impl);
System.out.println("System already!");
}catch (Exception e){
System.out.println("在建立遠端連線的情況出現了異 常"+e.getMessage());
e.printStackTrace();
}
}
}
服務端類建立了一個遠端物件,將他和一個名字綁定了登錄檔register中。為了能讓客戶端呼叫遠端物件的方法,呼叫者必須事先獲得遠端物件的樁。為了自引導, Java RMI 為應用程式提供了一個登錄檔API,用於把遠端物件的樁繫結到一個名字,這樣遠端客戶端依靠查詢該名字就能獲得對應的樁。JavaRMI登錄檔只是一個允許客戶端獲得遠端物件樁的簡單-名字服務。一旦遠端物件被註冊到登錄檔中,呼叫者就可以查詢名字來獲得該物件及引用,然後呼叫物件的方法。
④實現客戶端類:
package lib.complex.client;
/**
* @description:實現客戶端呼叫類
*/
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class MyRmiClient {
public static void main(String args[]){
try{
//獲得執行rmiregistry服務的主機上的登錄檔
Registry registry = LocateRegistry.getRegistry();
//查詢並獲得遠端物件的存根stub
MyRmiInterface stub = (MyRmiInterface)registry.lookup("Hello");
//像在使用本地物件方法那樣,呼叫遠端方法
String response = stub.sayHello();
System.out.println("Response:" + response);
}catch (Exception e){
System.out.println("未設定codebase");
}
}
}
可以看到客戶端的程式碼可以非常簡單的。下面看看怎麼執行RMI這個簡單例項:
首先需要明確哪些類存在於客戶端,哪些類存在於服務端。
存在於客服端的類:公共介面類,客戶端呼叫類。
存在於伺服器的類:公共介面類,實現遠端介面類,實現服務端類。
PS:我的服務端的類的路徑為:D:\RMI\src\lib\complex\server
我的客戶端的類的路徑為:D:\RMI\src\lib\complex\client
①首先編譯服務端的原始檔:
可以看到我首先使用set JAVA_TOOL_OPTIONS=-Dfile.encoding=utf-8設定了編碼方式為UTF-8。然後使用javac -d . *.java命令來進行編譯,而不是簡單的javac *.java。從編譯後的資料夾來看
使用javac -d . *.java進行編譯後會出現一個資料夾,lib\complex\server\,然後底下才是class檔案,這路徑其實是package的包名。我們都知道如果使用javac *.java進行編譯會直接在原始檔的目錄下生成class檔案。那為什麼要使用javac -d . *java進行編譯呢。等後面執行服務端的類再說。
②啟動登錄檔服務:
③執行服務端類:
我這裡就丟擲了異常,java.rmi.UnmarshalException.百度了下,好像是說我codebase路徑錯了。codebase路徑設定為位元組碼的路徑也報錯,懇請哪位大佬給我解決一下。
下面說說codebase:
codebase就是遠端裝載類的路徑,當傳送者序列化物件時,會在序列化流中附加上codebase的資訊,這個資訊告訴接收方到什麼地方尋找該物件的執行程式碼。由於本地沒有類的物件,RMI提供了一些機制答應接收物件的一方去取回該物件的程式碼,而到什麼地方去取,這就需要傳送方配置codebase。客戶端可以把classpath看成是“原生代碼庫址”,而codebase則是“遠端程式碼庫址”。Java RMI使用一個叫做存根的特殊類,它能夠被下載到客戶端和遠端物件交流,進行方法呼叫。codebase屬性代表了一個或多個URL地址,從該地址那些存根類能夠被下載到客戶端。
由此,可以看出Stub是在伺服器產生,被下載到客戶端進行呼叫遠端物件。
RMI中codebase的使用方式:
①遠端物件伺服器通過設定java.rmi.server.codebase屬性指定遠端物件的codebase。RMI server向RMI registry註冊一個遠端物件,繫結到一個name上,在server JVM上設定的codebase被通知給了RMI registry。
②RMI client請求一個特定名字(登錄檔中每個遠端物件繫結到一個name上)的遠端物件的引用,用來呼叫遠端方法。
③RMI registry登錄檔向請求的類返回一個引用(存根例項)。客戶端會優先於codebase在本地的classpath中尋找存根類,如果找到了,那麼它就會在本地呼叫該類。然而,如果在本地的classpath找不到該stub 的存根類,客戶端會從遠端物件codebase中獲取類定義。
④客戶端從程式碼庫址(codebase)請求類,客戶端根據codebase中的URL獲取載入該存根類到客戶端。這時,客戶端就有了呼叫遠端方法的所有資訊。
給出具體實現的過程圖:
上面還遺留一個問題就是為什麼要使用javac -d . *.java編譯,而不使用javac *.java
下面來看看如果使用javac *.java進行編譯會出現什麼問題。
下面執行看看:
發現報錯了java.lang.NoclassDefFoundError
這個錯誤原因就是在編譯時能夠找到該類,而在執行時則無法找到該類。
java.lang.ClassNotFoundException錯誤是指在編譯時就無法找到該類。
發生這個錯誤的原因就是:
有包的存在,必須使用javac -d .進行編譯,不然在執行時就無法找到位元組碼檔案。
還有一個問題就是前面提到的伺服器的Skeleton類一直沒有提到,因為jdk1.2以後的RMI可以通過反射API可以直接將請求傳送給真實類,所以不需要skeleton類了。
剛學習RMI,有不對的地方懇請給予指出,感謝。