天天看點

代理模式——遠端代理(一)

代理模式定義

為另一個對象提供一個替身或占位符以控制對這個對象的通路。使用代理模式建立代表對象,讓代表對象控制對某對象的通路,被代理的對象可是遠端的對象、建立開銷大的對象或需要安全控制的對象。

代理分三種:

  1. 遠端代理,幫助我們控制通路遠端對象:

    遠端代理可以作為另一個JVM上對象的本地代表。調用代理的方法,會被代理利用網絡轉發到遠端執行,并且結果會通過網絡傳回給代理,再由代理将結果轉給客戶。

  2. 虛拟代理,幫助我們控制通路建立開銷大的資源

    虛拟代理作為建立開銷大的對象的代表,經常會直到我們真正需要一個對象的時候才建立它。當對象在建立前和建立中時,由虛拟代理地來扮演對象的替身。對象建立後,代理就會将請求直接委托給對象。

  3. 保護代理,基于權限控制對資源的通路。

代理模式有很多的變體,如:緩存代理、同步代理、防火牆代理、寫入時複制代理等等。代理在結構上類似于裝飾者,但是目的不同。裝飾者是為對象加上行為,而代理則是為了控制通路。Java内置的代理支援,可以根據需要建立動态代理,并将所有調用配置設定到所選的處理器上,關于動态代理,可以參考​​《代理模式——保護代理(三)》​​

本篇講一講遠端代理:

代理做的事情就是控制和管理通路。

所謂的代理,就是代表某個真實的對象。其實幕後是代理利用網絡和一個遠端的真正對象溝通。這個代理假裝它是真正的對象,但是其實一切的動作是代理對象利用網絡和真正的對象溝通。

代理之是以需要控制通路,是因為我們的客戶不知道如何和遠端對象溝通。從某個方面來看,遠端代理控制通路,可以幫我們處理網絡上的細節。

遠端代理

所謂“遠端代理”就好比“遠端對象的本地代表”。

所謂“遠端對象”就是一種對象,活在不同的java虛拟機(JVM)堆中。更一般的說法是在不同的位址空間運作的遠端對象。

所謂本地代表,就是一種可以由本地方法調用的對象,其行為會轉發到遠端對象中。

是以當客戶對象調用代理對象,就像是在做遠端方法調用,其實隻是調用本地堆中的”代理“對象上的方法,再由代理處理所有網絡通信的低層細節。Java已經有内置遠端調用的功能了,可以幫助我們實作遠端代理。

變量隻能引用和目前代碼語句在同一堆空間中的對象。

JAVA RMI

RMI提供了客戶輔助對象(Stub)和服務輔助對象(Skeleton),為客戶輔助對象建立和服務對象相同的方法。RMI的好處在于你不必親自寫任何網絡或I/O代碼。客戶程式調用遠端方法(即真正的服務所在)就和在運作在客戶自己的本地JVM上對對象進行正常方法調用一樣。

RMI提供了所有運作時的基礎設施,讓這一切正常工作。包括了查找服務,這個查找服務用來尋找和通路遠端對象。

雖然調用遠端方法就如同調用本地方法一樣,但是客戶輔助對象會通過網絡發送方法調用,是以網絡和I/O的确是存在的。

RMI将客戶輔助對象稱為stub(樁),服務輔助對象稱為skeleton(骨架)。

将一個普通的對象變成可以被遠端客戶調用的遠端對象

第一步:制作遠端接口

遠端接口定義出可以讓客戶遠端調用的方法。客戶将用它作為服務的類類型。Stub 和實際的服務都實作此接口。

(1)擴充java.rmi.Remote。Remote是一個“記号”接口,是以Remote不具有方法。對于RMI來說,Remote接口具有特别的意義,是以我們必須遵守規則。

