代理模式(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),用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上;一个虚拟机中的对象调用另一个虚拟机中的对象的方法,而允许被远程调用的对象需要通过一些标志加以标识。
详情的可以看看这篇文章 。
制作远程服务 将一个普通的对象变成可以被远程客户调用的远程对象,简要步骤:
制作远程接口:远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub 和实际的服务都实现此接口。 制作远程实现:做实际工作的类,为远程接口中定义的远程方法提供了真正的实现,是客户真正想要调用方法的对象(比如我们的GumballMachine)。 利用 rmic 产生的 stub 和 skeleton:客户和服务的辅助类。不需要创建,因为运行 rmic 工具时就会自动处理。 启动 RMI registry:rmireistry 就像电话簿,客户可以从中查到代理的位置(就是客户的 stub helper 对象)。 开始远程服务:让服务对象开始运行。服务实现类会去实例化一个服务的实例,并将这个服务注册到 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 命令:
执行 rmireistry
执行命令启动 rmireistry
启动服务
在这个简单的例子中,我们从实现类中的 main 方法启动:
客户取得 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 { 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 { 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); Naming.rebind("//" + location + "/gumballmachine" , gumballMachine); } catch (Exception e) { e.printStackTrace(); } } }
在命令行注册:
启动糖果机服务:
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。