Java 远程方法调用(RMI)

RMI简介

第 1 页(共23 页)我们将从 Java 远程方法调用(RMI)开始讨论,Java 1.1 中引入了这种技术。

RMI 的用途是使分布在不同虚拟机中的对象的外表和行为都象本地对象一样。调用远程对象的虚拟机有时称为客户机。类似地,我们将包含远程对象的虚拟机称为服务器。

获取远程对象的引用和获取本地对象的引用有点不同,但一旦获得了引用,就可以象调用本地对象一样调用远程对象,如下面的代码片段所示。RMI 基础结构将自动截取请求,找到远程对象,并远程地分派请求。

这种位置透明性甚至包括垃圾收集。也就是说,客户机不必特地释放远程对象,RMI 基础结构和远程虚拟机为您处理垃圾收集。

RMI体系结构

第 2 页(共23 页)为了实现位置透明性,RMI 引入了两种特殊类型的对象:存根(stub)和框架(skeleton)。

存根是代表远程对象的客户机端对象。存根具有和远程对象相同的接口或方法列表,但当客户机调用存根方法时,存根通过 RMI 基础结构将请求转发到远程对象,实际上由远程对象执行请求。

在服务器端,框架对象处理"远方"的所有细节,因此实际的远程对象不必担心这些细节。也就是说,您完全可以象编码本地对象一样来编码远程对象。框架将远程对象从 RMI 基础结构分离开来。在远程方法请求期间,RMI 基础结构自动调用框架对象,因此它可以发挥自己的作用。

关于这种设置的最大的好处是,您不必亲自为存根和框架编写代码。JDK 包含工具 rmic,它会为您创建存根和框架的类文件。

远程异常

第 3 页(共23 页)实际上,RMI 使用 TCP/IP 套接字来传达远程方法请求。尽管套接字是相当可靠的传输,但还是有许多事情可能出错。例如,假设服务器计算机在方法请求期间崩溃了。或者,假设客户机和服务器计算机是通过因特网连接的,而客户机掉线了。

关键在于,使用远程对象时比使用本地对象时有更多可能出错的机会。因此客户机程序能够完美地从错误中恢复就很重要了。

因此要让客户机知道此类错误,每个将要被远程调用的方法都必须抛出 RemoteException,java.rmi 包中定义了该异常。

服务器开发步骤概述

第 4 页(共23 页)让我们研究一下编写对象服务器所涉及的步骤。我们将花一些时间研究客户机端。

您需要做的第一件事情是定义用于远程对象的接口。这个接口定义了客户机能够远程地调用的方法。远程接口和本地接口的主要差异在于,远程方法必须能抛出上一页描述的 RemoteException。

接下来,编写一个实现该接口的类。

然后,编写在服务器上运行的主程序。这个程序必须实例化一个或多个服务器对象,然后,通常将远程对象注册到 RMI 名称注册表,这样客户机就能够找到对象。

最后,生成存根和框架的代码。JDK 提供了 rmic 工具,它读取远程对象的类文件并为存根和框架创建类文件。

编写远程接口

第 5 页(共23 页)下面的代码显示了一个简单远程接口的接口定义。实现这个接口的对象提供三个方法:一个方法返回字符串、一个方法接受字符串作为参数、而另一个方法不接受参数也不返回任何结果。正如先前提到的,这些方法必须能抛出 RemoteException,如果客户机和服务器之间的通信出错,则客户机将捕获此异常。

注:该接口本身继承了 java.rmi 包中定义的 Remote 接口。Remote 接口本身没有定义方法,但通过继承它,我们说明该接口可以被远程地调用。

这里是编写远程接口的代码:

import java.rmi.*;
public interface Meeting extends Remote
{
public String getDate ()
throws RemoteException;
public void setDate ( String date )
throws RemoteException;
public void scheduleIt()
throws RemoteException;
}

实现远程接口

第 6 页(共23 页)现在,让我们研究一个实现远程 Meeting 接口的类。通常,MeetingServer 继承 UnicastRemoteObject 类,UnicastRemoteObject 类提供远程对象所需的基本行为。术语"单播(unicast)"是指每个客户机存根引用单个远程对象的现象。以后,RMI 可能会允许"多播(multicasting)",即一个存根可以引用几个对等的远程对象。使用多播,RMI 基础结构可以均衡一组远程对象之间的负载。