import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote {      

(2)聲明所有的方法都會抛出RemoteException

客戶使用遠端接口調用服務。換句話說,客戶會調用實作遠端接口的Stub上的方法,而Stub底層用到了網絡和I/O,是以各種壞事情都可能會發生。客戶必須認識到此風險,通過處理或聲明遠端異常來解決。如果接口的方法聲明了異常,任何在接口類型的引用上調用方法的代碼也必須處理或聲明異常。

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyRemote extends Remote {
    public String sayHello() throws RemoteException;

}      

(3)确定變量和傳回值是屬于原語(primitive)類型或者可序列化(Serializable)類型

遠端方法的變量和傳回值,必須屬于原語類型或Serializable類型。遠端方法的變量必須被打包并通過網絡運送,這要靠序列化來完成。如果你使用原語類型、字元串和許多API中内定的類型(包括數組和集合),都不會有問題。如果你傳送自己定義的類,就必須保證你的類實作了Serializable接口。

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyRemote extends Remote {
    public String sayHello() throws RemoteException;

}      

第二步:制作遠端實作

這是做實際工作的類,為遠端接口中定義的遠端方法提供了真正的實作。這就是客戶真正想要調用的方法的對象。

(1)實作遠端接口,也就是客戶将要調用的方法的接口。

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    
    @Override
    public String sayHello() throws RemoteException {
        return "Server says, 'Hello'";
    }
    ...
}      

(2)擴充UnicastRemoteObject

