移动测试开发 JDK RMI 探索与使用
探索 JDK RMI
RMI(Remote Method Invoke)是 JDK 自带的一个远程调用框架,可以基于 RMI 构建自己的分布式应用。现在很多分布式服务框架都会选择支持 RMI 协议,比如 dubbo,spring rmi,Hessian 等 RPC 框架。为了对 RMI 有更多的了解,下面我们一起来读代码。
一、代码示例
为了更直观的了解 RMI 的使用,先来看一段代码。
新建三个 maven 工程,分别是 rmiclient,rmidependency,rmiserver,其中 rmiclient 是客服端,rmiserver 是服务端,rmidependency 是一个只包含接口的服务工程。rmiclient 和 rmiserver 引入 rmidependency 依赖。rmidependency 中 HelloWorldService 接口如下:
public interface HelloWorldService extends Remote {
public String sayHello(String from, String to) throws RemoteException;
}
rmiserver 中 HelloWordServiceImpl 实现了 HelloWorldService 接口的类:
public class HelloWorldServiceImpl extends UnicastRemoteObject implements HelloWorldService {
public HelloWorldServiceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String from, String to) throws RemoteException {
return from + " say hello to " + to;
}
}
在 rmiServer 中发布服务
public class RMIServer
{
public static void main( String[] args )
{
try {
//1.服务创建及发布:HelloWorldService 需要继承自 UnicastRemoteObject,当初始化时会自动将 HelloWorldServiceImpl任务一个服务发布。
HelloWorldService helloWorldService = new HelloWorldServiceImpl();
// 2. 创建注册中心:创建本机 1099 端口上的 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);
// 3. 服务注册:将服务绑定到注册表中
registry.bind("rmi://localhost:1099/helloWorldService", helloWorldService);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
在 rmiclent 中调用远程服务
public class RMIClient
{
public static void main( String[] args )
{
try {
HelloWorldService helloWorldService = (HelloWorldService) Naming.lookup("rmi://localhost:1099/helloWorldService");
if(helloWorldService != null) {
String result = helloWorldService.sayHello("java", "菜鸟");
System.out.println(result);
} else {
System.out.println("rmi 查找服务错误");
}
//Naming.list("rmi://localhost:1099");
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
以上,可以看出 RMI 开发中有三个重要角色,分别是客户端,服务端,注册中心,其中
(1)注册中心:负责服务注册
(2)服务端:是提供服务的地方,是真正实现约定接口的地方
(3)客户端:是服务的调用方,是需要使用服务的一方。
通过上面的代码发现,无论是 HelloWorldServiceImpl 还是 Registry 都是 RemoteObjet 的子类。具体类图如下
RemoteObject 最重要的属性是 RemoteRef ref, RemoteRef 的实现类 UnicastRef,UnicastRef 包含属性 LiveRef ref。LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。
二、启动服务端
服务端通过 LocateRegistry.createRegistry(1099) 来创建注册中心,如下,在创建注册中心的时候,会初始化一个 HashTable 的 bindings 属性,在使用 bind() 方法注册服务的时候,会保存到 bindings 结构中。之后如果注册中心的端口是 1099 并且系统开启了安全管理器,那么可以在限定的权限集内(listen 和 accept)绕过系统的安全校验,反之需要进行安全校验。之后通过 setup() 中的 exportObject 方法来暴露服务。Ps:继承 UnicastRemoteObject 的类,最终也会调用调用 exportObject 方法。
public RegistryImpl(final int var1) throws RemoteException {
this.bindings = new Hashtable(101);
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> {
return RegistryImpl.registryFilter(var0);
}));
return null;
}
}, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
} catch (PrivilegedActionException var3) {
throw (RemoteException)var3.getException();
}
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
}
}
private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}
setup() 里面调用的是 UnicastServerRef 的 exportObject() 方法,进入该方法,这里面为 RestryImpl 创建了一个代理,代理的作用是创建了服务于客户端的 RegistryImpl 的 Stub 对象,最后用 skeleton、stub、UnicastServerRef 对象、id 和一个 boolean 值构造了一个 Target 对象,也就是这个 Target 对象基本上包含了全部的信息。
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();
Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}
if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}
上面提到了 Stub(存根)和 Skeleton(骨架)对象,分别是客户端的代理和服务端的代理,Stub 和 Skeleton 通过 socket 进行通信。上面的代码封装了 Target 方法之后,调用了 LiveRef 的 exportObject() 方法,追到了 TCPTransport 的 exportObject() 方法,如下,这个方法里面做了两件事。一是 listen() 方法创建一个 ServerSocket。并启动一条线程等待客户端的请求,接着调用父类 Transport 的 exportObject() 将 Target 对象存放进 ObjectTable 中。
public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
this.listen();
++this.exportCount;
}
boolean var2 = false;
boolean var12 = false;
try {
var12 = true;
super.exportObject(var1);
var2 = true;
var12 = false;
} finally {
if (var12) {
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
}
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
三、客户端获取获取服务
通过客户端调用远程服务分为两个步骤:一是获取注册中心 registry;二是根据注册中心获取服务的代理类 service。通过示例代码,可以追溯到 LocateRegistry 的 getRegistry() 方法。
public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
this.listen();
++this.exportCount;
}
boolean var2 = false;
boolean var12 = false;
try {
var12 = true;
super.exportObject(var1);
var2 = true;
var12 = false;
} finally {
if (var12) {
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
}
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}
}
这个方法做的是通过传入的 host 和 port 构造 RemoteRef 对象,并创建了一个本地代理。代理对象其实是 RegistryImpl_Stub 对象。但注意此时这个代理其实还没有和服务端的 RegistryImpl 对象关联,毕竟是两个 JVM 上面的对象,这里我们也可以猜测,代理和远程的 Registry 对象之间是通过 socket 消息来完成的。
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}
this.ref.invoke(var2);
Remote var22;
try {
ObjectInput var4 = var2.getInputStream();
var22 = (Remote)var4.readObject();
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2);
}
return var22;
} catch (RuntimeException var18) {
throw var18;
} catch (RemoteException var19) {
throw var19;
} catch (NotBoundException var20) {
throw var20;
} catch (Exception var21) {
throw new UnexpectedException("undeclared checked exception", var21);
}
}
获取注册中心后,在客户端直接生成代理对象 RegistryImpl_Stub,RegistryImpl_Stub 通过 newCall() 方法和服务端建立连接,并向服务端传输远程接口名称,使用的注册中心方法集合以及集合的索引,来调用远程方法。实际调用 RemoteRef 的 invoke 方法进行网络通信,来获得远程对象实例。
四、总结
通过上面的分析,RMI 的调用流程可以简化成下面的结构。
但是里面还有很多细节值得仔细研究,例如 rmi 在调用的方法中存在引用型数据,是如何进行序列化和反序列化?bindings 数据结构在整个调用过程中到底处于什么样的角色?骨干和存根是如何封装通信?后续笔者也会持续分享。
另外 RMI 优点是避免重复造轮子,降低开发工作量,优化部署结构等,缺点是调用过程是两个 jvm 通信,增加了调用时间,而且如果网络不稳定会影响方法调用,降低了可用性,所以技术选型的时候需要按照业务需要平衡一下,或者可以选择 dubbo 这种支持 rmi 协议的 RPC 框架来进行管理。