代理模式——远程代理(一)
代理模式定义
为另一个对象提供一个替身或占位符以控制对这个对象的访问。使用代理模式创建代表对象,让代表对象控制对某对象的访问,被代理的对象可是远程的对象、创建开销大的对象或需要安全控制的对象。
代理分三种:
- 远程代理,帮助我们控制访问远程对象:
远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。 - 虚拟代理,帮助我们控制访问创建开销大的资源
虚拟代理作为创建开销大的对象的代表,经常会直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理地来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。 - 保护代理,基于权限控制对资源的访问。
代理模式有很多的变体,如:缓存代理、同步代理、防火墙代理、写入时复制代理等等。代理在结构上类似于装饰者,但是目的不同。装饰者是为对象加上行为,而代理则是为了控制访问。Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器上,关于动态代理,可以参考《代理模式——保护代理(三)》
本篇讲一讲远程代理:
代理做的事情就是控制和管理访问。
所谓的代理,就是代表某个真实的对象。其实幕后是代理利用网络和一个远程的真正对象沟通。这个代理假装它是真正的对象,但是其实一切的动作是代理对象利用网络和真正的对象沟通。
代理之所以需要控制访问,是因为我们的客户不知道如何和远程对象沟通。从某个方面来看,远程代理控制访问,可以帮我们处理网络上的细节。
远程代理
所谓“远程代理”就好比“远程对象的本地代表”。
所谓“远程对象”就是一种对象,活在不同的java虚拟机(JVM)堆中。更一般的说法是在不同的地址空间运行的远程对象。
所谓本地代表,就是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
所以当客户对象调用代理对象,就像是在做远程方法调用,其实只是调用本地堆中的”代理“对象上的方法,再由代理处理所有网络通信的低层细节。Java已经有内置远程调用的功能了,可以帮助我们实现远程代理。
变量只能引用和当前代码语句在同一堆空间中的对象。
JAVA RMI
RMI提供了客户辅助对象(Stub)和服务辅助对象(Skeleton),为客户辅助对象创建和服务对象相同的方法。RMI的好处在于你不必亲自写任何网络或I/O代码。客户程序调用远程方法(即真正的服务所在)就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。
RMI提供了所有运行时的基础设施,让这一切正常工作。包括了查找服务,这个查找服务用来寻找和访问远程对象。
虽然调用远程方法就如同调用本地方法一样,但是客户辅助对象会通过网络发送方法调用,所以网络和I/O的确是存在的。
RMI将客户辅助对象称为stub(桩),服务辅助对象称为skeleton(骨架)。
将一个普通的对象变成可以被远程客户调用的远程对象
第一步:制作远程接口
远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub 和实际的服务都实现此接口。
(1)扩展java.rmi.Remote。Remote是一个“记号”接口,所以Remote不具有方法。对于RMI来说,Remote接口具有特别的意义,所以我们必须遵守规则。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote {
- 1
- 2
- 3
- 4
(2)声明所有的方法都会抛出RemoteException
客户使用远程接口调用服务。换句话说,客户会调用实现远程接口的Stub上的方法,而Stub底层用到了网络和I/O,所以各种坏事情都可能会发生。客户必须认识到此风险,通过处理或声明远程异常来解决。如果接口的方法声明了异常,任何在接口类型的引用上调用方法的代码也必须处理或声明异常。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote { public String sayHello() throws RemoteException;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
(3)确定变量和返回值是属于原语(primitive)类型或者可序列化(Serializable)类型
远程方法的变量和返回值,必须属于原语类型或Serializable类型。远程方法的变量必须被打包并通过网络运送,这要靠序列化来完成。如果你使用原语类型、字符串和许多API中内定的类型(包括数组和集合),都不会有问题。如果你传送自己定义的类,就必须保证你的类实现了Serializable接口。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote { public String sayHello() throws RemoteException;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
第二步:制作远程实现
这是做实际工作的类,为远程接口中定义的远程方法提供了真正的实现。这就是客户真正想要调用的方法的对象。
(1)实现远程接口,也就是客户将要调用的方法的接口。
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote { @Override public String sayHello() throws RemoteException { return "Server says, 'Hello'"; } ...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
(2)扩展UnicastRemoteObject
为了要成为远程服务对象,你的对象需要某些“远程的”功能。最简单的方式是扩展java.rmi.server.UnicastRemoteObject,让超类帮你做这些工作。
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
- 1
- 2
(3)设计一个不带变量的构造器,并声明RemoteException
超类UnicastRemoteObject会带来一个小问题:它的构造器会抛出RemoteException。唯一的解决方法就是为你的远程实现声明一个构造器,然后抛出RemoteException。当类被实例化的时候,超类的构造器总是会被调用。如果超类的构造器抛出异常,那么你只能声明子类的构造器也抛出异常。
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote { public MyRemoteImpl() throws RemoteException{} //不需要在方法体中放进任何代码,只需要有办法声明超类构造器会抛出异常 ... }
- 1
- 2
- 3
- 4
- 5
(4)用RMI Registry注册此服务
目前,我们已有了MyRemoteImpl这个远程服务了,必须让它可以被远程客户调用。我们要做的就是将此服务实例化,然后放进RMI registry中(要先确定RMI Registry正在运行,否则注册会失败)。当注册这个对象时,RMI系统其实注册的是stub,因为这是客户真正需要的。注册服务使用了java.rmi.Naming类的静态rebind()方法。
try{ MyRemote myRemote = new MyRemoteImpl(); //先产生远程对象 Naming.rebind("RemoteHello",myRemote);//再使用Naming.rebind()绑定到rmiregistry,客户将使用我们所注册的名称RemoteHello在RMI registry中寻找他。 }catch (Exception e){ e.printStackTrace(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
第三步:利用rmic产生的stub和 skeleton
这就是客户和服务的辅助类。你不需要自己创建这些类,也不用理会它们的代码是什么。当你运行rmic工具时,这都会自动创建。rmic工具是JDK内的一个工具,用来为一个服务类产生stub和skeleton。命名习惯是在远程实现的名字后面加上_Stub或_Skel。rmic有一些选项可以调整,包括要不要产生skeleton、查看源代码,甚至使用IIOP作为协议。
使用rmic 的方式:将类产生在当前目录下,请注意,rmic必须看到你的实现类,所以你可能会从你的远程实现所在的目录执行rmic。
cd到放置.class的~/Desktop/ProxyPattern/out/production/ProxyPattern目录,注意不用进入impl目录,然后运行rmic impl.MyRemoteImpl,用package的完整路径,后面是类名,注意不带后缀.class。
~/Desktop/ProxyPattern/out/production/ProxyPattern$ rmic impl.MyRemoteImpl
Warning: generation and use of skeletons and static stubs for JRMP
is deprecated. Skeletons are unnecessary, and static stubs have
been superseded by dynamically generated stubs. Users are
encouraged to migrate away from using rmic to generate skeletons and static
stubs. See the documentation for java.rmi.server.UnicastRemoteObject.
~/Desktop/ProxyPattern/out/production/ProxyPattern$ ls impl
MyRemoteImpl.class MyRemoteImpl_Stub.class
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
MyRemoteImpl_Stub.class成功生成出来了。
第四步:启动RMI registry (rmiregistry)
rmiregistry就像是电话簿,客户可以从中查到代理的位置,也就是客户的stub helper对象。
开启一个终端,启动rmiregistry。先确保启动目录可以访问到我们的类。最简单的方法就是从"classes"目录启动。
~/Desktop/ProxyPattern/out/production/ProxyPattern$ rmiregistry
|
- 1
- 2
- 3
- 4
第五步:开始远程服务
必须让服务对象开始运行。我们的服务实现会实例化一个服务的实例,并将这个服务注册到RMI registry,注册之后,这个服务就可以供客户调用了。
我们从实现类(即MyRemoteImpl)中的main()方法启动的,main方法会先实例化一个服务对象,然后到RMI registry中注册。
~/Desktop/ProxyPattern/out/production/ProxyPattern$ java impl/MyRemoteImpl
|
- 1
- 2
客户如何取得stub对象?
我们新开一个项目叫ProxyPatternClient作为客户端。客户端的代码在此,欢迎下载学习。
以下看看客户如何取得stub对象。
客户必须取得stub对象(我们的代理)以调用其中的方法。所以我们就需要RMI Registry的帮忙。客户从Registry中寻找(lookup)代理,就像根据名字在电话簿里寻找一样。
(1)客户到RMI registry中查找
Naming.lookup("rmi://127.0.0.1/RemoteHello"); //这里需要IP地址或主机名,RemoteHello是在服务端绑定/重绑定时用的名称。
- 1
客户在做lookup时,必须要有stub类,就是之前用rmic产生的,否则stub在客户端就无法被反序列化。客户端也需要调用远程对象方法所返回的序列化对象的类。
(2)RMI registry返回Stub对象
作为lookup方法的返回值,RMI会自动对stub反序列化。在客户端必须有stub类(由rmic为你产生)。
把前面~/Desktop/ProxyPattern/out/production/ProxyPattern/impl/MyRemoteImpl_Stub.class类拷由到客户端目录~/Desktop/ProxyPatternClient/out/production/ProxyPatternClient/impl目录下(impl目录要自己建)。
除此之外还要把服务端的MyRemote.java也拷一份到客户端。要注意完整的类路径要与服务端的一样。即MyRemote.java在服务端的完整类路径是inter.MyRemote,那么在客户端也必须是这样。
(3)客户调用stub的方法,就像stub就是真正的服务对象一样。
完整的客户端代码:
public class MyRemoteClient { public static void main(String[] args) { new MyRemoteClient().go(); } public void go(){ try{ MyRemote myRemote = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");//这里需要IP地址或主机名,RemoteHello是在服务端绑定/重绑定时用的名称。 String s = myRemote.sayHello();//看起来和普通方法调用没有什么差别。 System.out.println("@@@:"+s); }catch (Exception e){ e.printStackTrace(); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
常犯错误:
- 忘记在启动远程服务之前先启动rmiregistry(要用Naming.rebind()注册服务,rmiregistry必须是运行的。)
- 忘记了让变量和返回值的类型成为可序列化的类型
- 忘记了给客户端提供stub类。
关键字transient:告诉JVM不要序列化这个字段。
额外补充:
动态类下载:利用动态类下载,在序列化对象(像stub)可以被“贴”上一个URL,告诉客户的RMI系统去寻找对象的类文件。在反序列化对象的过程,如果RMI没有在本地发现类,就会利用HTTP的GET从该URL取得类文件。
谢谢阅读。有兴趣的还可以继续阅读《代理模式——虚拟代理(二)》
文章来源: blog.csdn.net,作者:WongKyunban,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_40763897/article/details/88634286
- 点赞
- 收藏
- 关注作者
评论(0)