《Head First 設計模式》筆記10
代理模式(Proxy)
為另一個物件提供一個替身或佔位符以控制對這個物件的訪問。
栗子
還記得上一個筆記中的糖果機吧,現在產品經理想要一份寫著糖果機位置、庫存和當前的狀態報告。
是不是挺簡單的?趕緊寫程式碼。
糖果機加上位置資訊:
class GumballMachine {
// ...
private String location;
public GumballMachine(String location, int count) {
this.location = location;
// ...
}
public String getLocation() {
return location;
}
// ...
}
一個監控糖果機的監視器:
class GumballMonitor {
private GumballMachine gumballMachine;
public GumballMonitor(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void report() {
System.out.println("糖果機:" + gumballMachine.getLocation());
System.out.println("當前庫存:" + gumballMachine.getCount());
System.out.println("當前狀態:" + gumballMachine.getState());
}
}
測試
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine("廣州", 600);
GumballMonitor monitor = new GumballMonitor(gumballMachine);
monitor.report();
}
完美通過測試,收拾東西回家洗澡。
滿足了假的需求
產品經理的需求並沒有完全表達清楚,我們就開始寫了,最後白費了時間和精力,而且沒完成任務。(記得問清楚需求再去實現)
需求是要一個能遠端的監控器,而按我們上面的監視器和糖果機程式碼,它們就是在同一個 JVM 上執行的,就相當於一個本地監控器,什麼意思呢?相當於在教室裡裝了一個攝像頭,而且是實時監控,沒聯網的,那麼只能在教室看,對於坐在辦公室的老師來說這個攝像頭沒起作用。
前置知識
RMI:遠端方法呼叫(Remote Method Invocation),用於不同虛擬機器之間的通訊,這些虛擬機器可以在不同的主機上、也可以在同一個主機上;一個虛擬機器中的物件呼叫另一個虛擬機器中的物件的方法,而允許被遠端呼叫的物件需要通過一些標誌加以標識。
詳情的可以看看這篇文章。
製作遠端服務
將一個普通的物件變成可以被遠端客戶呼叫的遠端物件,簡要步驟:
1. 製作遠端介面:遠端介面定義出可以讓客戶遠端呼叫的方法。客戶將用它作為服務的類型別。Stub 和實際的服務都實現此介面。
2. 製作遠端實現:做實際工作的類,為遠端介面中定義的遠端方法提供了真正的實現,是客戶真正想要呼叫方法的物件(比如我們的GumballMachine)。
3. 利用 rmic 產生的 stub 和 skeleton:客戶和服務的輔助類。不需要建立,因為執行 rmic 工具時就會自動處理。
4. 啟動 RMI registry:rmireistry 就像電話簿,客戶可以從中查到代理的位置(就是客戶的 stub helper 物件)。
5. 開始遠端服務:讓服務物件開始執行。服務實現類會去例項化一個服務的例項,並將這個服務註冊到 RMI registry。註冊之後,這個服務就可以供客戶呼叫了。
製作遠端介面
擴充套件 java.rmi.Remote
Remote 不具有方法,只是作為一個“記號”介面。對 RMI 來說,Remote 介面具有特別的意義。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote {
public String sayHello() throws RemoteException;
}
注意:
所有的方法都要宣告丟擲 RemoteException,因為客戶會呼叫實現遠端介面的 Stub 上的方法,而 Stub 底層用到了網路和 I/O,所以各種意外都可能發生。
方法上的變數和返回值都必須屬於原語(primitive)或可序列化(Serializable)型別(遠端方法的變數必須被打包並通過網路運送,這需要序列化)。原語型別、字串和許多 API 中內定的型別都不會有問題,但如果是自己定義的類,必須保證它實現了 Serializable。
製作遠端實現
你的服務必須實現遠端介面,它是客戶將要呼叫的方法的介面。
import java.rmi.server.UnicastRemoteObject;
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
public MyRemoteImpl() throws RemoteException { }
public String sayHello() {
return "伺服器:你好";
}
// 為了方便後面的啟動服務
public static void main(String[] args) {
try {
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:
擴充套件 UnicastRemoteObject:為了成為遠端服務物件,你的物件需要某些“遠端的”功能。最簡單的方式就是擴充套件 UnicastRemoteObject,讓超類幫你做這些工作。
不帶變數的構造器要宣告 RemoteException,這樣當類被例項化的時候,超類的構造器總是會被呼叫。如果超類的構造器丟擲異常,那麼你只能什麼子類的構造器也丟擲異常。
產生 Stub 和 Skeleton
在遠端實現類上執行 rmic 命令:
rmic MyRemoteImpl
執行 rmireistry
執行命令啟動 rmireistry
rmireistry
啟動服務
在這個簡單的例子中,我們從實現類中的 main 方法啟動:
java MyRemoteImpl
客戶取得 Stub 物件
客戶總是使用遠端介面做為服務型別,事實上客戶不需要知道遠端服務的真正類名什麼。
import java.rmi.*;
class MyRemoteClient {
public static void main(String[] args) {
try {
// 通過查詢 RemoteHello 註冊名,找到遠端服務
MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
System.out.println(service.sayHello());
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:
要先啟動 rmireistry 註冊,再啟動遠端服務。
遠端方法的變數和返回值型別必須為可序列化的型別。
必須給客戶提供 Stub 類。
客戶機內有:Client.class、MyRemoteImpl_Stub.class、MyRemote.class
遠端服務機內有:MyRemoteImpl.class、MyRemoteImpl_Stub.class、MyRemoteImpl_Skel.class、MyRemote.class
繼續完成需求
把糖果機變成一個遠端服務
同樣,按照上面的步驟進行。
1.建立遠端介面:
import java.rmi.*;
public interface GumballMachineRemote extends Remote {
public int getCount() throws RemoteException;
public String getLocation() throws RemoteException;
public State getState() throws RemoteException;
}
2.State 這個返回型別不是序列化的,要修改:
import java.io.Serializable;
public interface State extends Serializable {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
3.每個實體狀態都維護著一個糖果機的引用,而我們不希望整個糖果機都被序列化並隨著 State 物件一起傳送:
加上 transient 關鍵字,告訴 JVM 不要序列化這個欄位。
public class SoldState implements State {
transient GumballMachine gumballMachine;
// ...
}
4.糖果機要實現遠端介面:
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {
// 構造方法會拋 RemoteException 異常,因為超類
public GumballMachine(String location, int count) throws RemoteException {
// ...
}
// ...
}
5.修改監控器:
import java.rmi.*;
public class GumballMonitor {
// 改成遠端介面上的糖果機
private GumballMachineRemote gumballMachine;
public GumballMonitor(GumballMachineRemote gumballMachine) {
this.gumballMachine = gumballMachine;
}
public void report() {
try {
System.out.println("糖果機:" + gumballMachine.getLocation());
System.out.println("當前庫存" + gumballMachine.getCount());
System.out.println("當前狀態" + gumballMachine.getState());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
在 RMI registry 中註冊
糖果機服務已經完成了,現在要把它裝上,好開始接收請求。
首先要確保將它註冊到 RMI registry 中,好讓客戶可以找到它:
public class GumballMachineTestDrive {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("GumballMachine <location> <count>");
System.exit(1);
}
try {
String location = args[0];
int count = Integer.parseInt(args[1]);
GumballMachineRemote gumballMachine = new GumballMachine(location, count);
// 用糖果機的位置釋出糖果機的 stub
Naming.rebind("//" + location + "/gumballmachine", gumballMachine);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在命令列註冊:
rmiregistry
啟動糖果機服務:
java GumballMachineTestDrive GuangZhou 100
監控器測試程式
現在一切就緒,可以嘗試監控更多糖果機:
public class GumballMonitorTestDrive {
public static void main(String[] args) {
List<String> locations = Arrays.asList( "rmi://GuangZhou/gumballmachine",
"rmi://ShangHai/gumballmachine",
"rmi://BeiJing/gumballmachine");
List<GumballMonitor> monitors = new ArrayList<>();
locations.forEach(location -> {
try {
// 為每個遠端機器建立一個代理
GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location);
monitors.add(new GumballMonitor(machine));
} catch (Exception e) {
e.printStackTrace();
}
});
monitors.forEach(monitor -> monitor.report());
}
}
虛擬代理(Virtual Proxy)
遠端代理
遠端代理可以作為另一個 JVM 上物件的本地代表。呼叫代理方法,會被代理利用網路轉發到遠端執行,並且結果會通過網路返回給代理,再由代理將結果轉給客戶。
本地客戶 Client 發出請求 -> 本地的遠端代理 proxy 轉發該請求 -> 遠端物件 RealSubject
虛擬代理
虛擬代理作為建立開銷大的物件的代表。虛擬代理經常直到我們真正需要一個物件的時候才建立它。當物件在建立前和建立中時,由虛擬代理來扮演物件的替身。物件建立後,代理就會將請求委託給物件。
本地客戶 Client 發出請求 -> 本地的虛擬代理 proxy 處理請求 -> 如果 RealSubject(開銷大的物件)已經建立,proxy 就把請求委託給 RealSubject;否則 proxy 建立該 RealSubject。