下面的代码样本显示了两个方法的实现:Meeting 接口中定义的 getDate 方法和一个无参数构造器。请注意:这两者都抛出 RemoteException;所有由客户机远程调用的方法和构造器都需要抛出这个异常。在本示例中,构造器无实际工作可做,但我们还是需要定义它,这样它能够抛出远程异常。

但是 getDate 方法很有趣。它向调用者返回一个字符串的引用。虽然这看起来可能很简单,但是,在这里 RMI 基础结构和框架以及存根实际上有很多工作要做。它们必须协同工作,以便将字符串的一个副本传回客户机,然后,在客户机的虚拟机中作为对象重新创建。

这里是实现远程接口的代码:

import java.rmi.*;
import java.rmi.server.*;
public class MeetingServer
extends UnicastRemoteObject
implements Meeting
{
private String ivDate = new String ( "1/1/2000" );
public MeetingServer()
throws RemoteException
{
}
public String getDate()
throws RemoteException
{
return ivDate;
}
...
}

编写RMI服务器概述

第 7 页(共23 页)除了实现接口之外,我们还需要编写服务器的主程序。目前 RMI 不支持作为 applet 的服务器程序,所以主程序必需是独立的 Java 应用程序。您要么为主程序编码一个单独的 Java 类,要么象我们在这里所做的一样,只在实现类中编码一个 main 方法。

还请注意,我们编码的 main 函数可以向命令行抛出任何与 RMI 相关的异常。对于象本文中的小样本程序,这样做没有问题,但在实际程序中,您可能要将执行的步骤包括到独立的 try-catch 块中,从而能够更好地执行错误处理。

这里是编写 RMI 服务器的代码结构:

import java.rmi.*;
import java.rmi.server.*;
public class MeetingServer
extends UnicastRemoteObject
implements Meeting
{
...
public static void main (String [] args ) throws
RemoteException, java.net.MalformedURLException,
RMISecurityException
{
// 1. Set Security manager
// 2. Create an object instance
// 3. Register object into the name space
}
}

*** 本图将被删除。留在这里仅供对照/检查用 ***

服务器 main 通常要做的步骤是:

安装安全性管理器类,它允许服务器程序从其它机器接收存根类

创建服务器对象的实例

将服务器对象注册到 RMI 命名注册表以便客户机程序能找到该服务器对象现在,让我们进一步研究这些步骤。

设置安全性管理器

第 8 页(共23 页)第一步是安装 RMI 安全性管理器。尽管这不是严格必须的,但它确实允许服务器虚拟机下载类文件。例如,假设客户机调用服务器中的方法,该方法接受对应用程序定义的对象类型(例如 BankAccount)的引用。通过设置安全性管理器,我们允许 RMI 运行时动态地将 BankAccount 类文件复制到服务器,从而简化了服务器上的配置。

让 RMI 动态地下载这些类的弊端是有安全性风险。也就是说,实质上我们是在让服务器执行来自另一台机器的代码。虽然我们希望这些类文件不会危及服务器,但如果希望避免这样的风险,则您的 RMI 服务器不应该安装安全性管理器。然后,您必须确保将所有类文件安装在本地服务器的类路径中。

这里是用于设置安全性管理器的代码:

import java.rmi.*;
import java.rmi.server.*;
public static void main (String [] args ) throws
RemoteException, java.net.MalformedURLException,
RMISecurityException
{
System.setSecurityManager (new RMISecuritymanager() );
MeetingServer ms = new MeetingServer();
Naming.rebind (
"rmi://myhost.com/Meeting", ms );
}

*** 本图将被删除。留在这里仅供对照/检查用 ***

注:传递对象参数类型是涉及许多方面的主题,因为有两种方法实现它。一种是在通信线路上仅传递引用;另一种是将对象序列化并在远程创建新对象。在本教程中,我们不会更深入地讨论这些问题,但您应该阅读 JDK 中的 RMI 文档以获取更多详细信息。

命名远程对象

第 9 页(共23 页)服务器的下一步工作是创建服务器对象的初始实例,然后将对象的名称写到 RMI 命名注册表。RMI 命名注册表允许您将 URL 名称分配给对象以便客户机查找它们。要注册名称,需调用静态 rebind 方法,它是在 Naming 类上定义的。这个方法接受对象的 URL 名称以及对象引用。

