走进代理模式
在上一篇的状态模式中,我们实现了一个糖果机GumballMachine, 客户通过投币可以买到糖果.
现在设想这样一种情况: 糖果机是属于某个糖果公司的,这个公司的CEO坐在办公室里喝茶,看报纸, 但他还想要随时了解糖果机的状态,
比如还剩下多少个糖果,卖了多少钱等等. CEO要怎样才能远程"遥控"糖果机获取信息呢?
这就得用到代理模式(Proxy Pattern)了.
可能我们会想到通过socket网络连接, 自己定义好协议去控制糖果机.这个其实就是代理模式的底层实现.
所谓代理(Proxy)就是代表某个真实的对象, 这个"代理"可以看作是真正的对象, 比如CEO在办公室中拿到了糖果机的"代理", 然后CEO
对"代理"进行的任何操作,到会"传送"到远程真实的糖果机上, 然后糖果机就会作出反应, 看起来好像"代理"就是真正的糖果机一样.
这一切的动作是利用网络和真正对象沟通的.
远程代理的角色
远程代理就好比"远程对象的本地代表".
何谓"远程对象"? 这是一种对象,活在不同的Java虚拟机(JVM)堆中(更一般的说法,在不同的地址空间运行的远程对象).
何谓"本地代表"? 这是一种可以由本地方法调用的对象,其行为会转发到远程对象中.
你的客户对象所做的就像是在做远程方法调用, 但其实只是调用本地堆中的"代理对象"上的方法,
再由代理处理所有网络通信的底层细节
JAVA远程方法调用(RMI)
如果要自己手动去实现网络通信的底层, 那么显然会大大加大编程复杂度, 幸运的是, Java中已经有内置工具给我们使用了.
这就是: Java远程方法调用(RMI: Remote Mehod Invocation)
用Java RMI创建远程服务的基本步骤:
1. 创建远程接口
- // 这个接口扩展自Remote,支持远程调用
- public interface MyRemote extends Remote{
- // 每次远程调用都是“有风险的”,因为会设计网络和I/O
- // 所以这个接口的方法都要声明异常
- // 还要确定方法的返回值是primitive类型或者可序列化(Serializable)类型
- // 远程方法的变量会被打包并通过网络运送,这要靠序列化来完成
- // 自定义的类必须序列化
- public String sayHello() throws RemoteException;
- }
- public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
- // 声明默认构造器
- protected MyRemoteImpl() throws RemoteException {
- super();
- }
- // 实现接口
- public String sayHello() throws RemoteException {
- return "Hello, world!";
- }
- public static void main(String[] args) {
- try {
- MyRemote service = new MyRemoteImpl();
- // 用Naming.rebind()绑定到remiregistry
- Naming.rebind("rmi://127.0.0.1/RemoteHello", service);
- } catch (Exception e){
- e.printStackTrace();
- }
- }
- }
3. 命令行编译上面两个类(PS. 用的是Ubuntu系统,不是用IDE写的,所以两个类也没有package,放在同一个目录下。实际上,如果直接用IDE编译会遇到一个问题,后面会提)
这样就把Remote.java 和RemoteImpl.java两个类都编译了,编译完后,产生两个编译文件:
MyRemote.class和MyRemoteImpl.java
4. 用rmic产生stub和skeleton
rmic是JDK内的一个工具,用来为一个服务类产生stub和skeleton。命名习惯是在远程实现的名字后面加上_Stub或_Skel。rmic有一些选项可以调整,包括不要产生skeleton,查看源代码,甚至使用IIOP作为协议。这个例子使用的是rmic常见的方式,将类产生在当前目录下(就是cd到的目录)。注意,rmic必须看到你的实现类,所以你可能会从你的远程实现所在的目录下执行rmic
执行完之后,发现目录下多了一个名为MyRemote_Stub.class的文件:
5. 执行rmiregistry
开启一个终端,启动rmiregistry
注意这个终端不要关闭!
6. 启动服务端服务
开启另一个终端,启动服务
这个终端也不要关闭。
---------------------------分割线------------------------------
上面是服务端的代码,下面来实现客户端的代码。
客户必须取得stub对象(我们的代理)以调用其中的方法。所以我们就需要RMI Registry的帮忙。客户从Registry中寻找(lookup)代理,就好像在电话簿里寻找一样,说:“我要找这个名字的stub”.
- import java.rmi.*;
- public class MyRemoteClient {
- public static void main (String[] args) {
- new MyRemoteClient().go();
- }
- public void go() {
- try{
- // 返回值是Object,所以要转换类型
- // 需要ip地址或主机名,以及服务器绑定/重绑定时用的名称
- MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/RemoteHello");
- // 看起来和一般的方法调用没有区别!(除了必须注意RemoteException之外)
- String s = service.sayHello();
- System.out.println(s);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
编译,并且运行,发现输出“Hello world!”了!
OK !这就是RMI的基本过程了,当然实际应用中肯定还会有些变化的。
对于RMI, 程序员常犯的三个错误:
1. 忘了在启动远程服务之前先启动rmiregistry(要用Naming.rebind()注册服务, rmiregistry必须是运行的)
2. 忘了让变量和返回值的类型成为可序列化的类型(这种错误无法在编译期发现, 只会在运行时发现)
3. 忘了给客户提供stub类.
在敲上面的例子时, 我用IDE(Eclipse)来写过,但是步骤3编译时,遇到的问题:
java.rmi.ConnectException: Connection refused to host: 127.0.0.1
我试了其它电脑,还换了系统实现上述代码, 编译时都遇到了这个问题。
有没有人知道告诉我一下?
定义代理模式
代理模式为另一个对象提供一个替身或占位符以控制这个对象的访问
- Subject, 为RealSubject和Proxy提供了接口, 通过实现同一个接口, Proxy在RealSubject出现的地方取代它
- RealSubject是真正做事的对象,它是被proxy代理和控制访问的对象.
- Proxy持有RealSubject的引用.在某些例子中, Proxy还会负责RealSubject对象的创建与销毁.客户和RealSubject的交互都必须通过Proxy.因为Proxy和RealSbuject实现相同的接口(Subject),所以任何用到RealSubject的地方,都可以用Proxy取代.Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的控制.这些情况包括RealSubject是远程的对象, RealSubject创建开销大, 或RealSubject需要被保护.
代理模式的其他玩法
远程代理是一般代理模式的一种实现, 但是代理了模式的变体相当多
比如: 虚拟代理, 缓存代理,同步代理, 防火墙代理和写入时复制代理.
限于篇幅, 下次再继续学习