RMI简单示例

一.同一台主机上

  要定义和使用一套基于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)创建或获取RegistryLocateRegistry类相关方法:

  // 在本地创建一个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)绑定RegistryRegistry相关方法):

  // 在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

你可能感兴趣的:(RMI简单示例)