简述

资料参考:

RMI特点

  • Java原生提供
  • 可以根据一个名字来获取远程对象
  • 调用远程对象的方法时,RMI屏蔽了底层通信细节,与远程通信就像调用本地方法一样
  • 远程动态加载类的定义,这是RMI非常独特的功能

远程对象

面向接口,接口的实现类可以位于不同的JVM,这些用于远程调用的实现类称为 remote objects (远程对象)。

远程对象有如下特征:

  • 实现接口java.rmi.Remote
  • 对象中的每个方法必须声明可能抛出java.rmi.RemoteException

RMI对于从另一个虚拟机传递过来的远程对象视为和本地对象一样。客户端使用stub来作为远程对象的代理,对stub进行方法调用,会反映到远程对象的方法调用上,stub对象实现了与远程对象相同的接口。

使用RMI构建分布式应用

后续简称提供远程调用服务的为服务端,使用远程服务的为客户端。

有如下步骤:

  • 设计和实现应用中的组件,确定哪些对象需要被远程访问,然后定义远程接口(接口中是可被远程调用的方法),客户端仅仅存在接口的定义而没有实现,服务端提供实现。
  • 编译资源
  • 对类进行标记,使其可以通过网络传输,类的定义会通过网络进行传输到另一个JVM上
  • 启动RMI仓库、服务端和客户端

实战

本示例旨在使用RMI技术来构造一个通用的计算引擎,接收多个客户端的自定义任务,运算后返回结果。任务由一个特定接口来抽象,具体要做什么由客户端来定义。RMI动态加载任务代码到计算引擎的JVM中,再执行任务,这种系统通常叫做behavior-based application面向行为的应用

后续为了使条理更清晰,会在标题指出类所在的工程和包名。

示例运行环境:openjdk1.8

编写RMI服务端程序

设计远程接口(位于server程序中的com.test.rmi.common包)

// 描述了客户端的任务
// RMI使用jdk序列化来传输对象,所以这个Task的实现类必须要实现 java.io.Serializable 标记接口
public interface Task<T> {
T execute();
} // 接收远程任务Task,执行后返回结果
// 这个接口拓展了Remote,实现了这个接口的对象就称为远程对象
public interface Compute extends Remote {
// 支持被远程调用的方法,这个方法必须要声明,可能会抛出 RemoteException,当出现协议错误或通信错误时,RMI框架会抛出这个异常
<T> T executeTask(Task<T> t) throws RemoteException;
}

实现远程接口(位于server程序中的com.test.rmi.server包)

RMI服务端需要在运行时创建和实例化这些远程对象,并把他们暴露出去

