本教程是针对JDK 8编写的。本页中描述的示例和实践不利用后续版本引入的改进,并且可能使用不再可用的技术。
请查看Java语言更改,了解Java SE 9及后续版本中更新的语言特性的概述。
请查看JDK发行说明,了解所有JDK版本的新功能、增强和删除或弃用选项的信息。
本节讨论了实现计算引擎类的任务。一般来说,实现远程接口的类至少应该完成以下几个任务:
RMI服务器程序需要创建初始的远程对象并将其导出到RMI运行时,以使其可以接收传入的远程调用。这个设置过程可以封装在远程对象实现类本身的方法中,也可以包含在另一个类中。设置过程应该完成以下几个任务:
计算引擎的完整实现如下所示。
类实现了远程接口 engine.ComputeEngine
Compute
,并且还包括了用于设置计算引擎的 main
方法。以下是 ComputeEngine
类的源代码:
package engine; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import compute.Compute; import compute.Task; public class ComputeEngine implements Compute { public ComputeEngine() { super(); } public <T> T executeTask(Task<T> t) { return t.execute(); } public static void main(String[] args) { if (System.getSecurityManager() == null) { System.setSecurityManager(new SecurityManager()); } try { String name = "Compute"; Compute engine = new ComputeEngine(); Compute stub = (Compute) UnicastRemoteObject.exportObject(engine, 0); Registry registry = LocateRegistry.getRegistry(); registry.rebind(name, stub); System.out.println("ComputeEngine bound"); } catch (Exception e) { System.err.println("ComputeEngine exception:"); e.printStackTrace(); } } }
以下各节将讨论计算引擎实现的每个组成部分。
计算引擎的实现类声明如下:
public class ComputeEngine implements Compute
此声明表示该类实现了Compute
远程接口,因此可以用作远程对象。
ComputeEngine
类定义了一个远程对象实现类,该类实现了一个远程接口和没有其他接口。 ComputeEngine
类还包含两个只能在本地调用的可执行程序元素。其中之一是ComputeEngine
实例的构造函数。第二个元素是一个main
方法,用于创建ComputeEngine
实例并使其对客户端可用。
ComputeEngine
类有一个不带参数的构造函数。构造函数的代码如下:
public ComputeEngine() { super(); }
此构造函数只调用超类构造函数,即Object
类的无参数构造函数。虽然即使从ComputeEngine
构造函数中省略了超类构造函数,超类构造函数仍会被调用,但出于清晰起见,它被包含在内。
远程对象的类为每个在远程接口中指定的远程方法提供实现。 Compute
接口包含一个远程方法executeTask
,其实现如下:
public <T> T executeTask(Task<T> t) { return t.execute(); }
此方法实现了ComputeEngine
远程对象与其客户端之间的协议。每个客户端都向ComputeEngine
提供一个具有Task
接口的特定实现的Task
对象的实例。 ComputeEngine
执行每个客户端的任务,并将任务的execute
方法的结果直接返回给客户端。
远程方法的参数或返回值可以是几乎任何类型,包括本地对象、远程对象和原始数据类型。更准确地说,只要实体是基本数据类型、远程对象或可序列化对象(即实现java.io.Serializable
接口),就可以将任何类型的实体传递给远程方法或从远程方法返回。
一些对象类型不符合这些标准,因此无法传递给远程方法或从远程方法返回。其中大多数对象,如线程或文件描述符,封装的信息只在单个地址空间内才有意义。许多核心类,包括java.lang
和java.util
包中的类,实现了Serializable
接口。
有关参数和返回值传递规则的规定如下:
static
或transient
的字段外,所有字段都会被复制。可以按类别覆盖默认的序列化行为。通过引用传递远程对象意味着通过远程方法调用对对象状态所做的任何更改都会反映在原始远程对象中。当传递远程对象时,只有那些远程接口才对接收方可用。实现类中定义的任何方法或类实现的非远程接口中定义的任何方法都对接收方不可用。
例如,如果您传递对ComputeEngine
类实例的引用,接收方只能访问计算引擎的executeTask
方法。该接收方将无法看到ComputeEngine
构造函数,其main
方法,或其对java.lang.Object
的任何方法的实现。
在远程方法调用的参数和返回值中,非远程对象是按值传递的。因此,在接收Java虚拟机中创建对象的副本。接收方对对象状态的任何更改仅反映在接收方的副本中,而不反映在发送方的原始实例中。发送方对对象状态的任何更改仅反映在发送方的原始实例中,而不反映在接收方的副本中。
main
方法ComputeEngine
实现中最复杂的方法是main
方法。main
方法用于启动ComputeEngine
,因此需要进行必要的初始化和管理工作,以准备服务器接受来自客户端的调用。此方法不是远程方法,这意味着它不能从不同的Java虚拟机中调用。由于main
方法声明为static
,因此该方法与任何对象都不相关,而是与ComputeEngine
类相关。
main
方法的第一个任务是创建和安装安全管理器,它保护来自Java虚拟机中运行的不受信任的下载代码对系统资源的访问。安全管理器确定下载的代码是否可以访问本地文件系统或执行任何其他特权操作。
如果RMI程序未安装安全管理器,则RMI不会为作为远程方法调用的参数或返回值接收到的对象下载类(除了从本地类路径)。这个限制确保下载的代码执行的操作受到安全策略的约束。
这是创建和安装安全管理器的代码:
if (System.getSecurityManager() == null) { System.setSecurityManager(new SecurityManager()); }
接下来,main
方法创建了一个ComputeEngine
实例,并使用以下语句将其导出到RMI运行时:
Compute engine = new ComputeEngine(); Compute stub = (Compute) UnicastRemoteObject.exportObject(engine, 0);
静态的UnicastRemoteObject.exportObject
方法导出提供的远程对象,以便它可以从远程客户端接收对其远程方法的调用。第二个参数是一个int
,用于指定用于侦听该对象的传入远程调用请求的TCP端口。通常使用值零,表示使用匿名端口。实际端口将由RMI或底层操作系统在运行时选择。但也可以使用非零值指定要用于侦听的特定端口。一旦exportObject
调用成功返回,ComputeEngine
远程对象就准备好处理传入的远程调用。
exportObject
方法返回导出的远程对象的存根。请注意,变量stub
的类型必须是Compute
,而不是ComputeEngine
,因为远程对象的存根只实现了导出的远程对象实现的远程接口。
exportObject
方法声明可以抛出RemoteException
,这是一种受检异常类型。main
方法使用其try
/catch
块处理此异常。如果不以这种方式处理异常,RemoteException
必须在main
方法的throws
子句中声明。如果必要的通信资源不可用,例如如果请求的端口已绑定到其他用途,则尝试导出远程对象可能会抛出RemoteException
。
在客户端可以调用远程对象上的方法之前,它必须首先获得对远程对象的引用。获取引用可以通过与程序中获取任何其他对象引用的方式完成,例如作为方法的返回值的一部分或作为包含此类引用的数据结构的一部分。
系统提供了一种特殊类型的远程对象,即RMI注册表,用于查找其他远程对象的引用。RMI注册表是一个简单的远程对象命名服务,可以通过名称获取对远程对象的引用。注册表通常仅用于定位RMI客户端需要使用的第一个远程对象。然后,该第一个远程对象可能提供查找其他对象的支持。
java.rmi.registry.Registry
远程接口是用于在注册表中绑定(或注册)和查找远程对象的API。java.rmi.registry.LocateRegistry
类提供了用于合成到特定网络地址(主机和端口)的注册表的远程引用的静态方法。这些方法创建包含指定网络地址的远程引用对象,而不执行任何远程通信。LocateRegistry
还提供了用于在当前Java虚拟机中创建新注册表的静态方法,尽管此示例不使用这些方法。一旦远程对象在本地主机上的RMI注册表中注册,任何主机上的客户端都可以通过名称查找远程对象,获取其引用,然后调用对象上的远程方法。注册表可以由在主机上运行的所有服务器共享,或者单个服务器进程可以创建和使用自己的注册表。
ComputeEngine
类使用以下语句为对象创建一个名称:
String name = "Compute";
然后,代码将名称添加到服务器上运行的RMI注册表中。稍后使用以下语句执行此步骤:
Registry registry = LocateRegistry.getRegistry(); registry.rebind(name, stub);
此rebind
调用对本地主机上的RMI注册表进行远程调用。与任何远程调用一样,此调用可能导致抛出RemoteException
,这由main
方法末尾的catch
块处理。
请注意以下关于Registry.rebind
调用的内容:
LocateRegistry.getRegistry
的无参数重载合成对本地主机上默认的注册表端口1099的引用。如果注册表在除1099之外的端口上创建,则必须使用具有int
参数的重载。ComputeEngine
的实例)永远不会离开它们所在的Java虚拟机。因此,当客户端在服务器的远程对象注册表中执行查找时,将返回存根的副本。在这种情况下,远程对象实际上是通过(远程)引用而不是通过值传递的。bind
、unbind
或rebind
远程对象引用。此限制防止远程客户端删除或覆盖服务器注册表中的任何条目。但是,可以从任何主机(本地或远程)请求lookup
。一旦服务器在本地RMI注册表中注册,它会打印一条指示准备开始处理调用的消息。然后,main
方法完成。不需要让线程等待以保持服务器运行。只要在另一个Java虚拟机中有对ComputeEngine
对象的引用,无论是本地还是远程的,ComputeEngine
对象都不会被关闭或垃圾回收。因为程序在注册表中绑定了对ComputeEngine
的引用,所以它可以被远程客户端(注册表本身)访问。RMI系统保持ComputeEngine
的进程运行。只有当从注册表中删除其绑定并且没有远程客户端持有ComputeEngine
对象的远程引用时,才会回收ComputeEngine
。
ComputeEngine.main
方法中的最后一段代码处理可能出现的任何异常。在代码中可能抛出的唯一检查异常类型是RemoteException
,可以通过UnicastRemoteObject.exportObject
调用或注册表rebind
调用引发。无论哪种情况,程序在打印错误消息后不能做更多的事情,只能退出。在某些分布式应用程序中,可以从无法进行远程调用的故障中恢复。例如,应用程序可以尝试重新尝试操作或选择另一个服务器继续操作。