代理模式(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。