// 实现远程任务接口,当前实现类就是远程对象了
public class ComputeEngine implements Compute {
// 实现远程接口中的方法
@Override
public <T> T executeTask(Task<T> t) {
// 返回的可能是任意类型,这些类型必须要实现Serializable接口
// 除了 static 或 transient 以外的字段都会被序列化传输
return t.execute();
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
// 注册 SecurityManager,用于保护本地资源
// 因为RMI会下载远程的类到本地来运行,SecurityManager会判断这些代码是否有权限执行某些操作
// 如果不注册这个,RMI不会执行远程代码
System.setSecurityManager(new SecurityManager());
}
try {
// 创建和导出远程对象
// 只有导出之后的对象才可以被其他客户端远程调用
String name = "Compute";
Compute engine = new ComputeEngine();
// 指定监听的服务端口为0,即运行时会随机选中一个可用的端口来使用
// 导出成功后返回的stub对象必须是远程接口类型
Compute stub = (Compute) UnicastRemoteObject.exportObject(engine, 0);
// 注册远程对象到RMI仓库中(或其他命名服务)
// RMI仓库是一种特殊的远程对象,用于根据名字查找其他远程对象,可以使客户端根据名字获取远程对象的引用
// LocateRegistry有其他静态方法可以创建一个新的RMI仓库,这里先不用
// getRegistry方法不指定参数的话,则默认从本地的1099端口中获取RMI仓库,可以指定为其他端口
Registry registry = LocateRegistry.getRegistry();
// rebind是一个对RMI仓库的远程调用,所以这个方法可能抛出 RemoteException
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
// 这里不需要使用阻塞来保持main线程的存活
// 因为只要 ComputeEngine 注册到了外部的RMI仓库上(RMI仓库持有了这个对象的引用), 这个远程对象就不会被GC,RMI框架就会保持当前线程的存活
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}

以上指定了SecurityManager,所以需要再创建一个文件,如名为server.policy,内容如下:

grant codeBase "file:C:\\Users\\94713\\Downloads\\decorator-master\\target\\classes" {
permission java.security.AllPermission;
};

以上指定的路径为我本地idea工程的输出类目录,实际运行时指定为jar包所在的路径即可。

指定这个的用途是使JVM对特定路径下的代码文件进行权限控制,如上面就赋予所有执行权限,因为这是我本地的代码,所以完全信任是没有问题的。

编写RMI客户端程序

复用远程接口(位于client程序中的com.test.rmi.common包)

复用与Server端相同的远程接口,直接拷贝server端的com.test.rmi.common

// 描述了客户端的任务
// RMI使用jdk序列化来传输对象,所以这个Task的实现类必须要实现 java.io.Serializable 标记接口
public interface Task<T> {
T execute();
} // 接收远程任务Task,执行后返回结果
// 这个接口拓展了Remote,实现了这个接口的对象就称为远程对象
public interface Compute extends Remote {
// 支持被远程调用的方法,这个方法必须要声明,可能会抛出 RemoteException,当出现协议错误或通信错误时,RMI框架会抛出这个异常
<T> T executeTask(Task<T> t) throws RemoteException;
}

定义客户端任务(位于client程序中的com.test.rmi.client包)

// 因为任务需要被传输,所以除了要实现Task接口以外,还要实现序列化接口
public class Pi implements Task<BigDecimal>, Serializable {
private int taskData;
public Pi(int taskData) {
this.taskData = taskData;
}
private static final long serialVersionUID = 227L; @Override
public BigDecimal execute() {
System.out.println("这里进行复杂计算");
// 模拟复杂任务,对数据+2返回
return BigDecimal.valueOf(taskData + 2);
}
}

开始调用

public class ComputePi {
public static void main(String args[]) {
// 和服务端一样,也是为了安全,因为客户端也会下载服务端中的代码来执行,如获取调用的返回值
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try { String name = "Compute";
// 根据一个host来获取RMI仓库,默认端口为1099
Registry registry = LocateRegistry.getRegistry("127.0.0.1");
// 根据名字从RMI仓库中查找远程对象
Compute comp = (Compute) registry.lookup(name);
Pi task = new Pi(54);
BigDecimal pi = comp.executeTask(task);
System.out.println(pi);
} catch (Exception e) {
System.err.println("ComputePi exception:");
e.printStackTrace();
}
}
}

和server端一样,客户端这里也需要创建一个权限文件,我这里名为client.policy,内容为

grant codeBase "file:C:/Users/94713/Desktop/demo/target/classes" {
permission java.security.AllPermission;
};

指定了client工程的输出类目录,作用在server端已解释过,这里不再赘述。

这里存在三者关系:客户端、服务端、RMI仓库

