1. 程式人生 > >Java設計模式—代理模式之遠端代理

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,有不對的地方懇請給予指出,感謝。