RMI(Remote Method Invocation,远程方法调用)是用Java在JDK1.1中实现的。经过多个JDK版本迭代,目前RMI的实现方式跟最开始底层实现还是有很大差别的。远程方法调用允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
RMI使用JRMP(Java Remote Messaging Protocol)进行通信。JRMP是专为Java的远程对象制定的协议。因此,Java RMI具有Java的”Write Once,Run Anywhere”的优点,是分布式应用系统的百分之百纯Java解决方案。RMI主要使用在跨进程应用中或者分布式应用,因此一般开发过程中很少接触到RMI编程。
RMI底层是通过Socket进行的通信,但是使用RMI的好处在于我们不需要面向网络编程,摒除了复杂的网络解析那一层,仍然是采用面向对象的方式。由于RMI会创建一个类似客户辅助对象和服务辅助对象,客户调用客户辅助对象上的方法,仿佛客户辅助对象就是真正的服务。客户辅助对象再为我们转发这些请求。在服务端,服务辅助对象负责从客户辅助对象接收请求,将调用的信息解包,然后调用真正的服务方法。这种操作方式中,同服务对象交互的也是服务辅助对象。RMI辅助生成客户辅助对象和服务辅助对象。
由于在使用RMI编程的时候,服务端和客户端不是同一个工程目录,因为他们之间调用不是类与类之间的直接调用,在上面也介绍了实际上RMI底层是通过Socket传输数据的,因此RMI中所有涉及到远程方法调用的变量都必须是可序列化的。
RMI一般将客户辅助对象称为Stub(桩),服务辅助对象称为Skeleton(骨架)。现在新版的Java已经不需要显示的Stub和Skeleton对象了,但是尽管如此,还是由一些东西负责Stub和Skeleton行为的。
RMI使用一般要遵循如下规则:
首先定义一个实体类,实现Serializable接口。
public class User implements Serializable { private static final long serialVersionUID = 1L; public String name; public int age; public User(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "User [name=" + name + ", age=" + age + "]"; } }
定义一个远程接口以及该接口实现类。
public interface RemoteUser extends Remote { User getUser() throws RemoteException; int getAge() throws RemoteException; } public class RemoteUserImpl implements RemoteUser { @Override public User getUser() throws RemoteException { return new User("admin",20); } @Override public int getAge() throws RemoteException { return 20; } }
创建导出远程对象,注册远程对象,向客户端提供远程对象服务。
public static void testUser(){ try { RemoteUser user = new RemoteUserImpl(); RemoteUser stub = (RemoteUser) UnicastRemoteObject.exportObject(user, 9999); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("user", stub); System.out.println("绑定成功!"); } catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } }
在本示例中UnicastRemoteObject是使用exportObject()方法来处理远程对象的,实际上也可以在远程接口的实现类直接继承自UnicastRemoteObject。直接使用继承的方式是比较简单的方式创建远程对象,但是需要提供一个无参的构造方法,并抛出RemoteException异常。
客户端将服务端创建的实体类以及远程接口需要Copy一份过来,包名不可以更改。
public static void testUser() { try { Registry registry = LocateRegistry.getRegistry("localhost"); RemoteUser remoteUser = (RemoteUser) registry.lookup("user"); User user = remoteUser.getUser(); int age=remoteUser.getAge(); System.out.println(user); //User [name=admin, age=20] System.out.println(age); //20 } catch (RemoteException e) { e.printStackTrace(); } catch (NotBoundException e) { e.printStackTrace(); } }
rmic是Java中RMI的编译命令。rmic编译的时候跟javac不一样,类名一定要写全,比如:rmic -classpath D:/workspace/bin com.sunny.server.RemoteUserImpl。而且文件名后面不能有.class。还有这个class一定要放在classpath下。
在JDK1.8中使用rmic命令可以看到只生成了RemoteUserImpl_Stub.class,但是rmic命令可以指定编译时使用的rmic版本号,当使用v1.1版本时仍然可以看到RemoteUserImpl_Skel.class文件。
public final class RemoteUserImpl_Stub extends RemoteStub implements RemoteUser{ private static final long serialVersionUID = 2L; private static Method $method_getAge_0; private static Method $method_getUser_1; static { $method_getAge_0 = (com.sunny.server.RemoteUser.class).getMethod("getAge", new Class[0]); $method_getUser_1 = (com.sunny.server.RemoteUser.class).getMethod("getUser", new Class[0]); } static Class class$(String s){ ... return Class.forName(s); } public RemoteUserImpl_Stub(RemoteRef remoteref){ super(remoteref); } public int getAge() throws RemoteException{ Object obj = super.ref.invoke(this, $method_getAge_0, null, 0x6d7a3d73b0a83838L); return ((Integer)obj).intValue(); } public User getUser() throws RemoteException{ Object obj = super.ref.invoke(this, $method_getUser_1, null, 0x57ab22fce7623f5eL); return (User)obj; } }
public final class RemoteUserImpl_Stub extends RemoteStub implements RemoteUser{ private static final Operation operations[] = { new Operation("int getAge()"), new Operation("com.sunny.server.bean.User getUser()") }; private static final long interfaceHash = 0xeb847cb50cd67648L; public RemoteUserImpl_Stub(){} public RemoteUserImpl_Stub(RemoteRef remoteref){ super(remoteref); } public int getAge()throws RemoteException{ RemoteCall remotecall = super.ref.newCall(this, operations, 0, 0xeb847cb50cd67648L); super.ref.invoke(remotecall); ObjectInput objectinput = remotecall.getInputStream(); int i = objectinput.readInt(); super.ref.done(remotecall); ... return i; } public User getUser() throws RemoteException{ ... User user = (User)objectinput.readObject(); return user; } } public final class RemoteUserImpl_Skel implements Skeleton{ private static final Operation operations[] = { new Operation("int getAge()"), new Operation("com.sunny.server.bean.User getUser()") }; ... public void dispatch(Remote remote, RemoteCall remotecall, int opnum, long hash)throws Exception{ RemoteUserImpl remoteuserimpl = (RemoteUserImpl)remote; switch (opnum) { case 0: // '/0' remotecall.releaseInputStream(); int j = remoteuserimpl.getAge(); ... objectoutput.writeInt(j); break; case 1: // '/001' remotecall.releaseInputStream(); com.sunny.server.bean.User user = remoteuserimpl.getUser(); ObjectOutput objectoutput1 = remotecall.getResultStream(true); objectoutput1.writeObject(user); ... break; ... } } public Operation[] getOperations(){ return (Operation[])operations.clone(); } }
上述示例如果Server端直接运行两次就会抛出类似如下异常:
Caused by: java.net.BindException: Address already in use: JVM_Bind at java.net.DualStackPlainSocketImpl.bind0(Native Method) at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106) at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387) at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190) at java.net.ServerSocket.bind(ServerSocket.java:375) at java.net.ServerSocket.<init>(ServerSocket.java:237) at java.net.ServerSocket.<init>(ServerSocket.java:128) at sun.rmi.transport.proxy.RMIDirectSocketFactory.createServerSocket(RMIDirectSocketFactory.java:45) at sun.rmi.transport.proxy.RMIMasterSocketFactory.createServerSocket(RMIMasterSocketFactory.java:345) at sun.rmi.transport.tcp.TCPEndpoint.newServerSocket(TCPEndpoint.java:666) at sun.rmi.transport.tcp.TCPTransport.listen(TCPTransport.java:330)
这个异常刚好跟我们使用Socket编程时,端口被占用的异常一样。所以我们也将服务端设置为一个ServerSocket,实体类和远程接口定义及实现跟上面示例一样,唯一不同的地方,我们这里抛出的异常是一个IOException,RemoteUser_Skel定义如下:
public class RemoteUser_Skel { private ServerSocket server; private RemoteUser remoteUser; public RemoteUser_Skel() { try { server = new ServerSocket(10086); remoteUser = new RemoteUserImpl(); } catch (IOException e) { e.printStackTrace(); } } public void dispatch() { System.out.println("server start"); ObjectOutputStream oos = null; try { while (true) { Socket socket = server.accept(); System.out.println("socket:"+socket); if (socket != null) { ObjectInputStream ois = new ObjectInputStream( socket.getInputStream()); int index = ois.readInt(); System.out.println("server:" + index); if (index == 1001) { oos = new ObjectOutputStream(socket.getOutputStream()); User user = remoteUser.getUser(); oos.writeObject(user); oos.flush(); } else if (index == 1002) { oos = new ObjectOutputStream(socket.getOutputStream()); int age = remoteUser.getAge(); oos.writeInt(age); oos.flush(); } } } } catch (IOException e) { e.printStackTrace(); } } }
客户端的代码这里就不贴出来了,可以直接 下载源码 查看详细示例。
其实这里的仿写可以更暴力一些,只要我们将服务端的代码的远程接口实现类直接序列化,通过序列化将RemoteUserImpl直接通过Socket传输到客户端,然后客户端可以直接反序列化出RemoteUserImpl实例,但是这种实现安全有个大问题,竟让将整个实现类在网络上传输。
本文就是简单介绍一下RMI的使用,这种模式的跨进程应用很类似Android中Binder机制,再加上设计模式中将RMI的实现称为远程代理,所以这次抽时间整理了一下相关知识点。
java RMI原理详解
Java RMI原理与使用
RMI原理揭秘之远程对象
《Head First设计模式》