Java漫游笔记-14-RMI和WebService

很多时候,我们应用程序所有的组件都在同一台主机的同一个 JVM 中运行。
然而,有的时候,基于一些原因(例如:性能、安全),我们也会把程序的某些部分分别部署在不同的机器上。
这时候,我们就不得不考虑这些分布在不同机器上的组件(或是子系统)应如何通信,来共同实现业务功能。
在 Java 中,组件间要通信,可以基于消息的方式实现,也可以基于远程调用的方式实现。
前面学习过的 TCP/IP 、UDP/IP 编程,就可以实现不同主机间的通信。
但是,这样一来,我们就需要处理网络连接、数据收发 以及 Java 对象的序列化等一系列的问题。想想就头大。
如果能够取得远程对象的引用就好了,因为这样我们就可以直接调用它的方法,就好像大家都在同一个 JVM 中。
这就是 RMI(远程方法调用,Remote Method Invocation)提供给我们的功能。

RMI

基本原理

首先,我们要理解两个概念。

  • 这里说的“远程”,并不仅仅指的是不同的主机之间,在同一台主机的不同虚拟机之间,也属于远程。
  • 如果一个对象的方法能够在不同的 JVM 之间被调用,那么这个对象就可以称为远程对象(Remote Object)。

基于 RMI 的应用,通常由服务端程序和客户端程序组成。
服务端程序负责创建一些远程对象,并使得对这些远程对象的引用能够被访问。
客户端程序则会获取远程对象的引用,并调用这些远程对象所提供的方法。

其基本原理大致如下:

RMI

编程模型

基于 RMI 开发一个分布式应用主要遵循以下几个步骤:

  1. 定义远程接口:也就是指定哪些方法能够被客户端远程调用。
  2. 实现远程接口:编写远程接口具体的实现类。
  3. 编写服务器端代码:创建并暴露远程对象,启动远程对象注册表,监听请求。
  4. 编写客户端代码:通过远程注册表的引用,查询并获取远程对象,并调用其方法。

参考代码

  1. 定义远程接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import java.rmi.Remote;
    import java.rmi.RemoteException;

    // 服务器端对外提供的接口,必须继承自 Remote
    public interface TimeService extends Remote {

    // 方法抛出 RemoteException
    // 远程调用底层涉及网络、IO 操作,因此总是有风险
    public String now() throws RemoteException;

    }
  2. 实现远程接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java.rmi.RemoteException;
    import java.text.SimpleDateFormat;
    import java.util.Date;

    public class TimeServiceImpl implements TimeService {

    @Override
    public String now() throws RemoteException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    return sdf.format(new Date());
    }

    }
  3. 编写服务器端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    import java.rmi.server.UnicastRemoteObject;

    public class RMIServer {

    public static void main(String[] args) throws RemoteException,
    AlreadyBoundException {

    // 远程对象接收调用的端口
    final int REMOTE_PORT = 54321;
    // 远程对象在注册表中绑定的名称
    final String REMOTE_NAME = "TimeService";

    // 注册表接受请求的端口
    final int REGISTRY_PORT = 1099;

    // 创建远程对象
    TimeService timeService = new TimeServiceImpl();
    // 暴露该远程对象,以便能够接收传入的调用
    // 这会通过动态代理,自动生成 stub 类
    // 如果端口设置为 0 ,则会使用匿名端口
    UnicastRemoteObject.exportObject(timeService, REMOTE_PORT);

    // 创建并启动远程对象注册表
    Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);
    // 将远程对象绑定到注册表,使得客户端可以查找到
    registry.bind(REMOTE_NAME, timeService);
    }

    }
  4. 编写客户端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;

    public class RMIClient {

    public static void main(String[] args) throws RemoteException,
    NotBoundException {

    // 在默认注册表端口 1099 获取远程注册表的引用
    Registry registry = LocateRegistry.getRegistry("localhost");

    final String REMOTE_NAME = "TimeService";
    // 根据名称获取远程对象的引用
    // 实际上是动态生成的代理对象(Stub)
    TimeService timeService = (TimeService) registry.lookup(REMOTE_NAME);
    System.out.println(timeService.getClass().getName()); // com.sun.proxy.$Proxy0
    System.out.println(timeService.toString()); // TimeServiceImpl_Stub[...]

    // 调用其方法
    String now = timeService.now();
    System.out.println("现在是北京时间:" + now);
    }

    }

