远程方法调用RMI(Remote Method Invocation),指的是运行在一个Java虚拟机中的对象调用另一个虚拟机上对象的方法,可通过Java RMI实现,Java RMI是分布式应用系统的百分之百纯Java解决方案。
1 通过反射机制简单实现
在分析Java RMI之前,我们先自己做一个简单的实现,其中利用了反射机制。这个例子很常见,来自于孙卫琴老师的《JAVA网络编程精解》第10章(书籍下载链接:https://pan.baidu.com/s/1DgrLoIbHNXi7v9x0vE8iVQ 提取码:lbgk)。流程如下图,比较简单,客户端将Call
对象发送给服务端,其中包含了要在服务端调用的类名,方法名,参数类型及参数值,然后服务端采用反射机制调用对应类对象中的方法,运行后将结果返回客户端。
代码实现:
工程结构:
1.创建远程对象接口
package com.codezhao.rmitest.byreflect;
import java.util.Date;
/**
* @author codeZhao
* @date 2021/1/11 14:06
* @Description 远程对象接口
*/
public interface HelloService {
public String echo(String msg);
public Date getTime();
}
2.远程对象实现
package com.codezhao.rmitest.byreflect;
import java.util.Date;
/**
* @author codeZhao
* @date 2021/1/11 14:07
* @Description 远程对象实现
*/
public class HelloServiceImpl implements HelloService {
@Override
public String echo(String msg) {
return "echo" + msg;
}
@Override
public Date getTime() {
return new Date();
}
}
3.上面已经实现了远程对象,那客户端如何调用其中的echo()
和getTime()
方法呢?
客户端需要把要调用的类名或接口名、方法名、方法参数类型、方法参数值发给服务端传给服务端,服务端再把结果返回给客户端。我们首先创建一个Call
类,将这些需要传递的信息封装。
package com.codezhao.rmitest.byreflect;
import java.io.Serializable;
/**
* @author codeZhao
* @date 2021/1/11 14:09
* @Description Call对象,封装类名、方法名、参数、结果等,用于传输
*/
public class Call implements Serializable {
private static final long serialVersionUID = 588914845779341422L;
private String className; //类名
private String methodName; //方法名
private Class[] paramTypes; //方法参数类型
private Object[] params; //方法参数值
private Object result; //执行结果,方法正常运行时result为方法返回值,方法抛出异常时result为该异常
public Call() {
}
public Call(String className, String methodName, Class[] paramTypes, Object[] params) {
this.className = className;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.params = params;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class[] paramTypes) {
this.paramTypes = paramTypes;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
@Override
public String toString() {
return "SimpleServer{" +
"className='" + className + '\'' +
", methodName='" + methodName + '\'' +
'}';
}
}
4.实现服务端SimpleServer
,其中需要将远程对象注册,即采用Map保存类型与对象的映射;然后监听客户端的消息;处理请求并将结果返回。这里面就需要采用反射机制根据客户端发来的类信息动态调用对应的方法。
这里可能有人会问:通过注册信息直接获取类对应的对象,调用对象中的方法不行吗?这是因为服务端注册有很多不同类型的服务,所以注册的Map中value必须是Object类型对象,而Object类型对象是没法调用具体方法的。
可能还有人问:那干嘛还要注册,直接通过反射获取所有信息不行吗?这是因为客户端传来的一般是接口名,这个接口在服务端可能有多种实现类,所以必须要通过注册明确要调用的是哪个实现类的对象。而客户端传接口名而不是具体实现类的名,这样也可以解耦方便服务端升级而不影响客户端。
package com.codezhao.rmitest.byreflect;
import java.io.*;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* @author codeZhao
* @date 2021/1/11 14:20
* @Description 服务端,注册远程对象、接收客户端消息并处理
*/
public class SimpleServer {
private Map remoteObjects = new HashMap<String, Object>(); //存放远程对象的缓存
//将远程对象放入缓存
public void register(String className, Object remoteObject) {
remoteObjects.put(className, remoteObject);
}
public void server() throws IOException, ClassNotFoundException {
ServerSocket serverSocket = new ServerSocket(8083);
System.out.println("server startup...");
while (true) {
Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
Call call = (Call) ois.readObject(); //接受客户端发来的Call对象
System.out.println(call);
call = invoke(call); //调用对应远程对象方法
oos.writeObject(call);
ois.close();
oos.close();
socket.close();
}
}
//解析call中信息,调用对应远程对象的方法,将结果写入call中并返回
public Call invoke(Call call){
Object result = null;
try {
String className = call.getClassName();
String methodName = call.getMethodName();
Class[] paramTypes = call.getParamTypes();
Object[] params = call.getParams();
Class classType = Class.forName(className);
Method method = classType.getMethod(methodName, paramTypes);
Object remoteObject = remoteObjects.get(className);
if (remoteObject == null) {
throw new Exception(className + "的远程对象不存在");
} else {
result = method.invoke(remoteObject, params);
}
} catch (Exception e) {
result = e;
}
call.setResult(result);
return call;
}
public static void main(String[] args) throws Exception {
SimpleServer server = new SimpleServer();
server.register("com.codezhao.rmitest.byreflect.HelloService",
new HelloServiceImpl());
server.server();
}
5.客户端发起请求
package com.codezhao.rmitest.byreflect;
import java.io.*;
import java.net.Socket;
/**
* @author codeZhao
* @date 2021/1/11 22:05
* @Description 客户端,远程方法调用实现
*/
public class SimpleClient {
public void invoke() throws Exception {
Socket socket = new Socket("localhost", 8083);
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//创建远程对象
Call call = new Call("com.codezhao.rmitest.byreflect.HelloService",
"echo",
new Class[]{String.class},
new Object[]{"Java"});
oos.writeObject(call); //向服务端发送Call对象
call = (Call) ois.readObject(); //接收服务端返回的Call对象
System.out.println(call.getResult());
ois.close();
oos.close();
socket.close();
}
public static void main(String[] args) throws Exception {
SimpleClient client = new SimpleClient();
client.invoke();
}
}
6.启动服务端SimpleServer
,再启动客户端SimpleClient
2 Java RMI实现
Java RMI的api包其实就是封装了以上我们的实现,包括远程对象的注册、Call对象的实现、io操作、网络通讯等,同时也解决了分布式垃圾收集、安全检查和并发性等通用问题,开发人员只需关注特定问题的实现即可。
Java RMI中几个重要的概念:
(1)远程对象
处在服务端机器中,由客户端发起请求,调用远程对象中的方法。
(2)存根(Stub)和骨架(Skeleton)
Java RMI为远程对象分别生成了客户端代理和服务器端代理,位于客户端的代理类称为存根(Stub),位于服务器端的代理类称为骨架(Skeleton)。客户端代理类Stub会打包调用信息(类名、方法名、变量等),然后发送给服务器端代理类Skeleton,服务器端代理类Skeleton将收到的信息解包,找到远程对象及对应方法,Skeleton将方法执行结果打包发送给Stub,Stub解包后返回给调用者。
(3)Remote接口
Remote接口用来标识一个对象为远程对象,只有实现了Remote接口的对象才能被Java RMI作为远程对象使用。
Java RMI调用过程:
代码实现:
功能:客户端调用服务端方法,计算矩形面积。
说明:1~3为server代码,4为client代码,server和client放在不同module中,工程结构如下,注意远程对象在server和client的类路径要相同。
工程结构:
1.定义远程对象接口
注意方法也要抛出RemoteException
异常。
package com.codezhao.rmitest.byrmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @author codeZhao
* @date 2021/1/11 10:33
* @Description 远程对象接口
*/
public interface Rectangle extends Remote {
public void setLength(double length) throws RemoteException;
public void setWidth(double width) throws RemoteException;
public double getArea() throws RemoteException;
}
2.远程对象的实现
package com.codezhao.rmitest.byrmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* @author codeZhao
* @date 2021/1/11 10:36
* @Description 远程对象实现类
*/
public class RectangleImpl extends UnicastRemoteObject implements Rectangle {
private double length,width;
public RectangleImpl() throws RemoteException {
super();
}
@Override
public void setLength(double length) throws RemoteException{
this.length = length;
}
@Override
public void setWidth(double width) throws RemoteException{
this.width = width;
}
@Override
public double getArea() throws RemoteException{
return length * width;
}
}
3.注册远程对象
package com.codezhao.rmitest.byrmi;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
/**
* @author codeZhao
* @date 2021/1/11 11:07
* @Description 注册远程对象
*/
public class BindRectangleServer {
public static void main(String[] args) {
try {
RectangleImpl rectangle = new RectangleImpl();
//远程对象注册表实例
LocateRegistry.createRegistry(8083);
//将远程对象注册到RMI注册服务器上
Naming.bind("rmi://localhost:8083/Rectangle", rectangle);
System.out.println("server:");
System.out.println("绑定成功");
while (true);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
4.客户端调用
package com.codezhao.rmitest.byrmi;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @author codeZhao
* @date 2021/1/11 11:21
* @Description 客户端调用
*/
public class RectangleClient {
public static void main(String[] args) {
try {
//根据url在RMI注册表中查找远程对象
Remote remote = Naming.lookup("rmi://localhost:8083/Rectangle");
Rectangle rectangle = (Rectangle) remote;
//调用远程对象方法
rectangle.setLength(3);
rectangle.setWidth(4);
double area = rectangle.getArea();
System.out.println("client:");
System.out.println("area:" + area);
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
5.首先运行BindRectangleServer
,然后运行RectangleClient
,结果如下:
总结
本篇介绍了RMI是什么、怎么实现?重点介绍了Java RMI的实现方式。
欢迎关注我的公众号【codeZhao】,获取更多技术干货和职业思考。