RMI介绍
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使用一般要遵循如下规则:
远程接口必须为public属性(不能是“包访问”),否则一旦Client试图装载一个实现了远程接口的远程对象,就会得到一个错误;
远程接口必须扩展(extends)接口java.rmi.Remote;
除了应用程序本身可能抛出的Exception外,远程接口中的每个方法还必须在自己的throws从句中声明抛出java.rmi.RemoteException(否则运行Server时会抛出java.rmi.server.ExportException);
作为参数或返回值传递的一个远程对象必须声明为远程接口,不可声明为实现类。
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命令查看编译后的源码
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文件。
JDK1.8 rmic生成的文件反编译如下
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;
}
}
使用rmic v1.1生成的类
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();
}
}
使用socket仿写rmi实现
上述示例如果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实例,但是这种实现安全有个大问题,竟让将整个实现类在网络上传输。
欢迎工作一到五年的Java工程师朋友们加入Java架构开发:855801563
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代