可运行的Java RMI示例和踩坑总结
简述
资料参考:
- https://docs.oracle.com/javase/tutorial/rmi/overview.html
- https://blog.csdn.net/bigtree_3721/article/details/50614289
- https://stackoverflow.com/questions/11273353/java-rmi-and-classnotfoundexception
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示例和踩坑总结的更多相关文章
- 避坑手册 | JAVA编码中容易踩坑的十大陷阱
JAVA编码中存在一些容易被人忽视的陷阱,稍不留神可能就会跌落其中,给项目的稳定运行埋下隐患.此外,这些陷阱也是面试的时候面试官比较喜欢问的问题. 本文对这些陷阱进行了统一的整理,让你知道应该如何避免 ...
- java 注意事项---避免踩坑
1.......对象参数接收不能大写
- 【java】Split函数踩坑记
先看一段代码: String line = "openssh|7.1"; String[] pkg = line.split("|"); System.out. ...
- Java RMI 简单示例
一.创建远程服务 1.创建 Remote 接口,MyRemote.java import java.rmi.*; public interface MyRemote extends Remote{ p ...
- Java RMI 介绍和例子以及Spring对RMI支持的实际应用实例
RMI 相关知识 RMI全称是Remote Method Invocation-远程方法调用,Java RMI在JDK1.1中实现的,其威力就体现在它强大的开发分布式网络应用的能力上,是纯Java的网 ...
- Java RMI(远程方法调用)开发
参考 https://docs.oracle.com/javase/7/docs/platform/rmi/spec/rmi-arch2.html http://www.cnblogs.com/wxi ...
- java RMI原理详解
java本身提供了一种RPC框架——RMI(即Remote Method Invoke 远程方法调用),在编写一个接口需要作为远程调用时,都需要继承了Remote,Remote 接口用于标识其方法可以 ...
- Java RMI 的使用及原理
1.示例 三个角色:RMIService.RMIServer.RMIClient.(RMIServer向RMIService注册Stub.RMIService在RMIClient lookup时向其提 ...
- Java RMI 入门指南
开通博客也有好些天了,一直没有时间静下心来写博文,今天我就把两年前整理的一篇关于JAVA RMI入门级文章贴出来,供有这方面需要的同学们参考学习. RMI 相关知识 RMI全称是Remote Meth ...
随机推荐
- PXE基础装机环境
PXE基础装机环境 案例1:PXE基础装机环境 案例2:配置并验证DHC ...
- flask-redirect
flask-redirect from flask import Flask, url_for, request, redirect app = Flask(__name__) @app.route( ...
- 萌新带你开车上p站(二)
本文作者:萌新 前情提要:萌新带你开车上p站(一) 0x04flag 看题目描述似乎是一个和脱壳相关的逆向题目 按照给出的地址先下载过来 file看看 是个可执行文件 执行之 emm什么都看不出来, ...
- easy-mock 本地部署(挤需体验三番钟,里造会干我一样,爱象节款mock)
前言 很多小伙伴问我怎么在自己公司的项目里面添加配置mock,在vue项目里面都知道怎么配置mock,在大型前端项目里面就一脸疑惑了. 我就回答他,你今天会在vue项目里面用,那天换公司是用angul ...
- 【DataBase】 在Windows系统环境 下载和安装 解压版MySQL数据库
MySQL官网解压版下载地址:https://dev.mysql.com/downloads/mysql/ 为什么不推荐使用安装版?无脑下一步,很多配置的东西学习不到了 点选第一个就好了,下面的是调试 ...
- V - Largest Rectangle in a Histogram HDU - 1506
两种思路: 1 单调栈:维护一个单调非递减栈,当栈为空或者当前元素大于等于栈顶元素时就入栈,当前元素小于栈顶元素时就出栈,出栈的同时计算当前值,当前值所包含的区间范围为从当前栈顶元素到当前元素i的距离 ...
- X - Ehab and Path-etic MEXs CodeForces - 1325C
MMP,差一点就做对了. 题目大意:给你一个树,对这个树的边进行编号,编号要求从0到n-1,不可重复,要求MEX(U,V)尽可能的小, MEX(x,y)的定义:从x到y的简单路径上,没有出现的最小编号 ...
- gdb 调试中No symbol “***” in current context解决方法
主要是因为GCC/G++版本和GDB不匹配造成的,网上也有说是因为O2优化问题,具体啥原因需要自己尝试一下. 解决: 放狗搜索,解决办法是在编译是加-gdwarf-3即可,出现这样的原因是gcc,gd ...
- Postman:Pre-request Script
Pre-request Script:前置处理,会在发出请求前执行,主要用在生成一些动态参数. 例如:api接口都会有签名校验,这个校验在我们api测试的时候很不方便,这里可以利用 postman 前 ...
- 详解 File类
在讲解File类之前,本人先要讲解下 路径,因为我们对于文件的操作是离不开路径的: 目录 路径: File类 文件名称过滤器: 路径: 请观看本人博文 -- <详解 绝对路径与 相对路径> ...