  • 客户端从RMI仓库中获取远程对象
  • 远程对象实际存在于服务端
  • 客户端对远程对象进行方法调用,本质上是触发了服务端内的运算

示例中很关键的特点是:服务端要执行Pi这个运算任务,却不需要Pi这个任务类的定义,因为它运行时会从网络传递到服务端,实现了服务端与具体任务类的解耦

编译和运行程序

启动RMI仓库服务程序(jdk1.7以后需要指定参数useCodebaseOnly为false,否则会提示类找不到)

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

远程调用过程中需要提供类定义的下载,所以需要再启动一个静态文件服务。

我这里使用nodejs的一个第三方静态服务anywhere,能将指令运行的目录作为根目录,端口默认为8000

anywhere

此时的静态文件服务中没有文件,先不用放文件进去,等下再放。

指定参数运行服务端程序:

-Djava.rmi.server.codebase=http://127.0.0.1:8000/  -Djava.security.policy=C:\Users\94713\Desktop\p\server.policy
  • codebase指定的路径为刚刚部署的静态服务根目录
  • policy指定的路径为服务端的权限文件

运行起来之后会报错,提示有些类找不到,把对应缺少的类从服务端拷贝到静态文件服务的根目录上即可,如把com\test\rmi\common\Task.class连同包名目录一起拷贝过去,因为下载时就是根据类的全限定名转换成目录层级来查找下载的。

指定参数运行客户端

-Djava.security.policy=C:\Users\94713\Desktop\p\client.policy

也把提示缺少的类从客户端程序拷贝到静态服务上即可,此时程序能正常运行。

过程总结

以上忽略了一些我采坑的过程,这里直接给出结论。

服务端往RMI仓库中注册远程时,是先进行jdk序列化,传输到RMI仓库,传输的数据仅仅是对象的成员属性,而没有类的定义,所以RMI仓库反序列化时必须从某个地方下载类的定义,才能反序列成功。这个下载的地方就是服务端指定的运行参数-Djava.rmi.server.codebase=http://127.0.0.1:8000/

没有这个静态服务或者静态服务中没有对应的class文件的话,则服务端运行会报错,提示类找不到。其实这个错误的堆栈信息不是对应服务端的,而是对应RMI仓库程序的,仓库那边报错了,收集好堆栈信息后反馈给服务端,服务端再抛出来而已。

服务端和客户端存在一些公共的接口,他们的全限定名必须一致,否则运行过程中进行类型转换就会报错:

java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to com.example.agent.rmi.Compute
at com.example.agent.rmi.ComputePi.main(ComputePi.java:19)

所以更好的做法是将公共的类打成一个jar包,然后将jar包拷贝给服务端和客户端。直接拷贝java文件和对应的包层级到客户端或服务端中容易出错

启动RMI仓库时,官方的运行示例是不带参数的,而我的示例中添加了一个参数-Djava.rmi.server.useCodebaseOnly=false,这是因为jdk7以后有了变化,不加这个参数会导致RMI仓库程序要反序列类时不会从我指定的codebase路径中去下载,就会提示类找不到。(网上对此的解决办法是将类添加到RMI仓库程序的classpath上也能解决,但是在是太不优雅了而且麻烦)

关于SecurityManager。以上客户端和服务端都指定了,这是为了安全考虑,如服务端要接受任务来执行、客户端接收任务的返回值,这两个过程都可能需要从外部下载类的定义,并且运行类。被运行的类可能是很不安全的,所以直接运行可能导致出现严重后果,所以需要对这些代码做权限控制。

具体的控制办法就是在权限文件中完全信任本地的代码,除了本地的代码以外不信任。这样外部的代码运行权限就会小很多。如此时外部的代码想要连接某个外部服务,程序不会执行连接行为,并且会报错:

java.security.AccessControlException: access denied ("java.net.SocketPermission" "127.0.0.1:1099" "connect,resolve")

从而限制了外部代码的行为,保障本地程序的安全。

这样权限很低的代码具体能做什么,这点我还没去研究。但从以上示例中可以看出,执行简单的数字运算和控制台输出是没问题的。

可运行的Java RMI示例和踩坑总结的更多相关文章

  1. 避坑手册 | JAVA编码中容易踩坑的十大陷阱

    JAVA编码中存在一些容易被人忽视的陷阱,稍不留神可能就会跌落其中,给项目的稳定运行埋下隐患.此外,这些陷阱也是面试的时候面试官比较喜欢问的问题. 本文对这些陷阱进行了统一的整理,让你知道应该如何避免 ...

  2. java 注意事项---避免踩坑

    1.......对象参数接收不能大写

