RMI 应用通常有两个分开的程序组成,一个服务端程序和一个客户端程序。一个典型的服务端程序创建一些远程对象,使得对这些远程对象的引用可以被访问,等待客户端调用这些远程对象提供的方法。一个典型的客户端程序获取远程引用,指向一个或者多个服务端上的远程对象,然后调用这些远程对象所提供的方法。通常我们称这为分布式对象应用程序。
3.1 RMI的工作方式
分布式对象应用程序需要做的事情:
l 查找(定位)远程对象。 应用程序可以使用各种不同的机制取得远程对象的引用。比如应用程序可以通过 RMI 提供的简单的命名工具, RMI 注册。或者应用程序可以传递和返回远程对象作为远程方法调用的一部分。
l 和远程对象通信。 远程对象之间通信的细节由 RMI 处理,对程序员来说远程对象的通信和通常的 Java 方法调用没有区别。
l 加载传递过来的远程对象的类定义。 因为 RMI 允许对象双向传递,因此它提供了加载对象类定义和传递对象数据的机制。
下图(图3.1)描述了一个使用 RMI 注册机制取得远程对象引用的RMI分布式应用。 Server调用注册机制将一个名字和远程对象关联(或者叫绑定)。Client 根据名字在Server的注册机制里面查找远程对象,获得远程对象的引用,并调用远程对象的方法。下图还描述了这样的情况,RMI 系统使用Web Server,从Server 到Client或者从Client到Server,加载所需对象的类定义。
3.1.1 动态类加载
RMI的一个独特的核心优势就是能够加载一个未在接收端的Java虚拟机( JVM )中定义的类。在一个Java虚拟机中定义的一个对象所有的类型和行为,能够从这个Java虚拟机传递到另外一个Java虚拟机,甚至可以是远程的Java虚拟机。RMI根据对象的真实类型传递对象,所以当对象被传递到另一个Java虚拟机时对象的行为不会改变。这个特性使得新的对象类型和行为能够被引入到一个远程的Java虚拟机当中,也就是说动态扩展了应用程序的行为。下面的compute engine例子就是使用这种能力往分布式程序中引入新的行为。
3.1.2 远程对象
正如其他很多Java应用程序一样,一个构建在RMI之上的分布式应用也是由接口和类组成的。接口声明方法,类实现在接口中定义的方法,也许还会声明额外的方法。在分布式应用中,一些方法可能存在于某些Java虚拟机中但是却不在另一个Java虚拟机中。如果一个对象的方法能够在不同的Java虚拟机之间被调用,那么此对象被称作远程对象(remote objects)。
一个普通对象可通过实现远程接口( java.rmi.Remote )变成远程对象,这个远程接口有如下特征。
l 一个远程接口扩展 java.rmi.Remote 接口
l 每个远程接口里声明的方法除了声明抛出本身应用特定的异常之外,都要声明抛出 java.rmi.RemoteException 异常
当对象从一个Java虚拟机传递到另一个Java虚拟机时,RMI区别对待远程对象和非远程对象。当RMI传递一个远程对象到另一个JVM时,它实际上传递的是此远程对象对应的存根(stub)对象,而不是传递这个对象的拷贝。这个存根对象担当远程对象的代表或者代理的角色,为client提供到远程对象的引用。Client调用所获得的stub的方法,而这个stub则负责执行远程对象里这个方法的调用。
一个远程对象的stub(存根)实现了与这个远程对象所实现的远程接口的相同方法集合。这个特性使得stub能够被转型为远程对象实现的远程接口。然而,也只有那些在远程接口里声明的方法才能被接收端的JVM调用。
在客户端进行远程方法调用时,RMI框架会把遇到的网络通信失败转换为RemoteException,客户端可以捕获这种异常,并进行相应的处理。
3.1.3 远程方法中的参数与返回值传递
RMI规范对参数及返回值的传递作出了以下规定:
l 只有基本数据类型、远程对象及可序列化的对象才能作为参数或者返回值进行传递。
l 如果参数或返回值是一个远程对象,那么把它的存根对象传递到接收方。也就是接收方得到的是远程对象的存根对象。
l 如果参数或返回值是可序列化的对象,那么直接传递该对象的序列化数据。也就是说,接收方得到的是发送方的可序列化的对象的复制品。
l 如果参数或返回值是基本数据类型,那么直接传递该数据的序列化数据。也就是说,接收方得到的是发送方的基本数据类型的复制品。
3.1.4 远程对象的equals()、hashCode()和clone()方法
在Object对象中定义了equals()、hashCode()和clone()方法,这些方法没有声明抛出RemoteException。Java语言规定了当子类覆盖父类方法时,子类方法不能声明抛出比父类方法更多的异常。而RMI规范要求远程接口中的方法必须声明抛出RemoteException异常,因此无法在远程接口中定义equals()、hashCode()和clone()方法。这意味着一个远程对象的这些方法永远只能作为本地方法,被本地Java虚拟机内的其它对象调用,而不能作为远程方法,被客户端远程调用。
3.1.5 分布式垃圾收集
在Java虚拟机中,对于一个本地对象,只要不被本地Java虚拟机中的任何变量引用,它就会结束生命周期,可以被垃圾回收器回收。而对与一个远程对象,不仅会被本地Java虚拟机中的变量引用还会被远程引用。如将远程对象注册到rmiregistry注册表时,rmiregistry注册表则持有它的远程引用。
RMI框架采用分布式垃圾收集机制(DGC,Distributed Garbage Collection)来管理远程对象的生命周期。DGC的主要规则是,只有当一个远程对象不受任何本地引用和远程引用,这个远程对象才会结束生命周期。
当客户端获得了一个服务器端的远程对象存根时,就会向服务器发送一条租约通知,告诉服务器自己持有这个远程对象的引用了。此租约有一个租约期限,租约期限可通过系统属性java.rmi.dgc.leaseValue来设置,以毫秒为单位,其默认值为600 000毫秒。当到达了租约期限的一半时间,客户端如果还持有远程引用,就会再次向服务器发送租约通知。如果租约到期后服务器端没有继续收到客户端的新的租约通知,服务器端就会认为这个客户已经不再持有远程对象的引用。
有时,远程对象希望在不再受到任何远程引用时执行一些操作,如释放所占用的资源,以便安全的结束生命周期。这样的远程对象需要实现java.rmi.server.Unreferenced接口,该接口有一个unreferenced()方法,远程对象可以在这个方法中执行释放占用的相关资源的操作。当RMI框架监测到一个远程对象不再受到任何远程引用时,就会调用的这个对象的unreferenced()方法。
3.2 通过RMI创建分布式应用
通过使用RMI开发一个分布式应用遵循下面几个步骤:[3]
设计实现分布式应用的组件
编译源代码
使得你的类在网络上可访问
启动应用程序
3.2.1 设计实现应用程序组件
首先决定应用程序的体系结构,包括哪个组件是本地对象,哪个组件远程可访问。这一步骤包括:
l 定义远程接口. 一个远程接口指定了哪些方法能够被client远程调用。Client程序针对远程接口编程,而不是针对实现了这些远程接口的类。这些接口的设计包括了如何声明远程方法所需参数的对象类型,以及远程方法返回值类型。
l 实现远程对象. 远程对象需要实现至少一个远程接口。远程对象也可以实现其它的接口,但这个接口里声明的方法只在本地JVM可用。如果任何一个本地类要被用作为这些(远程)方法的参数或者是返回值,这些类也需要被实现。
l 实现客户端. 在远程接口定义好之后,使用远程对象的客户端可以在任何时候被实现,任何时候的意思包括在远程对象部署之后。
3.2.1.1 实现服务器端
ComputeEngine是一个服务器上的远程对象,从客户端接受任务,执行任务之后返回结果。这些任务是在服务端运行的机器上执行的。这种类型的分布式应用程序使得许多客户端使用性能强劲的机器或者是拥有特殊硬件资源的机器。
ComputeEngine的奇特之处在于它运行的任务不需要在它写代码或者运行的时候定义。新的任务可以随时被创建然后交由其执行。一个任务唯一的要求就是任务类必须实现一个特定的接口。需要完成的任务的代码能够被RMI系统下载到ComputeEngine。然后ComputeEngine运行这个任务,使用ComputeEngine所运行的机器上的资源。
执行任意任务的能力是由Java平台的动态特性保证的,这个动态特性又被RMI扩展到网络世界。RMI动态装载任务代码到ComputeEngine所在的JVM,然后运行这个任务,而不需要预先知道实现这个任务的类。这样一个有动态加载代码能力的应用通常被称为 behavior-based application 。这种应用程序通常要求允许代理的基础结构。有了RMI,这种应用构成了Java平台上分布式计算的基本的机制。
l 设计远程接口
compute.Compute接口定义了远程可访问的部分,下面是Compute接口的源代码。通过继承java.rmi.Remote接口,Compute接口表明自己是接口方法可以被另一个JVM调用的接口。所以实现该接口的对象就是远程对象。
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
T executeTask(Task t) throws RemoteException;
}
作为远程接口的一员,executeTask方法是一个远程方法。因此该方法需要定义为能抛出java.rmi.RemoteException异常。这个异常由RMI系统调用一个远程方法时抛出,表示通讯失败或者是协议错误发生。RemoteException是一个受检查的异常,所以任何调用远程方法的代码需要处理这个异常,要么捕获该异常,要么声明抛出子句。
ComputeEngine需要的第二个接口是Task接口,这个接口也是executeTask的类型参数。Compute.Task接口定义了ComputeEngine和它要执行的工作直接的接口,提供了开始这个工作的方法。下面是Task接口的源代码:
package compute;
public interface Task {
T execute();
}
Task接口就定义了一个方法,execute,无参数,也没有异常。因为这个接口没有继承Remote接口,所以方法也无需声明抛出java.rmi.RemoteException子句。
RMI使用Java对象序列化机制在JVM之间以值传递方式传输对象。对象要能被序列化就需要实现java.io.Serializable这个标识接口,因此实现Task接口的类也要实现java.io.Serializable接口,作为Task执行结果的对象的类也必须要实现这个接口。
不同种类的任务都能被一个Compute对象执行,只要这些任务都实现了Task接口类型。实现这个接口的类可以包含任何执行计算所需要的数据和其他执行计算所需要的方法。
RMI假定Task对象由Java语言程序编写,ComputeEngine先前不知道的Task对象的实现,在需要时由RMI下载到ComputeEngine所在的JVM。这个能力使得ComputeEngine的Client能够定义新的将要在Server上运行的任务,而不需要代码显式的被安装在Server机器上。
l 实现ComputeEngine类 (服务类)
概括的说,一个实现远程接口的类至少需要做以下步骤:
Ø 声明要实现的remote interface
Ø 为每个remote对象的定义构造函数
Ø 实现remote interface里的远程调用方法
一个RMI 的server端程序需要创建初始的远程对象并把他们发布到RMI的环境,使其能够接受远程调用。这一步骤可以包括在远程对象某个方法中,也可以在其他类的实体对象中。这一步骤要做如下步骤:
Ø 创建并安装一个security manager
Ø 创建并发布一个或者多个远程对象
Ø 使用RMI registry注册至少一个远程对象
实现安全管理器的目的是RMI框架利用Java安全管理器来确保远程方法调用的安全性。
下面是ComputeEngine的全部实现。这个类实现了远程调用接口(remote interface)Compute,还有main方法,用来安装compute engine。下面是这个类的源代码:
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();
//使用匿名端口导出远程对象,以便能够接收传入的调用。
//返回远程对象的stub
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}
远程方法需要的参数或者是返回值几乎可以是任何对象,包括本地对象,远程对象,元数据类型。更确切的说,任何实体对象只要是符合如下类型的实例都能作为远程对象可用的参数或者返回值,这些类型包括元数据类型,远程对象或者是一个可序列化对象(实现了java.io.Serializable的对象)。
有些对象类型不能满足上面的要求,因此不能传递给远程方法作为参数,或作为远程方法的返回值。这些对象如线程或者是文件描述符,它们只在单个地址空间内是有意义的,许多核心的类都实现了Serializable接口。
如何传递参数和获得返回值遵循如下的约定:
Ø 远程对象本质上通过引用传递。一个远程对象的引用包含在此对象所对应的存根对象中,在向注册处注册时传递就是这个存根对象,而客户端通过名字在注册出查找获得的也是这个存根对象。这个存根可作为一个实现了远程接口的客户端的代理。
Ø 本地对象通过值拷贝传递,其中使用对象序列化的技术。默认的拷贝方法是,拷贝除了被标识为static和transient的所有的域。默认的序列化行为可以被重载。
通过引用传递远程对象意味着所有对这个对象状态的修改都会影响到原来的远程对象。当传递远程对象引用的时候,只有远程接口提供的方法才能被接收者使用。任何在实现类中定义的方法或者类实现的非远程接口中定义的方法,接收端是不可用的。
在远程方法调用的参数返回值中,非远程对象的对象通过值拷贝传递。因此,在接受端的JVM中一个对象的拷贝被创建。任何对对象状态的更改只会影响到拷贝的对象,而不会影响发送端的原始对象实例。任何由发送端对对象状态的更改,只会影响发送端原始对象的实例,而不会影响接收端该对象的拷贝对象。
l 实现服务器 (由服务类中的main方法完成)
ComputeEngine中最复杂的代码就是这个main方法。Main方法用来启动ComputeEngien因此需要做必要的初始化,为server接受client的请求做准备。这个方法不是远程方法,就是说它不能被另一个JVM调用。因为main方法声明成静态的,这个方法不会和一个对象关联,只和ComputeEngine类关联。