RMI(Remote Method Invocation),翻译过来是远程方法调用的意思。所谓远程方法调用,实际上就是在不同系统之间的方法调用。比如说,A系统调用B系统的方法。RMI技术使得Java能够实现基于RPC的应用程序。
但是,对于客户对象来说,第2~6步都是透明的。也就意味着,开发人员无须关心任何网络传输的细节问题。
(1)定义服务端
// 定义业务接口
public interface IBiz extends Remote {
String sayHello(String name) throws RemoteException;
}
业务接口需要继承Remote父接口,而且接口中的方法需要抛出RemoteException异常。
// 业务实现类
public class BizImpl extends UnicastRemoteObject implements IBiz {
public BizImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
System.out.println("executing sayHello...");
return "hello " + name;
}
}
接口实现类需要继承UnicastRemoteObject父类,并且提供一个无参构造函数。
定义完成后,接下来新建一个服务测试类。
public class Test {
public static void main(String[] args) {
try {
IBiz biz = new BizImpl();
// 注册对外服务的端口号
LocateRegistry.createRegistry(8080);
// 绑定服务的识别名
Naming.bind("rmi://127.0.0.1:8080/hello", biz);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面代码首先新建一个业务对象,然后注册对外服务的端口号,最后把业务对象与服务的识别名进行绑定。
(2)定义客户端
首先,把上面业务接口类复制到客户端工程中。
public interface IBiz extends Remote {
String sayHello(String name) throws RemoteException;
}
注意,为了与服务端的业务接口保持一致,尽量不要任意修改业务接口方法的信息,否则可能会调用失败。
准备完成后,我们定义一个客户端测试类。
public class Test {
public static void main(String[] args) {
try {
// 连接远程服务,如果远程服务没有开启,就会抛出ConnectionException异常
IBiz biz = (IBiz) Naming.lookup("rmi://127.0.0.1:8080/hello");
// 调用服务对象方法
String info = biz.sayHello("aa");
System.out.println(info);
} catch (Exception e) {
e.printStackTrace();
}
}
}
该测试类的逻辑也很简单,首先调用java.rmi.Naming类提供的lookup方法连接远程服务。如果连接失败,giant方法会抛出ConnectionException异常。如果连接成功,我们就可以想调用本项目对象方法一样去调用远程服务对象的方法。
RMI技术存在不足的地方:
基于上述原因分析,我们有理由自己构建出一套无需太多开发规范限制,能够自由扩展加密方式、底层网络协议实现方式的RPC调度模块。如果借鉴Java原生的RMI技术实现自己的RPC组件,主要涉及到的技术点:
网络编程和反射方面的内容这里不去研究,我们主要了解一下动态代理技术。
代理模式是一种常见的设计模式,其目的是为了对其他对象提供一个代理以控制某个真实对象的方法。代理模式又分为静态代理和动态代理两种。其中动态代理是我们重点要讲解的部分。
Java动态代理可以动态创建并动态处理所代理对象的方法调用。在代理对象上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。
动态代理类是目标类的代理,它继承了目标类。使用动态代理后,客户端不再直接调用目标类方法,而是通过目标类的代理Proxy间接地调用目标类的方法。每次调用调用代理类方法的时候,都会自动执行代理处理器的invoke方法。我们可以根据业务需求在调用目标类的前后加入一些处理。例如:事务控制、记录日志等。
下面是Java实现动态代理的代码:
第一步:新建业务接口。
public interface IBiz extends Remote {
String sayHello(String name) throws RemoteException;
}
第二步:新建一个动态代理处理器类,该类实现了InvocationHandler接口,并实现它的invoke方法。
/*
动态代理处理器
*/
public class DynamicProxyHandler implements InvocationHandler {
/**
* 方法描述:调用代理对象时候自动触发该方法
* @param proxy 代理对象
* @param method 调用方法
* @param args 参数列表
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("执行代理对象的invoke方法...");
System.out.println("调用方法名:" + method.getName());
if (args != null && args.length > 0) {
System.out.println("传入参数:" + Arrays.toString(args));
}
return null;
}
}
第三步:定义完成后,我们新建一个测试类。
public class DynamicProxyTest {
public static void main(String[] args) {
try {
// 创建IBiz的代理对象
IBiz biz = (IBiz) Proxy.newProxyInstance(IBiz.class.getClassLoader()
, new Class[]{IBiz.class}
, new DynamicProxyHandler());
// 调用代理对象方法,并获取返回结果
String info = biz.sayHello("jacky");
// 输出返回结果
System.out.println(info);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面代码中,Proxy是JDK提供的一个动态代理类。它提供了一个newProxyInstance的静态方法创建代理对象。该方法第一个参数代表代理类的类加载器,第二个参数代表要动态代理的业务接口,第三个参数代表一个动态代理的处理器。只有当调用代理对象方法的时候,处理器的invoke方法才会自动被触发。
RPC服务端的目录结构如下图所示:
ConnectThread:线程类,用于监听客户端连接
Service:服务工具类,提供一些访问业务对象的方法;
ServiceFlag:服务标识类,在该类中定义了一些常量,用来标识不同的指令操作;
ServiceNotFoundException:异常类
ServiceThread:线程类,处理客户端发送过来的请求;
IBiz:业务接口类;
BizImpl:业务接口的实现类;
Server:测试类
具体代码实现:
(1)定义业务接口和实现类
public interface IBiz {
String sayHello(String name);
}
public class BizImpl implements IBiz {
@Override
public String sayHello(String name) {
return "hello " + name;
}
}
(2)服务工具类
public class Service {
// 服务端Socket
public static ServerSocket server;
// 服务端Socket默认监听的端口号
private static int servicePort = 1689;
// 如果server为空,该方法用于指定服务端Socket的端口号
public static void setPort(int port) {
if (server == null) {
servicePort = port;
}
}
// 该map保存了服务名和业务对象的对应关系
private static Hashtable bindservice = new Hashtable();
// 把bindName和bindObject组成的键值对添加到bindservice中
public static void bind(String bindName, Object bindObject) {
// 如果server为空,启动一个线程去监听客户端连接
// 如果有客户端连接进来,就读取客户端发送过来的数据,并返回结果
if (server == null) {
try {
server = new ServerSocket(servicePort);
ConnectThread ct = new ConnectThread(server);
ct.start();
} catch (Exception ex) {
ex.printStackTrace();
}
}
bindservice.put(bindName, bindObject);
}
/**
* 根据服务名获取绑定接口的名字
* @param bindName 服务名
* @return 返回服务名对应业务对象实现接口的名称
*/
public static String getBindObjectInterface(String bindName) {
// 定义一个变量,保存业务对象的接口
Class> interfaceClass = null;
try {
// 根据服务名获取对应的业务对象
Class> nowClass = bindservice.get(bindName).getClass();
// 获取业务对象实现的接口,如果没有,就从它的父类获取父类实现接口,以此类推,直到获取到实现接口为止。
while (nowClass.getSuperclass() != null && interfaceClass == null) {
if (nowClass.getInterfaces().length > 0) {
interfaceClass = nowClass.getInterfaces()[0];
break;
}
nowClass = nowClass.getSuperclass();
}
// 返回接口的名称
return interfaceClass.getCanonicalName();
} catch (Exception ex) {
return "";
}
}
/**
* 获取方法的返回值
* @param bindName 服务名
* @param methodName 方法名
* @param argtypes 参数类型列表
* @param args 实参列表
* @return 方法返回值
*/
public static Object getInvokeReturn(String bindName, String methodName,
Class>[] argtypes, Object[] args) {
try {
// 根据服务名获取业务对象
Object obj = bindservice.get(bindName);
// 根据方法名和参数类型获取该方法的Method对象
Method method = obj.getClass().getMethod(methodName, argtypes);
// 调用方法,并获取方法返回值
Object returnObj = method.invoke(obj, args);
// 返回结果
return returnObj;
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}
(3)监听客户端的线程类
public class ConnectThread extends Thread {
// 定义服务端Socket
ServerSocket server;
// 定义一个线程池
Executor exec = new ScheduledThreadPoolExecutor(50);
public ConnectThread(ServerSocket server) {
// 初始化服务端Socket
this.server = server;
}
public void run() {
while (true) {
try {
// 监听客户端连接
Socket client = server.accept();
// 在线程池中启动一条线程处理客户端请求
ServiceThread st = new ServiceThread(client);
exec.execute(st);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
(4)处理客户端请求的线程类
public class ServiceThread implements Runnable {
// 客户端Socket
Socket client;
public ServiceThread(Socket client) {
// 初始化客户端Socket
this.client = client;
}
public void run() {
// 服务名
String bindName = "";
try {
// 创建字节输入流对象,用于读取客户端发送过来的数据
DataInputStream dis = new DataInputStream(client.getInputStream());
// 创建字节输出流对象,用于向客户端返回结果
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
// 读取命令,1代表调用接口方法, 2代表调用带返回值接口方法
int command = dis.readInt();
// 判断
switch (command) {
// 如果command等于1,那么就读取服务名,然后把接口名发送给客户端
case ServiceFlag.SERVICE_GETINTERFACE:
// 读取服务名
bindName = dis.readUTF();
// 获取接口名,然后发送给客户端
dos.writeUTF(Service.getBindObjectInterface(bindName));
break;
// 如果command等于2,那么就读取服务名、方法名、参数类型和参数名称,然后把序列化后的返回值发送给客户端
case ServiceFlag.SERVICE_GETINVOKERETURN:
// 读取服务名
bindName = dis.readUTF();
// 读取方法名
String methodName = dis.readUTF();
// 读取参数类型
ObjectInputStream ois = new ObjectInputStream(
client.getInputStream());
Class>[] argtypes = (Class[]) ois.readObject();
// 读取实参
Object[] args = (Object[]) ois.readObject();
// 调用方法,并获取返回值
Object obj = Service.getInvokeReturn(bindName, methodName,
argtypes, args);
// 对返回值进行序列化操作
ObjectOutputStream oos = new ObjectOutputStream(
client.getOutputStream());
// 把序列化后的结果发送给客户端
oos.writeObject(obj);
break;
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
(5)服务标识类
public interface ServiceFlag {
int SERVICE_GETINTERFACE = 1; // 调用接口方法
int SERVICE_GETINVOKERETURN = 2; // 调用带返回值接口方法
}
(6)自定义异常类
public class ServiceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 6482029944969529522L;
public ServiceNotFoundException() {
super("没有该服务!");
}
}
(7)服务端测试类
public class Server {
public static void main(String[] args) {
// 创建业务对象
IBiz biz = new BizImpl();
// 把业务对象和服务名进行绑定,并启动线程监听客户端连接
Service.bind("hello", biz);
}
}
ConnectThread:线程类,用于监听客户端连接
Service:服务工具类,提供一些访问业务对象的方法;
ServiceFlag:服务标识类,在该类中定义了一些常量,用来标识不同的指令操作;
ServiceNotFoundException:异常类
ServiceThread:线程类,处理客户端发送过来的请求;
IBiz:业务接口类;
BizImpl:业务接口的实现类;
Server:测试类
具体代码实现:
(1)业务接口
public interface IBiz {
String sayHello(String name);
}
该业务接口可以从服务端拷贝过来。
(2)服务工具类
public class Service {
// 服务端Socket默认监听的端口号
private static int servicePort = 1689;
/**
* 连接远程服务
* @param address IP地址
* @param bindName 服务名
* @return 返回代理对象
* @throws ServiceNotFoundException
*/
public static Object lookup(String address, String bindName) throws ServiceNotFoundException {
try (
// 创建客户端Socket对象
Socket client = new Socket(InetAddress.getByName(address), servicePort);
// 创建字节输出流对象
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
// 创建字节输入流对象
DataInputStream dis = new DataInputStream(client.getInputStream());
) {
// 发送操作指令
dos.writeInt(ServiceFlag.SERVICE_GETINTERFACE);
// 发送服务名
dos.writeUTF(bindName);
// 读取服务端发送过来的接口名称
String interfaceName = dis.readUTF();
// 根据接口名获取Class对象
Class> interfaceClass = Class.forName(interfaceName);
// 创建动态代理处理器
ServiceHandler handler = new ServiceHandler(servicePort, bindName, address);
// 创建接口的代理对象
Object proxyObj = Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class[] { interfaceClass },
handler);
// 返回代理对象
return proxyObj;
} catch (Exception ex) {
ex.printStackTrace();
throw new ServiceNotFoundException();
}
}
}
(3)动态代理处理器
public class ServiceHandler implements InvocationHandler {
// 端口号
int port;
// 服务名
String bindName;
// 服务IP地址
String address;
public ServiceHandler(int port, String bindName, String address) {
this.port = port;
this.bindName = bindName;
this.address = address;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
Socket client = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 创建客户端Socket对象
client = new Socket(InetAddress.getByName(address), port);
// 创建字节输出流对象
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
// 发送指令
dos.writeInt(ServiceFlag.SERVICE_GETINVOKERETURN);
// 发送服务名
dos.writeUTF(bindName);
// 发送方法名
dos.writeUTF(method.getName());
// 创建对象输出流对象,用于序列化操作
oos = new ObjectOutputStream(client.getOutputStream());
// 发送方法类型
oos.writeObject(method.getParameterTypes());
// 发送实参
oos.writeObject(args);
// 创建对象输入流对象,用于反序列化操作
ois = new ObjectInputStream(client.getInputStream());
// 读取服务发送过来的数据内容
Object obj = ois.readObject();
// 返回内容
return obj;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
(4)服务标识类
public interface ServiceFlag {
int SERVICE_GETINTERFACE = 1; // 调用接口方法
int SERVICE_GETINVOKERETURN = 2; // 调用带返回值接口方法
}
(5)自定义异常类
public class ServiceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 6482029944969529522L;
public ServiceNotFoundException() {
super("没有该服务!");
}
}
(6)客户端测试类
public class Client {
public static void main(String[] args) throws ServiceNotFoundException {
IBiz biz = (IBiz) Service.lookup("127.0.0.1", "hello");
String info = biz.sayHello("aa");
System.out.println(info);
}
}
注意:由于RPC通信过程中有大量数据需要在客户端和服务端之间进行来回传输,所以需要依赖到序列化和反序列化的相关技术。因此,在RPC里面所有能够被远程调用的方法参数和返回值都要求必须能够被序列化和反序列化的。如果在实际开发中某些数据无法进行序列化和反序列化操作,那么它们就无法应用到RPC场景中。