為了要成為遠端服務對象,你的對象需要某些“遠端的”功能。最簡單的方式是擴充java.rmi.server.UnicastRemoteObject,讓超類幫你做這些工作。

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {      

(3)設計一個不帶變量的構造器,并聲明RemoteException

超類UnicastRemoteObject會帶來一個小問題:它的構造器會抛出RemoteException。唯一的解決方法就是為你的遠端實作聲明一個構造器,然後抛出RemoteException。當類被執行個體化的時候,超類的構造器總是會被調用。如果超類的構造器抛出異常,那麼你隻能聲明子類的構造器也抛出異常。

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {

    public MyRemoteImpl() throws RemoteException{} //不需要在方法體中放進任何代碼,隻需要有辦法聲明超類構造器會抛出異常
    ...
    }      

(4)用RMI Registry注冊此服務

目前,我們已有了MyRemoteImpl這個遠端服務了,必須讓它可以被遠端客戶調用。我們要做的就是将此服務執行個體化,然後放進RMI registry中(要先确定RMI Registry正在運作,否則注冊會失敗)。當注冊這個對象時,RMI系統其實注冊的是stub,因為這是客戶真正需要的。注冊服務使用了java.rmi.Naming類的靜态rebind()方法。

try{
    MyRemote myRemote = new MyRemoteImpl();  //先産生遠端對象
    Naming.rebind("RemoteHello",myRemote);//再使用Naming.rebind()綁定到rmiregistry,客戶将使用我們所注冊的名稱RemoteHello在RMI registry中尋找他。
    }catch (Exception e){
       e.printStackTrace();
    }      

第三步:利用rmic産生的stub和 skeleton

這就是客戶和服務的輔助類。你不需要自己建立這些類,也不用理會它們的代碼是什麼。當你運作rmic工具時,這都會自動建立。rmic工具是JDK内的一個工具,用來為一個服務類産生stub和skeleton。命名習慣是在遠端實作的名字後面加上_Stub或_Skel。rmic有一些選項可以調整,包括要不要産生skeleton、檢視源代碼,甚至使用IIOP作為協定。

使用rmic 的方式:将類産生在目前目錄下,請注意,rmic必須看到你的實作類,是以你可能會從你的遠端實作所在的目錄執行rmic。

cd到放置.class的~/Desktop/ProxyPattern/out/production/ProxyPattern目錄,注意不用進入impl目錄,然後運作rmic impl.MyRemoteImpl,用package的完整路徑,後面是類名,注意不帶字尾.class。

~/Desktop/ProxyPattern/out/production/ProxyPattern$ rmic impl.MyRemoteImpl
Warning: generation and use of skeletons and static stubs for JRMP
is deprecated. Skeletons are unnecessary, and static stubs have
been superseded by dynamically generated stubs. Users are
encouraged to migrate away from using rmic to generate skeletons and static
stubs. See the documentation for java.rmi.server.UnicastRemoteObject.
~/Desktop/ProxyPattern/out/production/ProxyPattern$ ls impl
MyRemoteImpl.class  MyRemoteImpl_Stub.class      

MyRemoteImpl_Stub.class成功生成出來了。

第四步:啟動RMI registry (rmiregistry)

rmiregistry就像是電話簿,客戶可以從中查到代理的位置,也就是客戶的stub helper對象。

開啟一個終端,啟動rmiregistry。先確定啟動目錄可以通路到我們的類。最簡單的方法就是從"classes"目錄啟動。

~/Desktop/ProxyPattern/out/production/ProxyPattern$ rmiregistry
|      

第五步:開始遠端服務

必須讓服務對象開始運作。我們的服務實作會執行個體化一個服務的執行個體,并将這個服務注冊到RMI registry,注冊之後,這個服務就可以供客戶調用了。

我們從實作類(即MyRemoteImpl)中的main()方法啟動的,main方法會先執行個體化一個服務對象,然後到RMI registry中注冊。

~/Desktop/ProxyPattern/out/production/ProxyPattern$ java impl/MyRemoteImpl
|      

​​服務端的代碼,歡迎下載下傳學習。​​

客戶如何取得stub對象?

我們新開一個項目叫ProxyPatternClient作為用戶端。​​用戶端的代碼在此,歡迎下載下傳學習。​​ 以下看看客戶如何取得stub對象。

客戶必須取得stub對象(我們的代理)以調用其中的方法。是以我們就需要RMI Registry的幫忙。客戶從Registry中尋找(lookup)代理,就像根據名字在電話簿裡尋找一樣。

(1)客戶到RMI registry中查找

Naming.lookup("rmi://127.0.0.1/RemoteHello");  //這裡需要IP位址或主機名,RemoteHello是在服務端綁定/重綁定時用的名稱。      

客戶在做lookup時,必須要有stub類,就是之前用rmic産生的,否則stub在用戶端就無法被反序列化。用戶端也需要調用遠端對象方法所傳回的序列化對象的類。

(2)RMI registry傳回Stub對象

作為lookup方法的傳回值,RMI會自動對stub反序列化。在用戶端必須有stub類(由rmic為你産生)。

把前面~/Desktop/ProxyPattern/out/production/ProxyPattern/impl/MyRemoteImpl_Stub.class類拷由到用戶端目錄~/Desktop/ProxyPatternClient/out/production/ProxyPatternClient/impl目錄下(impl目錄要自己建)。

除此之外還要把服務端的MyRemote.java也拷一份到用戶端。要注意完整的類路徑要與服務端的一樣。即MyRemote.java在服務端的完整類路徑是inter.MyRemote,那麼在用戶端也必須是這樣。

(3)客戶調用stub的方法,就像stub就是真正的服務對象一樣。

完整的用戶端代碼:

public class MyRemoteClient {

    public static void main(String[] args) {
       new MyRemoteClient().go();

    }

    public void go(){
        try{
            MyRemote myRemote = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");//這裡需要IP位址或主機名,RemoteHello是在服務端綁定/重綁定時用的名稱。
            String s = myRemote.sayHello();//看起來和普通方法調用沒有什麼差别。
            System.out.println("@@@:"+s);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}      

常犯錯誤:

  1. 忘記在啟動遠端服務之前先啟動rmiregistry(要用Naming.rebind()注冊服務,rmiregistry必須是運作的。)
  2. 忘記了讓變量和傳回值的類型成為可序列化的類型
  3. 忘記了給用戶端提供stub類。

關鍵字transient:告訴JVM不要序列化這個字段。

額外補充: