2 分布式对象模型
2.1 分布式对象应用
RMI应用通常由两个独立的程序组成:服务器和客户端。典型的服务器应用程序创建多个远程对象,使这些远程对象的引用可访问,并等待客户端调用这些远程对象的方法。典型的客户端应用程序获取服务器中一个或多个远程对象的远程引用,然后调用这些对象的方法。RMI提供了服务器和客户端进行通信并相互传递信息的机制。这样的应用有时被称为分布式对象应用。
分布式对象应用需要:
-
定位远程对象
应用程序可以使用两种机制之一获取对远程对象的引用。应用程序可以将其远程对象注册到RMI的简单命名设施rmiregistry中,或者应用程序可以在正常操作的过程中传递和返回远程对象引用。
-
与远程对象通信
远程对象之间的通信细节由RMI处理;对于程序员来说,远程通信看起来像标准的方法调用。
-
为作为参数或返回值传递的对象加载类字节码
因为RMI允许调用者将对象传递给远程对象,RMI提供了加载对象代码以及传输数据的必要机制。
下面的插图描述了一个使用注册表获取远程对象引用的RMI分布式应用程序。服务器调用注册表将名称与远程对象关联。客户端在服务器的注册表中按名称查找远程对象,然后调用其方法。插图还显示了RMI系统使用现有的Web服务器从服务器到客户端以及从客户端到服务器加载用Java编程语言编写的类的字节码的过程,以及在需要时加载类字节码。RMI可以使用Java平台支持的任何URL协议(例如HTTP、FTP、文件等)加载类字节码。
2.2 术语定义
在Java SE平台的分布式对象模型中,远程对象是可以从另一个Java虚拟机调用其方法的对象,可能位于不同的主机上。这种类型的对象由一个或多个远程接口描述,这些接口是用Java编程语言编写的声明远程对象的方法。
远程方法调用(RMI)是在远程对象上调用远程接口的方法。最重要的是,对远程对象的方法调用与对本地对象的方法调用具有相同的语法。
2.3 分布式和非分布式模型对比
Java SE平台的分布式对象模型与Java SE平台的对象模型在以下方面类似:
-
对远程对象的引用可以作为参数传递或作为任何方法调用(本地或远程)的结果返回。
-
远程对象可以使用Java编程语言内置的强制转换语法转换为实现支持的一组远程接口中的任何一个。
-
内置的instanceof运算符可用于测试远程对象支持的远程接口。
Java SE平台的分布式对象模型与Java SE平台的对象模型在以下方面不同:
-
远程对象的客户端与远程接口交互,而不是与这些接口的实现类交互。
-
传递给远程方法调用的非远程参数和返回值是通过复制而不是通过引用传递的。这是因为对象的引用仅在单个虚拟机内有用。
-
远程对象通过引用传递,而不是通过复制实际的远程实现。
-
类java.lang.Object定义的某些方法的语义专门用于远程对象。
-
由于调用远程对象的失败模式本质上比调用本地对象的失败模式更复杂,客户端必须处理在远程方法调用期间可能发生的额外异常。
2.4 RMI接口和类概述
负责指定RMI系统远程行为的接口和类定义在java.rmi包层次结构中。以下图显示了几个这些接口和类之间的关系:
2.4.1 java.rmi.Remote
接口
在RMI中,远程接口是一个声明一组可以从远程Java虚拟机调用的方法的接口。远程接口必须满足以下要求:
-
远程接口必须至少直接或间接地扩展接口
java.rmi.Remote
。 -
远程接口或其超级接口中的每个方法声明必须满足远程方法声明的要求,如下所示:
-
远程方法声明的throws子句中必须包括异常
java.rmi.RemoteException
(或其超类,如java.io.IOException
或java.lang.Exception
),以及任何应用程序特定的异常(请注意,应用程序特定的异常不必扩展java.rmi.RemoteException
)。 -
在远程方法声明中,作为参数或返回值声明为远程对象(直接在参数列表中声明或嵌入在参数中的非远程对象中)必须声明为远程接口,而不是该接口的实现类。
-
接口java.rmi.Remote
是一个标记接口,不定义任何方法:
public interface Remote {}
远程接口必须至少扩展接口java.rmi.Remote
(或另一个扩展java.rmi.Remote
的远程接口)。但是,远程接口可以在以下条件下扩展非远程接口:
- 远程接口也可以扩展另一个非远程接口,只要扩展接口的所有方法(如果有的话)满足远程方法声明的要求。
例如,下面的接口BankAccount
为访问银行账户定义了一个远程接口。它包含用于向账户存款、获取账户余额和从账户取款的远程方法:
public interface BankAccount extends java.rmi.Remote {
public void deposit(float amount)
throws java.rmi.RemoteException;
public void withdraw(float amount)
throws OverdrawnException, java.rmi.RemoteException;
public float getBalance()
throws java.rmi.RemoteException;
}
下一个示例显示了一个有效的远程接口Beta
,它扩展了一个具有远程方法的非远程接口Alpha
和接口java.rmi.Remote
:
public interface Alpha {
public final String okay = "constants are okay too";
public Object foo(Object obj)
throws java.rmi.RemoteException;
public void bar() throws java.io.IOException;
public int baz() throws java.lang.Exception;
}
public interface Beta extends Alpha, java.rmi.Remote {
public void ping() throws java.rmi.RemoteException;
}
2.4.2 RemoteException
类
java.rmi.RemoteException
类是在远程方法调用期间由RMI运行时抛出的异常的超类。为确保使用RMI系统的应用程序的健壮性,远程接口中声明的每个远程方法必须在其throws子句中指定java.rmi.RemoteException
(或其超类,如java.io.IOException
或java.lang.Exception
)。
当远程方法调用因某种原因失败时,将抛出异常java.rmi.RemoteException
。远程方法调用失败的一些原因包括:
- 通信失败(远程服务器不可访问或拒绝连接;连接被服务器关闭等)
- 参数或返回值编组或解组期间的失败
- 协议错误
类RemoteException
是一个已检查异常(必须由远程方法的调用者处理,并由编译器检查),而不是RuntimeException
。
2.4.3 RemoteObject
类及其子类
RMI服务器功能由java.rmi.server.RemoteObject
及其子类java.rmi.server.RemoteServer
和java.rmi.server.UnicastRemoteObject
提供。
-
类
java.rmi.server.RemoteObject
为远程对象提供了java.lang.Object
方法hashCode
、equals
和toString
的实现,这对于远程对象是合理的。 -
创建远程对象并导出它们(使它们对远程客户端可用)所需的方法由类
UnicastRemoteObject
提供。 -
java.rmi.server.UnicastRemoteObject
类定义了一个单播远程对象,其引用仅在服务器进程存活时有效。
2.5 实现远程接口
实现远程接口的类的一般规则如下:
- 该类通常扩展
java.rmi.server.UnicastRemoteObject
,从而继承了java.rmi.server.RemoteObject
和java.rmi.server.RemoteServer
类提供的远程行为。 - 该类可以实现任意数量的远程接口。
- 该类可以扩展另一个远程实现类。
- 该类可以定义不在远程接口中出现的方法,但这些方法只能在本地使用,无法远程访问。
例如,以下类BankAcctImpl
实现了BankAccount
远程接口,并扩展了java.rmi.server.UnicastRemoteObject
类:
package mypackage;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class BankAccountImpl
extends UnicastRemoteObject
implements BankAccount
{
private float balance = 0.0;
public BankAccountImpl(float initialBalance)
throws RemoteException
{
balance = initialBalance;
}
public void deposit(float amount) throws RemoteException {
...
}
public void withdraw(float amount) throws OverdrawnException,
RemoteException {
...
}
public float getBalance() throws RemoteException {
...
}
}
注意:
-
如果需要,实现远程接口的类可以除了
java.rmi.server.UnicastRemoteObject
之外扩展其他类。但是,实现类必须承担导出对象的责任(由UnicastRemoteObject
构造函数处理),并且必须实现(如果需要)从java.lang.Object
类继承的hashCode
、equals
和toString
方法的正确远程语义。 -
如果导出一个远程对象而没有指定套接字工厂,或者使用不包含
RMIClientSocketFactory
和RMIServerSocketFactory
类型参数的UnicastRemoteObject.exportObject
方法或构造函数UnicastRemoteObject
导出对象,则远程对象将导出到所有本地地址。要将远程对象导出到特定地址,请参阅"RMI套接字工厂"部分。
2.6 远程方法调用中的参数传递
传递给远程对象的参数或返回值可以是任何可序列化的对象。这包括基本类型、远程对象和实现java.io.Serializable
接口的非远程对象。有关如何使类可序列化的更多详细信息,请参阅"Java对象序列化规范"。在读取参数、返回值和异常时,RMI系统会动态下载本地不可用的类。有关RMI在读取参数、返回值和异常时如何下载参数和返回值类的更多信息,请参阅"动态类加载"部分。
2.6.1 传递非远程对象
作为远程方法调用的参数传递或作为远程方法调用的结果返回的非远程对象是通过复制传递的;也就是说,该对象使用Java SE平台的对象序列化机制进行序列化。
因此,当将非远程对象作为远程方法调用的参数或返回值传递时,在调用远程对象之前,会复制非远程对象的内容。
当从远程方法调用返回非远程对象时,在调用方虚拟机中会创建一个新对象。
2.6.2 传递远程对象
在远程方法调用中将导出的远程对象作为参数或返回值传递时,将传递该远程对象的存根。未导出的远程对象不会被替换为存根实例。作为参数传递的远程对象只能实现远程接口。
2.6.3 引用完整性
如果在单个远程方法调用中从一个JVM传递两个对对象的引用到另一个JVM(作为参数或返回值),并且这些引用指向发送JVM中的同一对象,则这些引用将在接收JVM中引用对象的单个副本。更一般地说:在单个远程方法调用中,RMI系统在作为参数或返回值传递的对象之间保持引用完整性。
2.6.4 类注释
当对象在远程方法调用中从一个JVM发送到另一个JVM时,RMI系统会在调用流中的类描述符上注释信息(URL),以便在接收方加载类。要求在远程方法调用期间按需下载类。
2.6.5 参数传输
RMI调用中的参数被写入一个java.io.ObjectOutputStream
类的子类流,以便将参数序列化到远程调用的目的地。该ObjectOutputStream
子类通过覆盖replaceObject
方法来用存根实例替换每个导出的远程对象。对象参数通过ObjectOutputStream
的writeObject
方法写入流。ObjectOutputStream
通过writeObject
方法写入流中的每个对象(包括被写入的对象引用的对象)调用replaceObject
方法。RMI的ObjectOutputStream
子类的replaceObject
方法返回以下内容:
-
如果传递给
replaceObject
的对象是java.rmi.Remote
的实例,并且该对象已导出到RMI运行时,则返回远程对象的存根。如果对象是java.rmi.Remote
的实例且该对象未导出到RMI运行时,则replaceObject
返回对象本身。通过调用方法java.rmi.server.RemoteObject.toStub
获取远程对象的存根。 -
如果传递给
replaceObject
的对象不是java.rmi.Remote
的实例,则简单地返回该对象。
RMI的ObjectOutputStream
子类还实现了annotateClass
方法,该方法在调用流中注释类的位置,以便在接收方下载类。有关annotateClass
的使用更多信息,请参阅"动态类加载"部分。
由于参数被写入单个ObjectOutputStream
,在调用方引用同一对象的引用将在接收方引用对象的同一副本。在接收方,参数由单个ObjectInputStream
读取。
ObjectOutputStream
的默认写入对象行为(以及类似地,ObjectInputStream
的读取对象行为)在参数传递中保持不变。例如,在写入对象时调用writeReplace
,在读取对象时调用readResolve
,RMI的参数编组和解组流会遵守这些行为。
类似于上述RMI中的参数传递,返回值(或异常)被写入ObjectOutputStream
的子类,并具有与参数传输相同的替换行为。
2.7 定位远程对象
提供了一个简单的引导名称服务器,用于存储对远程对象的命名引用。可以使用类java.rmi.Naming
的基于URL的方法存储远程对象引用。
要在客户端上调用远程对象的方法,该客户端必须首先获取对该对象的引用。对远程对象的引用通常作为方法调用的参数或返回值获得。RMI系统提供了一个简单的引导名称服务器,用于从给定主机上获取远程对象。类java.rmi.Naming
提供了基于统一资源定位符(URL)的方法,用于查找、绑定、重新绑定、解绑和列出在特定主机和端口上维护的名称-对象对。