当前位置 : 首页 » 文章分类 :  开发  »  Java-RMI

Java-RMI

[TOC]


概述

Java RMI(Java Remote Method Invocation),即Java远程方法调用,是Java中一种用于实现远程过程调用的应用程序编程接口。允许运行在一个java虚拟机的对象调用运行在另一个java虚拟机上对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

代理

在客户端为远程对象安装一个代理。代理是位于客户端虚拟机中的一个对象,它对于客户端程序来说,就像是要访问的远程对象一样。客户端调用此代理时,只需进行常规的方法调用。而客户端代理则负责使用网络协议与服务器进行联系。网络模型如下:

存根(stub)

当客户端要调用远程对象的一个方法时,实际上调用的是代理对象上的一个普通方法,我们称此代理对象为存根(stub)。存根位于客户端机器上,而非服务器上。

参数编组

存根会将远程方法所需的参数打包成一组字节,对参数编码的过程就称为参数编组。参数编组的目的是将参数转换成适合在虚拟机之间进行传递的格式,在RMI协议中,对象是使用序列化机制进行编码的。

RMI接口发布和调用流程

  • 1、定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常。
    在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。
  • 2、创建远程接口的实现类,继承UnicastRemoteObject类实现序列化,必须显式定义无参构造方法。
    远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
  • 3、通过LocateRegistry.createRegistry()创建远程对象注册表Registry的实例,被创建的Registry服务将在指定的端口上侦听到来的请求(默认端口是1099)
  • 4、通过Naming.bind()将远程服务实现类绑定到指定的RMI地址上,执行这个方法后,相当于发布了RMI服务。
  • 5、客户端:通过Naming.lookup()在远程对象注册表Registry中查找指定name的对象,并返回远程对象的引用(一个stub),之后可通过stub调用远程对象的方法。

RMI接口发布调用实例

本实例是一个maven多模块项目,简介如下:

  • rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
  • rmi-server项目:服务端项目,发布rmi接口
  • rmi-client项目:客户端项目,调用rmi接口

服务端项目rmi-server

远程接口HelloService

定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常。
HelloService.java:

package com.masikkk.rmi.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

//远程接口,必须继承Remote接口,其中所有需要远程调用的方法都必须抛出RemoteException异常
public interface HelloService extends Remote{
    public String sayHello(String name) throws RemoteException;
    public int sum(int a,int b) throws RemoteException;
}

远程接口实现类HelloServiceImpl

HelloServiceImpl.java:

package com.masikkk.rmi.server;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

//远程接口实现类,必须继承java.rmi.server.UniCastRemoteObject类
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService{
    private static final long serialVersionUID = 4126819767704786465L;

    //如果父类的无参构造函数抛出了异常,则子类的无参构造函数不能省略不写,并且必须抛出父类的异常或父类异常的父类
    public HelloServiceImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello(String name){
        return "Hello, " + name;
    }

    @Override
    public int sum(int a, int b){
        return a+b;
    }    

}

远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根。
同时,因为UnicastRemoteObject类的默认构造方法抛出了RemoteException异常,所以实现类不能缺省无参构造方法,必须显式定义无参构造方法。

接口发布类HelloServiceMain

创建应用类,注册和启动服务端RMI,以被客户端调用。
HelloServiceMain.java:

package com.masikkk.rmi.server;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class HelloServiceMain {
    public static void main(String[] args) {
        try {
            HelloService helloService = new HelloServiceImpl();
            //创建远程对象注册表Registry的实例,被创建的Registry将在指定的端口(默认1099)上侦听到来的请求
            LocateRegistry.createRegistry(8889);
            //将远程服务实现类绑定到指定的RMI地址上,执行这个方法后,相当于发布了RMI服务 
            Naming.bind("rmi://localhost:8889/HelloService", helloService);
            System.out.println("远程对象绑定成功!");
        } catch (MalformedURLException e) {
            System.out.println("发生URL协议异常!");
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            System.out.println("发生重复绑定对象异常!");
            e.printStackTrace();
        } catch (RemoteException e) {
            System.out.println("创建远程对象发生异常!");
            e.printStackTrace();
        } 
    }
}