  3. 【java】Split函数踩坑记

    先看一段代码: String line = "openssh|7.1"; String[] pkg = line.split("|"); System.out. ...

  4. Java RMI 简单示例

    一.创建远程服务 1.创建 Remote 接口,MyRemote.java import java.rmi.*; public interface MyRemote extends Remote{ p ...

  5. Java RMI 介绍和例子以及Spring对RMI支持的实际应用实例

    RMI 相关知识 RMI全称是Remote Method Invocation-远程方法调用,Java RMI在JDK1.1中实现的,其威力就体现在它强大的开发分布式网络应用的能力上,是纯Java的网 ...

  6. Java RMI(远程方法调用)开发

    参考 https://docs.oracle.com/javase/7/docs/platform/rmi/spec/rmi-arch2.html http://www.cnblogs.com/wxi ...

  7. java RMI原理详解

    java本身提供了一种RPC框架——RMI(即Remote Method Invoke 远程方法调用),在编写一个接口需要作为远程调用时,都需要继承了Remote,Remote 接口用于标识其方法可以 ...

  8. Java RMI 的使用及原理

    1.示例 三个角色:RMIService.RMIServer.RMIClient.(RMIServer向RMIService注册Stub.RMIService在RMIClient lookup时向其提 ...

  9. Java RMI 入门指南

    开通博客也有好些天了,一直没有时间静下心来写博文,今天我就把两年前整理的一篇关于JAVA RMI入门级文章贴出来,供有这方面需要的同学们参考学习. RMI 相关知识 RMI全称是Remote Meth ...

随机推荐

  1. 基于 Spring Cloud 的微服务架构实践指南(下)

    show me the code and talk to me,做的出来更要说的明白 本文源码,请点击learnSpringCloud 我是布尔bl,你的支持是我分享的动力! 一.引入 上回 基于 S ...

  2. spring03

    学习了spring的数据源的使用以及spring的作用域引入外部属性文件 对应的bean的xml文件和properties文件如下 <?xml version="1.0" e ...

  3. 关于Python 迭代器和生成器 装饰器

    Python 简介Python 是一个高层次的结合了解释性.编译性.互动性和面向对象的脚本语言. Python 的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比 ...

  4. 外观模式(c++实现)

    外观模式 目录 外观模式 模式定义 模式动机 UML类图 源码实现 优点 缺点 模式定义 外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子 ...

  5. Win10 cmd的ssh命令连接linux虚拟机

    其实就是一个小发现了~ 闲的没事的时候在cmd里面敲了ssh命令,居然提示是一个命令,貌似以前是没有这功能的.然后就打开虚拟机试试能不能远程连接.没想到还成功了~ 有了这功能就省得安装专门的远程连接工 ...

  6. stand up meeting 11/20/2015

    3组员 今日工作 工作耗时/h 明日计划 计划耗时/h 冯晓云 将输出string里的翻译合理取分为动名词等各种词性,按约定格式返回,按热度排列,但每一个词性下的解释仍然是由“$$”分词:对于查询词为 ...

  7. Linux相关操作

    ssh配置秘钥 连接远程服务器时:需要用户持有“公钥/私钥对”,远程服务器持有公钥,本地持有私钥. 客户端向服务器发出请求.服务器收到请求之后,先在用户的主目录下找到该用户的公钥,然后对比用户发送过来 ...

  8. 从hfctf学习JWT伪造

    本文作者:Ch3ng easy_login 简单介绍一下什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519) ...

  9. Redis Linux安装+配置

    1.进入指定目录,下载资源(也可本地下载后复制到指定目录) wget http://download.redis.io/releases/redis-5.0.5.tar.gz 2.解压到指定目录 ta ...

  10. Java数组 —— 八大排序

    (请观看本人博文--<详解 普通数组 -- Arrays类 与 浅克隆>) 在本人<数据结构与算法>专栏的讲解中,本人讲解了如何去实现数组的八大排序. 但是,在讲解的过程中,我 ...