RMI即Remote Method Invocation(远程方法调用)。RMI允许运行在一个JVM系统中的对象调用另一JVM虚拟机的方法。RMI使得多个程序之间可以通信,也因此使得我们可以创建分布式系统。

一 概述

既然是一个系统调用另一个系统就必然需要有两个及以上的系统。RMI应用通常包含两个部分:服务器和客户端。通常,服务器程序负责创建远程对象,并且使这些远程对象可以被客户端访问,最后就是等待客户端程序的调用。而客户端程序就是负责获取远程对象的引用,然后在远程对象的引用上调用远程方法。

下图是RMI工作的流程图:

  1. 服务器将远程对象注册到RMI注册表中
  2. 客户端从RMI注册表中获取远程对象
  3. 客户端使用远程对象调用远程方法

二 RMI例子

下面通过一个简单的例子来学习RMI API的是使用。

2.1 需求说明

假如有一台计算能力较强的服务器,它提供了一个计算引擎供客户端使用。客户端通过计算引擎向服务器提交任务,并等待服务器的返回结果。服务器的计算引擎从客户端获取任务,运行任务并返回任何结果。
因此,计算引擎就是远程对象。所以服务器需要注册计算引擎对象到RMI注册表。而客户端需要从RMI注册表中获取计算引擎对象并调用其上的远程方法。

JDK提供了RMI相关的接口来帮助我们编写RMI代码。

Note:仅仅写完下面两部分代码并不能直接启动程序,还需要进行SecurityManager等相关的配置。具体的配置下面2.4章节会提到.也可以参考我的 github上的例子,直接按照其中的文档启动RMI服务。

2.2 服务器代码

针对上面的需求服务器端的代码主要分为两部分,第一部分是定义Task接口;第二部分是定义并实现计算引擎接口。

2.2.1 定义Task接口

Task接口的目的在于,当哭护短提交一个Task给计算引擎(Compute)时,都需要实现Task接口中的execute()方法,以便方Compute执行。Task的定义如下:

public interface Task<T> {

    T execute();
}

2.2.2 定义并实现计算引擎接口

2.2.2.1 定义Compute接口

Compute接口作为远程接口要遵守几个要素:

  1. 实现java.rmi.Remote接口
  2. 接口中的每个方法都应该抛出java.rmi.RemoteException
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Compute extends Remote {

    <T> T executeTask(Task<T> task) throws RemoteException;
}

RMI会对实现java.rmi.Remote的远程对象区别对待,当从服务器传递远程对象到客户端时,RMI并没有传递一份远程对象的拷贝,而是传递了远程对象的存根(也就是远程对象的代理)。

2.2.2.2 实现Compute接口

在Compute接口的实现中,需要实现executeTask方法,另外就是创建Compute对象并注册到RMI注册表中,也就是main方法中的逻辑。

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class ComputeEngine implements Compute {

    @Override
    public <T> T executeTask(Task<T> task) throws RemoteException {
        return task.execute();
    }

    public static void main(String[] args) throws RemoteException {
        if (System.getSecurityManager() == null)
            System.setSecurityManager(new SecurityManager());
        Compute compute = new ComputeEngine();
        Compute stub = (Compute) UnicastRemoteObject.exportObject(compute, 0);

        Registry registry = LocateRegistry.getRegistry();
        registry.rebind("compute", stub);
    }
}
main方法解释:
  1. 该main方法的第一个任务是创建和安装安全管理器。该管理器保护对Java虚拟机内运行的不受信任的下载代码的系统资源的访问。安全管理器确定下载的代码是否可以访问本地文件系统,还是可以执行任何其他特权操作。
    如果RMI程序未安装安全管理器,则RMI不会下载作为参数接收的对象的类(除本地类路径外)或返回远程方法调用的值。此限制可确保下载的代码执行的操作受安全策略的约束。
    更多关于安全管理器的知识待补充。

     if (System.getSecurityManager() == null)
         System.setSecurityManager(new SecurityManager());
    
  2. 创建ComputeEngine远程对象

     Compute compute = new ComputeEngine();
    
  3. 获取远程对象compute的存根
    存根就是远程对象的代理,远程对象的存根实现远程对象实现的同一组远程接口。所以只能从接收Java虚拟机调用远程接口中定义的那些方法。
    java.rmi.server.UnicastRemoteObject用于导出支持JRMP协议的远程对象和与远程对象通信的存根。该exportObject方法返回导出的远程对象的存根.第二个参数指定特定端口导出远程存根,以便接收调用。java.rmi.server.UnicastRemoteObject还提供了其它几个exportObject方法,可以参考API文档。
     Compute stub = (Compute) UnicastRemoteObject.exportObject(compute, 0);
    
  4. 获取注册表并注册远程对象compute的存根

     Registry registry = LocateRegistry.getRegistry();
     registry.rebind("compute", stub);
    

    获取到远程对象之后,为了能够让客户端能够获取到远程对象的存根,需要将远程对象的存根保存到某个地方。JDK提供了远程对象RMI注册表,用于查找,绑定对远程对象存根的引用。RMI注册表时一种简单的远程对象命名服务,它使客户端能够按名称获取远程对象的应用。
    java.rmi.registry.Registry是用于绑定、解绑集查询远程对象的远程接口。下面就是该接口声明的所有方法:

    java.rmi.registry.LocateRegistry用于获取对某个主机上启动RMI注册表的引用或创建一个接受对特定端口调用的RMI注册表。以下就是LocateRegistry的所有方法:

以上就是服务端的所有代码,一旦服务器向本地RMI注册表注册,服务端就可以等待客户端的调用。
另外,只要在另一个Java虚拟机(本地或远程)中有一个对ComputeEngine对象的引用,ComputeEngine就不会被垃圾回收,直到其绑定从注册表中删除,并没有远程客户端持有ComputeEngine对象的远程引用。

2.3 客户端代码

客户端的代码就更简单了,只要实现了Task接口,然后调用远程对象上的方法就好了。

实现Task接口

import java.io.Serializable;
import java.util.Random;

public class TaskImpl implements Task<Integer>, Serializable {

    @Override
    public Integer execute() {
        //execute的实现无关紧要
        return new Random().nextInt();
    }
}

获取远程对象并调用远程方法

步骤大致就是:

  1. 设置SecurityManager
  2. 获取Registry
  3. 检索Compute
  4. 执行远程方法
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ComputeTask {

    public static void main(String[] args) throws RemoteException, NotBoundException {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        Registry registry = LocateRegistry.getRegistry();
        Compute compute = (Compute) registry.lookup("compute");
        TaskImpl taskImpl = new TaskImpl();
        int result = compute.executeTask(taskImpl);
        System.out.println(result);
    }
}

2.4 运行RMI程序

运行RMI程序有3步:

  1. 启动rmiretistry
  2. 启动服务端程序
  3. 启动客户端程序

2.4.1 启动rmiretistry

在linux或mac下执行:

rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false

2.4.2 启动服务端程序

将服务端程序打成jar包

打包就是了。

设置SecurityManager policy文件配置

因为服务器代码中指定了SecurityManager,所以需要有对应的policy文件。

java.policy文件:

grant codeBase "file:${jarPath}" {
    permission java.security.AllPermission;
};

其中${jarPath}是main方法所在jar包的路径。

启动服务端

使用下面的参数启动服务端程序:

java \
-DjarPath=<jar包路径> \
-Djava.rmi.server.codebase=file:///<jar包路径> \
-Djava.security.policy=<policy文件路径> \
-jar <jar包路径>

下面是一个例子:

java \
-DjarPath=/Users/.../RMI/RMI-server/build/libs/RMI-server.jar \
-Djava.rmi.server.useCodebaseOnly=false
-Djava.rmi.server.codebase=file:///Users/.../RMI/RMI-server/build/libs/RMI-server.jar \
-Djava.security.policy=/Users/...RMI/RMI-server/java.policy \
-jar RMI-server.jar

2.4.3 启动客户端程序

将客户端程序打成jar包

打包客户端程序时,因为它依赖于服务器程序的远程接口,所以需要在MANIFEST.MF中指定服务端的jar包:Class-Path: RMI-server.jar

启动客户端

java \
-DjarPath=/Users/.../RMI/RMI-client/build/libs/RMI-client.jar \
-Djava.rmi.server.codebase=file:///Users/.../RMI/RMI-client/build/libs/RMI-client.jar \
-Djava.rmi.server.useCodebaseOnly=false \
-Djava.security.policy=/Users/.../RMI/RMI-client/java.policy \
-jar build/libs/RMI-client.jar

使用gradle命令启动

为了更方便的启动RMI应用程序,将上面的命令封装成了gradle task。参见Github:blog-RMI

参考资料