名称字符串是很有趣的部分。它包含 rmi:// 前缀、运行 RMI 对象的服务器的计算机主机名和对象本身的名称,这个名称正是您所想要的。注:您可以调用由 java.net.InetAddress 类定义的 getLocalHost 方法,而不必象我们在这里所做的一样硬编码主机名。

这里是命名远程对象的代码:

import java.rmi.*;
import java.rmi.server.*;
public static void main (String [] args ) throws
RemoteException, java.net.MalformedURLException,
RMISecurityException
{
System.setSecurityManager (
new RMISecuritymanager() );
MeetingServer ms = new MeetingServer();
Naming.rebind ("rmi://myhost.com/Meeting", ms );

生成存根和框架

第 10 页(共23 页)编写并编译了服务器实现之后,就准备创建存根和框架类。那很容易:只要运行 JDK 的 rmic 命令,指定实现类文件名(不带扩展名)。rmic 工具将为每个类文件创建一个存根和一个框架。然后,您需要正确地部署这些文件,在讨论完编写客户机端代码之后,我们将讨论部署问题。

客户机开发概述

第 11 页(共23 页)现在,让我们研究客户机端。首先,您必须确定是想编写客户机独立应用程序还是客户机 applet。应用程序的设置简单些,但 applet 更容易部署,因为 Java RMI 基础结构能够将它们下载到客户机机器。我们将讨论如何实现这两者。

在客户机中,代码需要首先使用 RMI 注册表来查找远程对象。一旦这样做了之后,客户机就可以调用由远程接口定义的方法。

应用程序vs. applet

第 12 页(共23 页)让我们来简略看一下 Java 应用程序和 applet 之间的差异。如果编写应用程序,必须定义一个 main 入口点,可以从中执行 RMI 启动代码。然后,您必须在客户机机器上安装应用程序的类文件。如果有多台客户机计算机,则您必须在每台机器上手工安装这个应用程序的类文件。并且,正如我们过一会儿将要看到的,您还要在客户机应用程序计算机上安装一些与 RMI 相关的服务器文件。

相反,如果您决定编写一个 applet,则不需要 main 入口点。applet 重写了由浏览器调用的 init 方法。可以在 init 中编写与您在应用程序的 main 中编写的同类代码。applet 的主要优点是,除了浏览器,您不必在客户机计算机上预安装任何东西 ― Java RMI 基础结构将自动下载所有必要的类文件。但是,请注意,您必须编写一个浏览器能够装入的 HTML 文件。我们将简略地讨论这一切。

编写RMI客户机应用程序

第 13 页(共23 页)让我们研究一下编写 RMI 独立应用程序所包含的步骤。我们照常将 main 方法编码为入口点,并且在其中执行 RMI 初始化步骤。首先设置安全性管理器,以便 RMI 运行时能够下载类文件,然后通过使用 RMI 命名注册表获取一个对远程对象的引用。最后我们就能够调用远程方法了。

这里是编写 RMI 客户机应用程序的代码:

import java.rmi.*;
public class MeetingClient
{
public static void main ( String [] args )
throws RemoteException,
java.net.MalformedURLException,
java.rmi.NotBoundException
{
// 1. Set Security Manager
// 2. Look up remote object from name space
// 3. Call remote methods
}
}

*** 本图将被删除。留在这里仅供对照/检查用 ***

虽然这个代码显示了仅在 main 方法中调用远程方法,但一旦应用程序检索到对象引用,则应用程序也可以在其它方法中调用远程方法。此外,请注意,不需要进行清理,即使对远程对象,Java RMI 基础结构也会保证进行垃圾收集工作。

现在我们将更详细地讨论这些步骤。

设置安全性管理器

第 14 页(共23 页)该代码看起来和对象服务器 main 中的代码类似。象对象服务器一样,客户机应用程序也可以选择是否设置安全性管理器。并且原因也相似:RMI 运行时会自动将远程对象的存根类文件下载到客户机,但仅当应用程序安装了安全性管理器时才能这样做。如果应用程序使用缺省的安全性管理器,则需要在客户机计算机的类路径预安装存根类文件,否则应用程序将捕获到一个安全性异常。

还应该注意,在这个代码中 main 方法只是将与 RMI 相关的异常抛回到命令行。更健壮的应用程序应该包含 try-catch 块以便本地进行错误处理。

这里是设置安全性管理器的代码:

import java.rmi.*;
public static void main ( String [] args )
throws RemoteException,
java.net.MalformedURLException,
java.rmi.NotBoundException
{
System.setSecurityManager(new RMISecurityManager());
...
}

查找对象

第 15 页(共23 页)一旦应用程序安装了可使用的安全性管理器,它就可以从 RMI 命名注册表检索远程对象的引用。要这样做,需调用静态 lookup 方法,传递对象服务器为远程对象所注册的相同名称。lookup 方法将检索对远程对象的引用并创建存根对象,这里,存根对象存储在变量 r 中。

如果客户机提供的名称与注册表中的名称不匹配,则 lookup 方法抛出 NotBoundException。如果所提供的 URL 无效,则 lookup 抛出 MalformedURLException。在下面的这个简单的代码片段中,没有显式地捕获这些异常,但在实际程序中您需要这样做。

请注意返回的引用类型是 Remote 类型,它是所有远程对象的超类。但是实际上,我们想要的是作为远程接口中定义的对 Meeting 接口的引用。因此,我们需要如下一页所述那样对引用进行向下强制类型转换。

这里是查找对象的代码:

import java.rmi.*;
public static void main ( String [] args )
throws RemoteException,
java.net.MalformedURLException,
java.rmi.NotBoundException
{
System.setSecurityManager(new RMISecurityManager());
Remote r = Naming.lookup {"rmi://myhost.com/Meeting" );
...
}

调用远程方法

第 16 页(共23 页)在应用程序能够调用远程方法之前,它必须转换远程引用的类型以匹配接口定义,在本示例中是 HelloInterface。虽然您可以使用简单的强制类型转换做到这一点,但这里显示的代码首先通过调用 instanceof 运算符来检查强制类型转换是否有效。当远程对象不是预期的类型时,这种技术可使您可以避免 InvalidCast 运行时异常。

一旦我们成功地对引用进行了强制类型转换,就可以调用远程方法;这里是 getDate 方法,它返回一个字符串。

这里是调用远程方法的代码:

import java.rmi.*;
public static void main ( String [] args )
throws RemoteException,
java.net.MalformedURLException,
java.rmi.NotBoundException
{
...
Remote r = Naming.lookup (
"rmi://myhost.com/Meeting");
String s = null;
if (r instanceof Meeting )
{
ms = (Meeting)r;s = ms.getDate();
}
}

编写RMI客户机applet

第 17 页(共23 页)现在,让我们研究将 RMI 客户机编写为 applet 而不是独立应用程序时这两者的差异。有三个基本差异:

通常的 applet 是在 init 方法而不是在 main 编码 RMI 初始化。

因为您不能更改 init 的方法说明(正在从 Applet 重写它),所以不能将 init 编码为可抛出的异常。因此必须用 try-catch 块处理异常。

不必安装 RMI 安全性管理器,因为 applet 自动使用允许下载远程类的 AppletSecurityManager。尽管有这些不同,applet 客户机中与 RMI 相关的代码和应用程序中的基本相同。在这里仍然要使用 RMI 命名注册表来查找远程引用,并将返回的引用强制转换成正确类型等。

applet 和应用程序之间的另一个差异是:要在浏览器内使用 applet,必需编写引用 applet 的 HTML 页面。在下一页我们研究这个问题。

这里是编写 RMI 客户机 applet 的代码:

import java.rmi.*;
public class MeetingClientApplet extends java.applet.Applet
{
public void init ()
{
try
(
//1. Look up remote object in the RMI registry
//2. Call remote methods (can also call from
//   other methods if you save th reference)
}
catch ( Exception e )
{
}
}
}

编写用于applet的HTML页面

第 18 页(共23 页)这是一个简单的 HTML 文件,它装入上一页显示的 applet。HTML 页面中关键的一行是 applet 标记,在这个标记中指定 applet 的类文件(无文件扩展名)。然后,需要在 Web 服务器上安装该 HTML 文件和 applet 类文件。当用户在其支持 Java 平台的浏览器中显示这个页面时,浏览器会将 applet 类文件下载到客户机计算机,并调用 init 方法,在本示例中,它开始 RMI 通信。

Meeting Client Applet


...