一.同一台主机上
要定义和使用一套基于RMI框架工作的系统,至少需要做一下几个工作:
1)定义RMI Remote接口;
2)实现这个RMI Remote接口;
3)生成Stub(桩)和 Skeleton(骨架)。这一步的具体操作视不同的JDK版本而有所不同(例如JDK1.5后,Skeleton不需要手动);“RMI注册表”的工作方式也会影响“Stub是否需要命令行生成”这个问题。
4)向“RMI注册表”注册在第2步我们实现的RMI Remote接口。
5)创建一个Remote客户端,通过java“命名服务”在“RMI注册表”所在的IP:PORT寻找注册好的RMI服务。
6)Remote客户端向调用存在于本地JVM中对象那样,调用存在于远程JVM上的RMI接口。
1.发布RMI服务
发布一个RMI服务,只需做三件事情:
(1)定义一个RMI接口;
(2)编写RMI接口的实现类;
(3)通过JNDI发布RMI服务。
1.1 定义一个 RMI 接口(服务端)
RMI 接口实际上还是一个普通的 Java 接口,只是 RMI 接口必须继承 java.rmi.Remote,此外,每个 RMI 接口的方法必须声明抛出一个 java.rmi.RemoteException 异常,就像下面这样代码:
import java.rmi.Remote; import java.rmi.RemoteException; /** * RMI服务接口 */ public interface IDataService extends Remote{ String sayHello() throws RemoteException; }
继承了Remote 接口,实际上是让JVM得知该接口是需要用于远程调用的,抛出了RemoteException是为了让调用RMI服务的程序捕获这个异常。毕竟远程调用过程中,什么奇怪的事情都会发生(比如:断网)。需要说明的是RemoteException 是一个“受检异常”,在调用的时候必须使用try...catch...自行处理。
1.2 编写RMI接口的实现类(服务端)
实现以上的HelloService是一件非常简单的事情,但需要注意的是,必须让实现类继承java.rmi.server.UnicastRemoteObject类(作用:将该接口公开,以表示该接口是用于远程调用的接口)。此外,必须提供一个构造器,并且构造器必须抛出java.rmi.RemoteException异常。我们既然使用JVM提供的这套RMI框架,那么就必须按照这个要求来实现,否则是无法成功发布 RMI 服务的,一句话:我们得按规矩出牌!
接口公开方式如下:
方式一:继承java.rmi.server.UnicastRemoteObject类
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; /** * RMI服务实现 */ public class DataServiceImpl1 extends UnicastRemoteObject implements IDataService{ private static final long serialVersionUID = 1L; protected DataServiceImpl1() throws RemoteException { super(); // 因为 UnicastRemoteObject 构造器抛出 RemoteException // 所以此处只能声明一个构造器并抛出对应异常 } @Override public String sayHello() throws RemoteException { return "Hello World"; } }
方式二:不继承UnicastRemoteObject类,则需要使用 UnicastRemoteObject类的静态方法exportObject(Remote obj, int port)将对象设置为公开接口(注意:其中如果端口设为 0 的话,则表示任何合适的端口都可用来监听客户连接)
1)在实现类的构造器中实现接口公开
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; /** * RMI服务实现 */ public class DataServiceImpl1 implements IDataService{ private static final long serialVersionUID = 1L; protected DataServiceImpl1() throws RemoteException { UnicastRemoteObject.exportObject(this, 0); } @Override public String sayHello() throws RemoteException { return "Hello World"; } }
2)在服务器端实现
public static void main(String[] args) { // 省略其它代码 IDataService stub = (IDataService) UnicastRemoteObject.exportObject(new DataServiceImpl1(), 1099); // 省略其它代码 }
为了满足 RMI 框架的要求,确实需要做很多额外的工作(继承了UnicastRemoteObject类,抛出了RemoteException 异常)。
1.3 通过JNDI发布RMI服务(服务端)
发布RMI服务,我们需要告诉JNDI三个基本信息:
1)域名或IP地址(host)
2)端口号(port)
3)服务名(service),
它们构成了RMI协议的URL(或称为“RMI 地址”):rmi://
如果我们是在本地发布 RMI 服务,那么host就是“localhost”。此外,RMI默认的port是“1099”,我们也可以自行设置port的值(只要不与其它端口冲突即可)。service实际上是一个基于同一host与port下唯一的服务名,建议使用 Java 完全类名来表示,这样比较容易保证 RMI 地址的唯一性。
注册服务共有三种方式:
1)LocateRegistry 类的对象的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的
2)利用命名服务 java.rmi.Naming 类的 rebind() 和 lookup() 来实现绑定注册和查找远程对象的
3)利用JNDI(Java Naming and Directory Interface,Java命名和目录接口) java.naming.InitialContext 类来 rebind() 和 lookup() 来实现绑定注册和查找远程对象的
其中第二种方式实际是对第一种方式的简单封装,在内部仍是调用Registry类的bind方法
RMI 地址为:rmi://localhost:1099/com.cn.suning.mq.rmi.IDataService。
只需简单提供一个main()方法就能发布RMI服务,如下:
import java.rmi.Naming; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Context; import javax.naming.InitialContext; public class ServerTest { public static void main(String[] args) { String name = "com.cn.suning.mq.rmi.IDataService"; String url = "rmi://localhost:1099/com.cn.suning.mq.rmi.IDataService"; //!!! 新添加了securityManageer !!! if(System.getSecurityManager() == null){ System.setSecurityManager(new SecurityManager()); } try { //创建服务 IDataService server1 = new DataServiceImpl1(); //创建本机1099端口上的RMI注册表 Registry registry1 = LocateRegistry.createRegistry(1099); //将服务绑定到注册表 /*******************注册方法一*******************************/ registry1.bind(name, server1); // /*******************注册方法二*******************************/ // Naming.bind(url, server1); // /*******************注册方法三*******************************/ // Context namingContext = new InitialContext(); // namingContext.bind("rmi:" + name, server1); // 此方式 name 需要以 rmi: 开头 } catch (Exception e) { System.out.println("============启动失败=============="); e.printStackTrace(); } System.out.println("============启动成功=============="); } }
需要注意的是,我们通过 LocateRegistry.createRegistry()方法在JNDI中创建一个注册表,只需提供一个RMI端口号即可。此外,通过Naming.rebind()方法绑定RMI地址与RMI服务实现类,这里使用了rebind()方法,它相当于先后调用Naming的unbind()与bind()方法。
1.4针对服务端注意点
关于前面几个类的代码还要进行一些细节的说明:
1)由于我们使用LocateRegistry创建了一个“本地RMI注册表”,所以不需要使用rmic命令生成Stub了(注意是“不需要手工生成”而不是“不需要”了),这是因为RMI Sever真实服务的JVM和RMI注册表的JVM是同一个JVM。
2)那么RMI Sever真实服务的JVM和RMI注册表的JVM可以是两个不同的JVM吗?当然可以。而且这才是RMI框架灵活性、健壮性的提现。
3)请注意RemoteUnicastServiceImpl的定义,它继承了UnicastRemoteObject。一般来说RMI Server的实现可以继承两种父类:UnicastRemoteObject和Activatable。
4)前者的意义是,RMI Server真实的服务提供者将工作在“本地JVM”上;后者的意义是,RMI Server真实的服务提供者,不是在“本地JVM”上运行,而是可以通过“RMI Remote Server 激活”技术,被序列化到“远程JVM”(即远程RMI注册表所在的JVM上),并适时被“远程JVM”加载运行。
5)“Naming.rebind”和“Naming.bind”的区别:前置是指“重绑定”,如果“重绑定”时“RMI注册表”已经有了这个服务name的存在,则之前所绑定的Remote Object将会被替换;而后者在执行时如果“绑定”时“RMI注册表”已经有这个服务name的存在,则系统会抛出错误。所以除非您有特别的业务要求,那么建议使用rebind方法进行Remote Object绑定。
6)registry.rebind和Naming.rebind绑定的区别:前者是使用RMI注册表绑定,所以不需要写完整的RMI URL了;后者是通过java的名称服务进行绑定,由于名称服务不止为RMI框架提供查询服务,所以在绑定是要书写完整的RMI URL。
如:
String name = "com.cn.rmi.IDataService"; String url = "rmi://localhost:1099/com.cn.rmi.IDataService"; (1)LocateRegistry.createRegistry(1099).bind(name, Object); (2)Naming.bind(url, server1);
运行这个main()方法,RMI服务就会自动发布,剩下要做的就是写一个RMI客户端来调用已发布的RMI服务。
2、调用RMI服务
同样也使用一个main()方法来调用RMI服务,相比发布而言,调用会更加简单,只需要知道两个东西:
1)RMI请求路径;
2)RMI接口(一定不需要RMI实现类,否则就是本地调用了)。
调用发布的RMI服务,如下:
import java.rmi.Naming; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import javax.naming.Context; import javax.naming.InitialContext; /** * 在同一台服务器上 * @author 18045075 */ public class ClientRMITest { public static void main(String[] args) throws Exception { String name = "com.cn.suning.mq.rmi.IDataService"; String url = "rmi://localhost:1099/com.cn.suning.mq.rmi.IDataService"; /***************** 以下为查找服务方法一 ************/ //获取注册表信息 // Registry registry = LocateRegistry.getRegistry(url, 1099); //在RMI服务注册表中查找名称为IDataService的对象 // IDataService service = (IDataService) registry.lookup(name); /***************** 以下为查找服务方法二 ************/ // IDataService service = (IDataService) Naming.lookup(url); /**************** 以下为查找服务方法三 ************/ Context namingContext = new InitialContext(); IDataService service = (IDataService) namingContext.lookup("rmi:" + name); // 调用服务 System.out.println(service.sayHello()); } }
因为使用的是“本地RMI注册表”,所以不需要做任何的配置、脚本指定等工作(包括不需要专门设置JRE权限、不需要专门指定classpath、不需要专门生成Stub和Skeleton)。
当我们运行以上main()方法,在控制台中看到“Hello World”输出,就表明RMI调用成功。
注意:
(1)要先运行服务端,再运行客户端。
(2)看似service.sayHello()对象和普通的对象没有区别,但实际上service.sayHello()对象的具体方法实现却不在本地的JVM中,而是在某个远程的JVM中(这个远程的JVM可以是RMI客户端同属于一台物理机,也可以属于不同的物理机)。
3、常用方法
(1)创建或获取Registry(LocateRegistry类相关方法:)
// 在本地创建一个Registry并指定端口
Registry registry = LocateRegistry.createRegistry(int port);
// 获得一个本地Registry对象并使用默认端口(1099)
Registry registry = LocateRegistry.getRegistry();
// 获得一个本地Registry对象并指定端口
Registry registry = LocateRegistry.getRegistry(int port);
// 获得一个指定服务器的Registry对象并使用默认端口(1099)
Registry registry = LocateRegistry.getRegistry(String host);
// 获得一个指定服务器的Registry对象并指定端口
Registry registry = LocateRegistry.getRegistry(String host, int port);
(2)绑定Registry(Registry相关方法):
// 在registry上将name与obj绑定
registry.bind(String name, Remote obj);
// 在registry上将name与obj重新绑定(替换原name的绑定)
registry.rebind(String name, Remote obj);
// 在registry上将name解绑(删除name的绑定)
registry.unbind(String name);
// 在registry上查找指定name并返回相应的obj对象
registry.lookup(String name);
(3)Naming
Naming.bind(“rmi://localhost:1099/compute”, stub);
通过Naming类方法,在指定name时,需要加上host地址。
Naming类相关方法:
// 将name与obj绑定
Naming.bind(String name, Remote obj);
// 将name与obj重新绑定(替换原name的绑定)
Naming.rebind(String name, Remote obj);
// 将name解绑(删除name的绑定)
Naming.unbind(String name);
// 查找指定name并返回相应的obj对象
Naming.lookup(String name);
二、不同主机上
参考地址:
https://blog.csdn.net/yinwenjie/article/details/49120813
https://segmentfault.com/a/1190000004494341
三、spring实现方式
https://www.cnblogs.com/lojun/articles/9665085.html