《Head First 设计模式》笔记10

代理模式(Proxy)

为另一个对象提供一个替身或占位符以控制对这个对象的访问。

栗子

还记得上一个笔记中的糖果机吧,现在产品经理想要一份写着糖果机位置、库存和当前的状态报告。

是不是挺简单的?赶紧写代码。

糖果机加上位置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GumballMachine {
// ...
private String location;

public GumballMachine(String location, int count) {
this.location = location;
// ...
}

public String getLocation() {
return location;
}

// ...
}

一个监控糖果机的监视器:

1
2
3
4
5
6
7
8
9
10
11
12
13
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());
}
}

测试

1
2
3
4
5
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 接口具有特别的意义。

1
2
3
4
5
6
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。

制作远程实现

你的服务必须实现远程接口,它是客户将要调用的方法的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 命令:

1
rmic MyRemoteImpl

执行 rmireistry

执行命令启动 rmireistry

1
rmireistry

启动服务

在这个简单的例子中,我们从实现类中的 main 方法启动:

1
java MyRemoteImpl

客户取得 Stub 对象

客户总是使用远程接口做为服务类型,事实上客户不需要知道远程服务的真正类名什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
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.创建远程接口:

1
2
3
4
5
6
7
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 这个返回类型不是序列化的,要修改:

1
2
3
4
5
6
7
8
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 不要序列化这个字段。

1
2
3
4
5
public class SoldState implements State {
transient GumballMachine gumballMachine;

// ...
}

4.糖果机要实现远程接口:

1
2
3
4
5
6
7
8
9
10
11
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.修改监控器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 中,好让客户可以找到它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}
}
}

在命令行注册:

1
rmiregistry

启动糖果机服务:

1
java GumballMachineTestDrive GuangZhou 100

监控器测试程序

现在一切就绪,可以尝试监控更多糖果机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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。

Author

Zoctan

Posted on

2018-04-06

Updated on

2023-03-14

Licensed under