要点

  • 远程对象除了 Remote 接口,也可以实现其它的接口,但其它接口里声明的方法只在本地 JVM 可用,不能被远程调用。
  • 远程接口声明的方法,必须抛出 RemoteException ,原因是远程调用的底层会涉及网络、IO 等操作,因此总是有风险。
  • 远程方法的参数和返回值,只允许是基本数据类型、可序列化的对象。原因也很简单,因为要进行序列化操作。
  • 在 JDK 1.5 之前,对于每个远程接口,我们都需要使用 RMI 编译器(rmic)手动生成 Stub 和 Skeleton 类,并打包到客户端。但是 JDK 1.5 之后,RMI 工具使用了动态代理,会在运行时动态生成代理对象,使用起来更简单。实际上,如果你现在还调用 rmic 的话,会出现以下警告信息:

    警告: 为 JRMP 生成和使用骨架及静态存根已过时。骨架不再必要, 而静态存根已由动态生成的存根取代。建议用户不再使用rmic来生成骨架和静态存根。

  • RMI 注册机制是一个简单的远程对象命名服务,就好像是一个共享的电话本,客户端通过远程对象的名字可以查到远程对象(实际上是 Stub)的引用。
  • RMI 框架采用分布式垃圾收集机制(DGC,Distributed Garbage Collection)来管理远程对象的生命周期。其主要规则为:当一个远程对象没有任何本地变量引用和远程引用时,这个远程对象才可以被垃圾回收器回收。如果远程对象希望 GC 回收前做一些安全退出的操作(例如:释放资源),可以实现 java.rmi.server.Unreferenced 接口,并在 unreferenced()方法中执行相关操作。当 RMI 框架监测到一个远程对象没有任何远程引用时,这个方法。

WebService

除了 RMI ,JDK 1.6 之后,也集成了 WebService 。因此,我们也可以通过这种方式实现远程调用。

基本原理

WebService 基于 XML 进行信息交换,相对 RMI 而言,它具有跨平台、跨语言的优势。

其基本原理大致如下:

WebService

编程模型

基于 WebService 开发应用,主要有以下几个步骤:

  1. 定义远程接口:也就是指定哪些方法能够被客户端远程调用。
  2. 实现远程接口:编写远程接口具体的实现类,并通过 @WebService 注解,配置对外暴露的 WebService 的名称,以及生成的 stub 代码的类名和包名等信息。
  3. 编写服务器端代码:发布 WebService 。
  4. 生成客户端辅助代码:使用 wsimport 工具,解析 WSDL ,并生成辅助代码。
  5. 编写客户端代码:导入辅助代码,并调用其方法。

参考代码

  1. 定义远程接口:

    1
    2
    3
    4
    5
    public interface TimeService {
    // 获取远程服务器的时间
    public String now();

    }
  2. 实现远程接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import java.text.SimpleDateFormat;
    import java.util.Date;

    import javax.jws.WebService;
    import javax.jws.soap.SOAPBinding;

    // 标记为 WebService 的实现类
    // 可以配置生成的 stub 代码的类名和包名等信息
    @WebService(name = "TimeService", serviceName = "RemoteTime", targetNamespace = "http://stub.webservice.codinglike.com")
    @SOAPBinding(style = SOAPBinding.Style.RPC)
    public class TimeServiceImpl implements TimeService {

    @Override
    public String now() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    return sdf.format(new Date());
    }

    }
  3. 编写服务器端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import javax.xml.ws.Endpoint;

    public class WebServiceServer {

    public static void main(String[] args) {
    // 发布 WebService
    // 客户端可以通过 http://localhost:54321/timeService?wsdl 访问 WSDL
    Endpoint.publish("http://localhost:54321/timeService",
    new TimeServiceImpl());
    }

    }
  4. 生成客户端辅助代码:

    1
    wsimport -keep http://localhost:54321/timeService?wsdl
  5. 编写客户端代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import com.codinglike.webservice.stub.RemoteTime;
    import com.codinglike.webservice.stub.TimeService;

    public class WebServiceClient {

    public static void main(String[] args) {
    // 先使用 wsimport -keep http://localhost:54321/timeService?wsdl 生成辅助代码
    // 再基于辅助代码实现业务功能
    RemoteTime stub = new RemoteTime();
    TimeService timeService = stub.getTimeServicePort();

    String now = timeService.now();
    System.out.println("现在是北京时间:" + now);
    }

    }

要点

  • WebService 使用 SOAP(简单对象访问协议,Simple Object Access Protocol)来传输的数据。
  • WSDL (网络服务描述语言,Web Services Description Language)用来描述如何访问具体的接口。

总结

以上内容介绍了基于 Java 自身技术实现远程通信的两种方式: RMI 和 WebService 。它们的共同点在于,都封装底层网络通信的细节,使用起来比较简单。但是,RMI 是 Java 独有的,而 WebService 是跨语言的。
除了这两种方式,还有一些第三方提供的技术和框架可以使用,例如:Spring Remoting 、JBoss Remoting 等。
但是,从目前的趋势来看,现在的应用架构更倾向于,让客户端直接通过 HTTP 与服务器进行通信。在 API 的设计风格上,RESTful 的架构越来越流行。而就传输的数据格式而言,相对于 XML 也更喜欢使用 JSON 。