Run As->Java Application 启动服务端即发布rmi接口。


客户端项目rmi-client

客户端调用类HelloClient

package com.masikkk.rmi.client;

import java.rmi.Naming;
import com.masikkk.rmi.server.HelloService;

public class HelloClient {
    public static void main(String[] args) {
        try {
            //在远程对象注册表Registry中查找指定name的对象,并返回远程对象的引用
            HelloService helloService = (HelloService)Naming.lookup("rmi://localhost:8889/HelloService");
            System.out.println(helloService.sayHello("masikkk.com"));
            System.out.println(helloService.sum(2, 3));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

因为客户端需要有服务端那边提供的接口,才可以访问,所以要将服务端的IHello接口完全拷贝(连同包)到客户端,当然为了方便,你在客户端工程中新建一个完全一样的接口也可以。实际运用中通常是要服务端接口打成jar包来提供的。这里我通过配置maven依赖引入服务端项目rmi-server:

<!-- 自己封装的rmi-server项目 -->
<dependency>
    <groupId>com.masikkk.rmi</groupId>
    <artifactId>rmi-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

参考


RMI与Spring整合

Spring RMI中,主要涉及两个类:org.springframework.remoting.rmi.RmiServiceExporter和org.springframework.remoting.rmi.RmiProxyFactoryBean
服务端使用RmiServiceExporter暴露RMI远程方法,客户端用RmiProxyFactoryBean间接调用远程方法。

RMI与Spring整合实例

本实例是一个maven多模块项目,和上面的原生java RMI实例在同一个父项目中,简介如下:

  • rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
  • rmi-spring-server项目:spring服务端项目,发布rmi接口
  • rmi-spring-client项目:spring客户端项目,调用rmi接口

服务端项目rmi-spring-server

maven依赖

<dependencies>

    <!-- JUnit4 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>

    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

</dependencies>

接口HelloSpringService

HelloSpringService.java:

package com.masikkk.rmi.spring.server;

//普通接口
public interface HelloSpringService{
    public String sayHello(String name);
    public int sum(int a,int b);
}

服务端创建普通接口,无需继承其他接口。

接口实现类HelloSpringServiceImpl

HelloSpringServiceImpl.java:

package com.masikkk.rmi.spring.server;

//接口实现类
public class HelloSpringServiceImpl implements HelloSpringService{
    @Override
    public String sayHello(String name){
        return "Spring:Hello, " + name;
    }

    @Override
    public int sum(int a, int b){
        return a+b;
    }    

}

服务端Spring上下文配置文件

编辑Java Build path,创建src/main/resources文件夹,在其中新建服务端Spring bean配置文件applicationContext-server.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Service实现类 -->
    <bean id="helloSpringServiceImpl" class="com.masikkk.rmi.spring.server.HelloSpringServiceImpl">
    </bean> 

    <!-- 将服务类导出为RMI服务 -->
    <bean id="springRMIServer" class="org.springframework.remoting.rmi.RmiServiceExporter">
        <property name="serviceName" value="rmiHelloSpringService"></property>
        <property name="service" ref="helloSpringServiceImpl"></property>
        <property name="serviceInterface" value="com.masikkk.rmi.spring.server.HelloSpringService"></property>
        <property name="registryPort" value="8899"></property>    
    </bean>

</beans>

用RmiServiceExporter暴露RMI接口时,需要配置的property有:

  • serviceName,发布的服务名称
  • service,接口实现类
  • serviceInterface,接口
  • registryPort,端口

最终的服务地址为:rmi://localhost:{registryPort}/{serviceName}

服务端启动类HelloSpringServiceMain

package com.masikkk.rmi.spring.server;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloSpringServiceMain {
    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("applicationContext-server.xml");
        System.out.println("Spring远程对象绑定成功!");        
    }
}

客户端项目rmi-spring-client

maven依赖

<dependencies>

    <!-- 自己封装的rmi-spring-server -->
    <dependency>
        <groupId>com.masikkk.rmi</groupId>
        <artifactId>rmi-spring-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <!-- JUnit4 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>

    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jms</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
    </dependency>

</dependencies>

客户端Spring上下文配置文件

创建客户端Spring bean配置文件applicationContext-client.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 客户端代理 -->
    <bean id="rmiClient" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
        <property name="serviceInterface" value="com.masikkk.rmi.spring.server.HelloSpringService"></property>
        <property name="serviceUrl" value="rmi://localhost:8899/rmiHelloSpringService"></property>
    </bean>

</beans>

注意属性serviceUrl的值,端口号和serviceName要与RmiServiceExporter中配置一致。

客户端JUnit测试类HelloSpringServiceTest

package com.masikkk.rmi.spring.client;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.masikkk.rmi.spring.server.HelloSpringService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-client.xml"})
public class HelloSpringServiceTest {
    @Autowired
    HelloSpringService helloSpringService; //自动装配rmiClient

    @Test
    public void test(){
        System.out.println(helloSpringService.sayHello("masikkk.spring"));
        System.out.println(helloSpringService.sum(10, 5));
    }
}

参考


GitHub项目源码

本文中所有代码已分享到GitHub,repo地址:https://github.com/masikkk/java-rmi ,是一个多模块maven项目,可导入为maven工程运行。

项目介绍

本项目是一个maven多模块项目,简介如下:

  • rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
  • rmi-server项目:原生java实现rmi的服务端项目,发布rmi接口
  • rmi-client项目:原生java实现rmi的客户端项目,调用rmi接口
  • rmi-spring-server项目:rmi与spring整合的服务端项目,发布rmi接口
  • rmi-spring-client项目:rmi与spring整合的客户端项目,调用rmi接口

运行方法

  • 原生java实现rmi实例:
    • 首先运行rmi-server项目发布接口:Run As->Java Application运行RMI接口发布类HelloServiceMain
    • 然后运行rmi-client项目调用接口:Run As->Java Application运行RMI接口调用类HelloClient
  • rmi与spring整合实例:
    • 首先运行rmi-spring-server项目发布接口:Run As->Java Application运行服务端启动类HelloSpringServiceMain
    • 然后运行rmi-spring-client项目调用接口:Run As->JUnit Test运行客户端JUnit测试类HelloSpringServiceTest

参考博文


相关类和接口

Remote

java.rmi.Remote
public interface Remote

Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。

Remote接口是标记接口,无方法。继承Remote接口的远程接口中的方法必须抛出RemoteException异常

实现类可以实现任意数量的远程接口,并且可以扩展其他远程实现类。RMI 提供一些远程对象实现可以扩展的有用类,这些类便于远程对象创建。这些类是 java.rmi.server.UnicastRemoteObject 和 java.rmi.activation.Activatable。


UnicastRemoteObject

java.rmi.server.UnicastRemoteObject
public class UnicastRemoteObject extends RemoteServer

用于导出带 JRMP 的远程对象和获得与该远程对象通信的 stub。

对于下面的构造方法和静态 exportObject 方法,正在导出的远程对象的 stub 按以下方式获得:

  • 如果使用 UnicastRemoteObject.exportObject(Remote) 方法导出该远程对象,则加载 stub 类(通常使用 rmic 工具从远程对象的类预生成)并按以下方式构造 stub 类的实例。
    • “根类”按以下情形确定:如果远程对象的类直接实现扩展 Remote 的接口,则远程对象的类为根类;否则,根类为直接实现扩展 Remote 接口的远程对象类的最具派生能力的超类。
    • 要加载的 stub 类的名称通过连接带有后缀 “_Stub” 的根类的二进制名称确定。
    • 按使用根类的类加载器的名称加载 stub 类。该 stub 类必须扩展 RemoteStub 并且必须有公共构造方法,该构造方法有一个属于类型 RemoteRef 的参数。
    • 最后,用 RemoteRef 构造 stub 类的实例。
  • 如果无法找到适当的 stub 类,或无法加载 stub 类,或创建 stub 实例时出现问题,则抛出 StubNotFoundException。
  • 对于所有其他导出方式:
    • 如果无法加载远程对象的 stub 类(如上所述)或将系统属性 java.rmi.server.ignoreStubClasses 设置为 “true”(不分大小写),则用以下属性构造 Proxy 实例:
      代理的实例由远程对象类的类加载器定义。
      该代理实现由远程对象类实现的所有远程接口。
      代理的调用处理程序是用 RemoteRef 构造的 RemoteObjectInvocationHandler 实例。
      如果无法创建代理,则抛出 StubNotFoundException。
    • 否则,将远程对象的 stub 类(如上所述)的实例用作 stub。

LocateRegistry

java.rmi.registry.LocateRegistry
public final class LocateRegistry extends Object

LocateRegistry 用于获取特定主机(包括本地主机)上的远程对象注册表的引用,或用于创建一个接受对特定端口调用的远程对象注册表。
注意,getRegistry 调用并不实际生成到远程主机的连接。它只创建对远程注册表的本地引用,即便远程主机上没有正运行的注册表,它也会成功创建一个引用。因此,调用作为此方法的结果返回的远程注册表的后续方法可能会失败。

createRegistry()

public static Registry createRegistry(int port)
创建并导出接受指定 port 请求的本地主机上的 Registry 实例。
导出 Registry 实例与调用静态 UnicastRemoteObject.exportObject 方法一样,都是将传入 Registry 实例和指定的 port 作为参数,只不过导出的 Registry 实例具有已知对象的标识标符(用值 ObjID.REGISTRY_ID 构造的 ObjID 实例)。
参数:port - 注册表在其上接受请求的端口
返回:注册表
抛出: RemoteException - 如果无法导出注册表


Naming

java.rmi.Naming
public final class Naming extends Object

Naming类提供存储和获得“远程对象注册表”上远程对象的引用的方法。Naming 类的每个方法都可将某个名称作为其一个参数,该名称是使用以下形式的 URL 格式(没有 scheme 组件)的 java.lang.String: //host:port/name。一个//host:port/name可以唯一定位一个RMI服务器上的发布了的对象。
其中 host 是注册表所在的主机(远程或本地),port 是注册表接受调用的端口号,name 是未经注册表解释的简单字符串。host 和 port 两者都是可选项。如果省略了 host,则主机默认为本地主机。如果省略了 port,则端口默认为 1099,该端口是 RMI 的注册表 rmiregistry 使用的“著名”端口

为远程对象 绑定 名称是指为远程对象关联或注册一个名称,以后可以使用该名称来查找该远程对象。可以使用 Naming 类的 bind 或 rebind 方法将远程对象与某个名称相关联。

一旦远程对象向本地主机上 RMI 注册表注册(绑定),远程(或本地)主机上的调用方可以通过名称查找远程对象,获得其引用,并在该对象上调用远程方法。注册表可由在一个主机上运行的所有服务器共享,需要时个别服务器进程也可以创建和使用自己的注册表。

实际上,从源码中可以看出,Naming类中的上述方法都是通过Registry类对“远程对象注册表”进行操作的。
注意: Naming类只是在“远程对象注册表”上进行存储和读取操作,该类并不能创建“远程对象注册表”。

bind()

public static void bind(String name, Remote obj)
将指定 name 绑定到远程对象。
参数:

  • name - 使用 URL 格式(不含 scheme 组件)的名称
  • obj - 远程对象的引用(通常是一个 stub)

抛出:

  • AlreadyBoundException - 如果已经绑定了名称
  • MalformedURLException - 如果该名称不是适当格式化的 URL
  • RemoteException - 如果无法联系注册表
  • AccessException - 如果不允许进行此操作(例如,如果起源于非本地主机)

源码:

public static void bind(String name, Remote obj)
    throws AlreadyBoundException,
        java.net.MalformedURLException,
        RemoteException
{
    ParsedNamingURL parsed = parseURL(name);
    Registry registry = getRegistry(parsed);

    if (obj == null)
        throw new NullPointerException("cannot bind to null");

    registry.bind(parsed.name, obj);
}

lookup()

public static Remote lookup(String name)
返回与指定 name 关联的远程对象的引用(一个 stub)。
参数:name - 使用 URL 格式(不含 scheme 组件)的名称
返回:远程对象的引用
抛出:

  • NotBoundException - 如果当前未绑定名称
  • RemoteException - 如果无法联系注册表
  • AccessException - 如果不允许执行此操作
  • MalformedURLException - 如果名称不是适当格式化的 URL

源码:

public static Remote lookup(String name)
    throws NotBoundException,
        java.net.MalformedURLException,
        RemoteException
{
    ParsedNamingURL parsed = parseURL(name);
    Registry registry = getRegistry(parsed);

    if (parsed.name == null)
        return registry;
    return registry.lookup(parsed.name);
}

RMI codebase

codebase问题其实是一个怎样从网络上下载类的问题,我想不只是在Jini和RMI程序开发中要用到。只要需要从网络上下载类,就要涉及到codebase问题。例如applet等。但是因为我对applet程序不是很熟悉,所以我就只谈Jini和RMI,但我想codebase问题应该是通用的。

毫无疑问,对于大多数Jini和RMI开发新手来说,如何使用codebase是比较轻易令人迷惑的一件事。下面我就讲讲codebase要注重的几个方面,也就是容 易出问题的地方。

一、为什么需要codebase

当我们用一个对象作为远程方法调用的参数时,对象是以序列化流来传输到远端,然后在远端重新生成对象。这样就可能在两个Java虚拟机中交换对象了。但是序列化是这种传递对象的一部分。当你序列化对象时,你仅仅是把对象的成员数据转化成字节流,而实际实现该对象的代码却没有。也就是说,传递的只是数据部分,而做为控制逻辑的程序代码部分却没有被传递。这就是RMI初学者轻易误解的地方,我已经序列化对象了,而且对象也传过去了,怎么还说找不到呢。其实,对象数据的确过去了,不过找不到是类定义,这个并不是序列化传过去的,RMI协议是不传递代码的。但是,对于本地没有的类文件的对象,RMI提供了一些机制答应接收对象的一方去取回该对象的类代码。而到什么地方去取,这就需要发送方设置codebase了。

二、什么是codebase

简单说,codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。
codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。

三、怎样设置codebase

在大多数情况下,你可以在命令行上通过属性java.rmi.server.codebase来设置codebase。例如,假如你在机器url上运行web服务器,端口是8080,你所提供下载的类文件在webserver的根目录下。那么运行应用程序的java 命令行:
-Djava.rmi.server.codebase=http://url:8080/
这就是告诉任何接受对象的程序,假如本地没有类文件,可以从这个url下载。

四、类文件应该在什么地方

当接收程序试图从url的webserver上下载代码时,它会把对象的包名转化成目录,到相对于codebase 的该目录下寻找(这点和classpath是一样的)。例如,假如你传递的是类文件yourgroup.project.bean的实例,那么接受方就会到下面的url去下载类文件:
-Djava.rmi.server.codebase=http://url:8080/yourgroup/project/bean.class
一定要保证你把类文件放到webserver根目录下的正确位置,这样这些类文件才能被找到。另一方面,假如你把所有的类文件包装成jar文件,那么设置codebase时就要明确的指出这个jar文件。(这又和 classpath一致了,其实codebase就是网络范围的类路径。)例如你的jar文件是myclasses.jar,那么codebase 如下:
-Djava.rmi.server.codebase=http://url:8080/myclasses.jar
你注重这两种形式的不同。用jar文件后面不用跟‘/’,而用路径的一定用跟‘/’。


上一篇 Hexo博客(06)添加多说评论系统

下一篇 Hexo博客(05)写作定制与插件