- 编写Java源代码和HTML文件的步骤。
- class文件和HTML文件的编译和部署步骤 。
- 启动RMI注册表、服务程序和小应用程序的 步骤。
本教学课程需要的文件包括:
- Java远程接口-- Hello.java
- 实现examples.hello.Hello接口的Java远程对象-- HelloImpl.java
- 调用远程方法sayHello的Java小应用程序-- HelloApplet.java
- 引用该小应用程序的主页的HTML代码-- hello.html
注:本教学课程中的名词“远程对象实现”(remote object implementation)、“对象实现”(object implementation)和“
实现”(implementation)等可互换使用,并均指实现远程接 口的类examples.hello.HelloImpl。
对于本教学课程中所用的所有源代码,您可选择以下 列文件格式下载:
- RMIgetStart.zip
- RMIgetStart.tar
- RMIgetStart.tar.Z
编写Java源代码 和HTML文件
因为Java语言要求在一类完全合格的包名称与至该类的 目录路径之间有一个映射,所以,在您开始编写Java语 句之前,您应该确定包名称和目录名称。此映射可使Java
编译器知道在哪个目录查找Java程序所提到的类文件。 对于本教学课程的程序,包名称是examples.hello,源程序 目录是$HOME/myscr/examples/hello。
要在Solaris上为您的源文件创建目录,可执行下列指 令:
mkdir -p $HOME/mysrc/examples/hello
在Windows平台上,您需要进入您选定的目录,然后键 入:
mkdir mysrc
mkdir mysrc\examples
mkdir mysrc\examples\hello
在这部分要完成三项工作:
- 将远程类的功能定义为Java接口
- 编写远程接口实现和服务器类
- 编写一个使用远程服务的客户程序
<将远程类的功能定 义为Java接口
在Java中,远程对象是实现远程接口的类的实例。您 的远程接口将声明您希望远程调用的每个方法。远程接 口具有下列特点:
- 远程接口必须被公开声明。否则,如果一个客户机程序 与该远程接口不在同一个包内,那么该客户机程序在试 图装载实现该远程接口的一个远程对象时就会得到一个
错误。
- 该远程接口继承于java.rmi.Remote接口。
- 除与应用程序相关的某些异常(Exception)之外,每个方法 都必须在其throws子句中声明java.rmi.RemoteException
(或一个 RemoteException的父类)。
- 作为一个参数或返回值传递(直接或嵌入在本地对象内 )的任何一个远程对象的数据类型都必须声明为该远程 接口类型(例如: Hello),而不能是实现类(HelloImpl)。
远程接口examples.hello.Hello的接口定义如下。该接口只包 含一个将串返回给调用程序的方法sayHello:
package examples.hello;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String sayHello() throws RemoteException;
}
因为远程方法调用失败的方式可能与本地方法调用非常 不同(因与网络相关的通信问题和服务程序问题),远程 方法可能会抛出一个java.rmi.RemoteException以报告通信失败
。如果您希望详细了解有关分布式系统的失败和恢复问 题,请阅读“
A Note on Distributed Computing”(《分布式计算初步》)一 书。
<编写远程接 口实现和服务器类
实现一个远程对象类至少必须:
- 声明它至少实现一个远程接口
- 为该远程对象定义构造方法
- 为可被远程调用的方法提供实 现
本文所说的“服务器”类系指具 有一个创建远程对 象实现的实例、并将该实例与rmiregistry中的一个名称相 关联的main方法的类。包含这一main方法的类可能是实现
类本身,也可能是另一个完全不同的类。
在本例中,该main方法是examples.hello.HelloImpl的一部分 。服务器程序必须:
- 创建并安装一个安全管理器。
- 创建某远程对象的一个或多个实例 。
- 为了进行引导,在RMI远程对象 注册表中至少注册一个远程对象。
下面先列出HelloImpl.java的源代码,然后再详细解释上述 6个步骤:
package examples.hello;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.RMISecurityManager;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject
implements Hello {
public HelloImpl() throws RemoteException
{
super();
}
public String sayHello ()
{
return "Hello World!";
}
public static void main
(String args []) {
// Create and install a security manager
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}
try {
HelloImpl obj = new HelloImpl() ;
// Bind this object instance to the name "HelloServer"
Naming.rebind("//myhost/HelloServer", obj);
System.out.println("HelloServer bound in registry");
} catch (Exception e) {
System.out.println("HelloImpl err: " + e.getMessage());
e.printStackTrace();
}
}
}
<实现远程接口
在Java语言中,当一个类声明它实现某一接口时,在 该类与编译器之间达成一个协议。进入该协议后,该类 保证它将为其正在实现的接口所声明的每种方法提供定
义或主体。接口方法隐含表示为public和abstract,所以, 如果实现类不履行协议,那么根据定义 它就 变成一个abstract类,如果该类没有被宣布为abstract,那
么,编译器就会指出这一事实。
本例中的实现类为examples.hello.HelloImpl。实现类声明它 正在实现的是哪个远程接口。HelloImpl类声明如下:
public class HelloImpl extends UnicastRemoteObject implements Hello
作为简化的类,实现类可扩展一个远程类,在本例中 为java.rmi.server.UnicastRemoteObject。UnicastRemoteObject扩展后
,HelloImpl类就能被用来创建一个使用RMI基于缺省的套 接字网络接口进行传输且不间断运行的远程对象。
如果您需要一个在客户程序发出请求时能被创建的远 程对象,那么在您阅读完本教学课程后,您可继续阅读 Remote
Object Activation (《远程对象的创建》)教学课程。此外 ,您还可在有关创
建定制RMI套接字工厂的教学课程中学习如何使用您 自己的通信协议,而非RMI作为缺省所使用的TCP套接字 网络接口。
为远程对象定义构造程序
远程类的构造程序与非远程类的构造程序具有相同的 功能:它初始化每个类的新创建实例的变量,并将该类 的一个实例返回给调用该构造程序的程序。
此外,您的远程对象实例还需要被“输出”。输出后 的远程对象可在一个匿名端口上监听对远程方法的调用 请求,并接受进入的远程方法请求。当您继承了java.rmi.server.UnicastRemoteObject
或java.rmi.activation.Activatable时,您的类在创建后就会被 自动输出。
如果您选择从除UnicastRemoteObject或Activatable以外的任 何类扩展一个远程对象,则您需要通过从您的类的构造 程序(或另一个初始化方法)中调用UnicastRemoteObject.exportObject
方法或Activatable.exportObject方法公开输出此远程对象。
因为对象输出可能会抛出一个java.rmi.RemoteException例外 ,所以您 必须定义一个抛出RemoteException的构造程
序,即使该构造程序只执行这一项工作。如果您忘记了 该构造程序,则javac将产生下列错误信息:
HelloImpl.java:13: Exception java.rmi.RemoteException must be
caught, or it must be declared in the throws clause of this method.
Super();
^
1 error
小结:一个远程对象的实现类必须:
- 实现一个远程接口
- 输出该对象,以便接受进入的远程方法调用
- 声明其构造程序并至少抛出一个java.rmi.RemoteException
examples.hello.HelloImpl类的构造程序如下:
public HelloImpl() throws RemoteException {
super();
}
请注意:
- super方法所调用的是输出远程对象的java.rmi.server.UnicastRemoteObject 的无参数构造程序。
- 该构造程序必须抛出java.rmi.RemoteException例外,因为如果 没有通信资源,则RMI在构造期间输出一个远程对象的努 力就会失败。
尽管对父类的无参数构造程序super()的调用是缺省的(即 使被忽略),但我们还是将其编入上面的例子,目的是 表明这样的事实:Java VM ( Java虚拟机)在构造一个类之
前构造了其父类。
<为每个远程方法提供实 现
一个远程对象的实现类包含实现在远程接口中所定义 的所有远程方法的程序代码。例如,下面就是sayHello方 法的实现,它向调用程序返回字符串"Hello
World !":
public String sayHello() throws RemoteException {
return "Hello World !";
}
向远程方法传送的参数或来自远程方法的返回值可以是 包括对象在内的任何Java类型,但条件是这些对象实现 了java.io.Serializable接口。java.lang和java.util中的核心Java
类大多实现了Serializable接口。在RMI中:
- 缺省情况下,本地对象通过复制传输,除了那些标记为 static或transient的元素以外,一个对象的所有数据元素 (或字段)均被复制传输。有关如何改变缺省序列化行为
的介绍,请参阅Java
Object Serialization Specification ( Java对象序列化技术规范) 。
- 远程对象传输其引用(reference)。某个远程对象的一个引 用实际上就是一个存根的引用,而该存根则是该远程对 象客户程序方面的代理。RMI
技术规范中对存根有详细介绍。我们将在本教学课程 中的“使用rmic生成存根及框架”部分创 建它们。
一个类可以定义在远程接口中没有规定的方法,但这 些方法只能在提供服务的虚拟机上被调用,而不能被远 程调用。
创建和安装安全管理程序
服务器的main方法首先需要创建和安装一个安全管理 程序:或者是RMISecurityManager,或者是您自己定义的程序 。例如:
if (System.getSecurityManager() == null) {
System.setSecurityManager (new RMISecurityManager());
}
必须运行安全管理程序,因为它能确保所装载的类不会 执行不允许执行的操作。如果不指定安全管理程序,那 么除了能在本地CLASSPATH中找到的以外,RMI客户机或服
务器就不允许装载其它的类。
<创建远程对象的一个或多个实 例
服务器的main方法需要创建提供服务的远程对象实现 的一个或多个实例。例如:
HelloImpl obj = new HelloImpl();
构造程序输出远程对象,意味着远程对象一旦被创建就 可以接受进入的调用 呼
叫 。
注册远程对象
调用程序(客户机应用程序、对等体或小应用程序)要 能调用远程对象的一个方法,则该调用程序必须首先获 得该远程对象的一个引用。
对于引导来说,RMI系统提供一个能将类似于"//host/objectname" 的URL格式名称关联到远程对象的远程对象注册表,其中 ,objectname是简单的字符串名称。
RMI注册表是一个简单的可使远程客户机获得向远程对 象引用的服务器端名称服务器。它一般只用来找到RMI客 户机需要与之对话的第一个远程对象。随后,这第一个
对象反过来又提供可找到与应用程序相关的其它对象。
例如,引用可以作为一个远程方法调用的参数或该调 用所获得的返回值。有关具体的工作原理的讨论,请参 阅Applying
the Factory Pattern to RMI《将工厂方式应用到RMI》一书 。
远程对象一旦在服务器上注册,则调用程序就能按照 名字搜索对象,获得远程对象的引用,再远程调用该对 象的方法。
例如,下列语句将名字"HelloServer"关联到远程对象的 引用:
Naming.rebind("//myhost/HelloServer", obj);
请注意以下对有关rebind方法的参数的说明:
- 第一个参数是URL格式的java.lang.String,表示远程对象的 地址和名字。
- 您需要将myhost的值修改为您的服务器名或IP地址。否则 ,如果忽略了URL中的主机名, 将采用当前主机作为 缺省值,而在URL中不必规定协议:如"HelloServer"。
- 可以在URL中提供端口号作为一个选项:如"//myhost:1234/HelloServer" 。端口号缺省值为1099。只有当服务器在不是缺省端口
1099的端口上创建了注册表时才需要规定端口号。
- 第二个参数是将在其上调用远程方法的对象实现的引用 。
- 在obj参数所规定的实际远程对象引用中,RMI运行时替 代向远程对象的存根的引用。诸如HelloImpl的实例等远程 实现对象永远不会离开创建它们的虚拟机,所以,当一
台客户机在服务器的远程对象注册表中进行搜索时,就 返回一个包含实现存根的对象。
考虑到安全原因,一个应用程序仅可以与在同一主 机上运行的注册表进行关联或解除关联。这样可以防止 客户机删除或重写服务器远程注册表中的任何项目。但
可以从任何主机进行搜索。
<编写使用远程服务的 客户机程序
分布式Hello World例程中的小应用程序远程调用sayHello 方法,以便得到字符串"Hello World!",而此字符串在该小 应用程序运行时被显示。该小应用程序的代码如下:
package examples.hello;
import java.applet.Applet;
import java.awt.Graphics;
import java.rmi.Naming;
import java.rmi.RemoteException;
public class HelloApplet extends Applet {
String message = "blank";
// "obj" is the identifier that we"ll use to refer
// to the remote object that implements the "Hello"
// interface
Hello obj = null ;
public void init() {
try {
obj
= (Hello)Naming.lookup("//" +
getCodeBase().getHost() + "/HelloServer");
message
= obj.sayHello();
} catch (Exception e) {
System.out.println("HelloApplet
exception: " +
e.getMessage());
e.printStackTrace();
}
}
public void paint(Graphics g) {
g.drawString(message, 25,
50);
}
}
- 首先,该小应用程序从服务器主机的rmiregistry中获得远 程对象实现(声明为"HelloServer")的一个引用。与Naming.rebind
方法相同,Naming.lookup方法也采用URL格式的java.lang.String 。在本例中,小应用程序通过使用getCodeBase方法和getHost
方法构造URL串。Naming.lookup可完成下列任务:
- 构造注册表存根实例(以便连接服务器的注册表),使用 主机名和端口号作为向Naming.lookup提供的参数
- 使用注册表存根调用注册表上的远程lookup方法,使用URL 的名称部分("HelloServer")
- 注册表返回与该名称相关联的helloImpl_Stub实例
- 接收远程对象实现(HelloImpl)存根实例并从CLASSPATH或存根 的代码库(codebase)中装载存根类(examples.hello.HelloImpl_Stub)
- Naming.lookup将存根返回给调用程序(HelloApplet)
- 小应用程序调用服务器远程对象的远程sayHello方法
- RMI序列化应答字符串"Hello World!"并返回
- RMI使字符串解除顺列化,并将其存储在名为message的变 量中
- 小应用程序调用paint方法,使字符串"Hello World!"在小应 用程序的绘图区显示出来。
构造后要作为参数传递给Naming.lookup方法的URL字符 串必须包含服务器的主机名。否则,小应用程序就会按 照缺省方式向客户机搜索,而AppletSecurityManager就会抛出
一个例外,因为小应用程序不能存取本地系统,而只能 与小应用程序的主机进行通信。
引用Hello World小应用程序的主页的HTML语句如下:
$#@60;HTML>
$#@60;title>Hello World$#@60;/title>
$#@60;center> $#@60;h1>Hello World$#@60;/h1> $#@60;/center>
The message from the HelloServer is:
$#@60;p>
$#@60;applet codebase="myclasses/"
code="examples.hello.HelloApplet"
width=500 height=120>
$#@60;/applet>
$#@60;/HTML>
请注意:
- 您希望从中下载Java类(classes)的计算机上必须有HTTP服 务程序运行。
- 本例中的codebase在主页本身被下载的目录下指定一个子 目录。使用这种相对路径通常是比较好的方法。例如, 如果被小应用程序的HTML所引用的codebase目录(其中包含
小应用程序的类文件)是在HTML目录以上的目录,则您可 能就需要使用诸如“../”的相对路径。
- 小应用程序的code属性规定了小应用程序的完全合格的 包名字,在本例中为examples.hello.HelloApplet:
code="examples.hello.HelloApplet" <
类文件和HTML文 件的编译和部署
Hello World例子中的源代码现在已经完成,$HOME/mysrc/examples/hello 目录中有以下4个文件:
- 包含Hello远程接口源代码的Hello.java。
- 作为HelloImpl远程对象实现源代码及Hello World小应用程序 服务器的HelloImpl.java。
- 小应用程序的源代码HelloAplet.java。
- 作为引用Hello World小应用程序的主页hello.html。
在本节中,您将对.java源文件进行编译并创建.class文件 。随后,您将运行rmic编译程序以创建存根和框架。存 根是客户机端将RMI调用传送给服务器端调度程序的远程
对象代理,而服务器端的调度程序又反过来将调用传送 给实际的远程对象实现。
在使用javac和rmic编译器时,您必须规定所产生的类 文件应该存储在什么地方。对于小应用程序,所有文件 均应驻留在小应用程序的代码库(codebase)目录中。在本
例中,这个目录就是$HOME/public_html/muclasses。
某些Web服务器允许通过构造“http://host/'username/” 这样的HTTP URL存取用户的public_html目录。如果您的Web服
务器不支持这种方式,则您可使用格式为"file://home/username/public_html" 的URL文件。
本节完成以下4项任务:
- Java源文件的编译
- 使用rmic创建存根和框架
- 将HTML文件移动到部署目录
- 为运行设置路径
<Java源文件的编译
在进行编译之前,必须确保部署目录$HOME/public_html/myclassess 和开发目录$HOME/mysrc/examples/hello能够通过计算机的CLASSPATH
进行存取。
要对Java源文件进行编译,则应运行下列javac指令:
javac -d $HOME/public_html/myclassess Hello.java HelloImpl.java
HelloApplet.java
此指令在目录$HOME/public_html/myclasses内创建目录examples/hello (如果它不存在的话)。随后,然后将文件Hello.class、HelloImpl.class
和helloApplet.class写入该目录。这三个文件分别是远程接 口、实现和小应用程序。有关javac选项的说明,可参阅 Solaris
的javac指南或Win32
javac指南。
使用rmic创建框架和存根
要创建存根和框架文件,可对包含my.package.MyImpl等远 程对象实现的编译好的类文件(.class)的完全合格的包名 字运行编译程序rmic。rmic指令以一个或多个类名字作为
参数,并以MyImpl_Skel.class和MyImpl_Stub.class的格式产生类 文件。
按照缺省设置,在JDK1.2中,rmic在运行时,-vcompat标 志保持打开(on),利用该标志可生成支持存取下列对象的 存根和框架:
- 来自1.1客户端的Unicast (非Activatable )远程对象
- 来自1.2客户端的所有类型的远程对象
如果您只需要支持1.2的客户端,则rmic可以-v1.2选项运 行。有关rmic选项的详细介绍,请参阅Solaris rmic指南或Win32 rmic指南。
例如,要为HelloImpl远程对象实现创建存根和框架,可 按照如下方式运行rmic:
rmic -d $HOME/public_html/myclasses examples.hello.HelloImpl
-d选项表示存放编译后的存根和框架类文件的根目录 。所以,上述指令可在目录$HOME/public_html/myclasses/examples/hello
中创建下列文件:
HelloImpl_Stub.class
HelloImpl_Skel.class
所创建的存根类实现了与远程对象完全相同的远程接口 集。即,客户机可使用Java语言内置的运算符进行运算 和类型检查。这还表明,Java远程对象支持真正面向对
象的多机组合形式。
<将HTML文件移动到部署目录
要使客户端能看到引用小应用程序的主页,就必须把 hello.html文件从开发目录移动到小应用程序的codebase目 录中。例如:
mv $HOME/mysrc/examples/hello/hello.html $HOME/public_html/
为运行设置路径
在运行HelloImpl服务程序时,必须确保能够通过服务 器的CLASSPATH存取$HOME/public_html/codebase目录。
启动RMI注册表、 服务程序和小应用程序
本节将完成以下三项任务:
- 启动RMI注册表
- 启动服务程序
- 运行小应用程序
启动RMI注册表
RMI注册表是允许远程客户机获得向一个远程对象的 引用的、简单的、服务端的名称服务器。它一般只用来 寻找应用程序需要与之对话的第一个远程对象。随后,
该对象又可提供与应用程序相关的支持以寻找其他对象 。
注意: 在启动rmiregistry之 前,您必须确定您将在其上运行registry的平台或窗口没
有设置CLASSPATH,或者在CLASSPATH 中不包含指向您希望 向您的客户机下载任何类的路径,这其中包括您的远程 对象实现类的存根。
如果在您启动rmiregistry之后,它能在其CLASSPATH中找 到您的存根类,则它将忽略服务器的java.rmi.server.codebase
属性,因此,您的客户机就不能为您的远程对象下载存 根代码。
要在服务器上启动registry,可执行rmiregistry指令。此 指令不产生输出,而且一般只在后台运行。有关rmiregistry 的详细介绍,请参阅Solaris
rmiregistry指南或Win32
rmiregistry指南。
例如,在Solaris平台上:
rmiregistry &
又如,在Windows 95或Windows NT平台上:
start rmiregistry
(如果不存在start指令,可使用javaw )。
注册表运行的缺省端口是1099。要在另一个端口上启 动注册表,可从指令行指定端口号。例如,要在Windows NT系统的端口2001上启动注册表:
start rmiregistry 2001
如果注册表在非缺省的端口上运行,您就需要在对注 册表进行调用时传递给java.rmi.Naming类的基于URL的名字 中指定端口号。例如,在Hello
World例子中如果注册表在 端口2001上运行,则将HelloServer的URL与远程对象引用产 生关联的调用就会是:
Naming.rebind("//myhost:2001/HelloServer", obj);
每次对远程接口进行修改后,或在远程对象实现中 使用了修改/添加的远程接口后,都必须先停止注册表 ,然后再重新启动。否则,在注册表中关联的对象引用
类型就会与修改后的类不相符。
<启动服务程序
在启动服务程序时,必须指定java.rmi.server.codebase属 性,这样才能动态地将存根类下载到注册表及客户机。 运行服务程序,将codebase属性设置为实现存根的地点。
因为codebase属性只能引用一个目录,所以必须确定在被 java.rmi.server.codebase所引用的目录中已经安装了需要下载 的所有类。
欲知有关每个java.rmi.server属性的说明,请
点按这里。要察看所有java.rmi.activation属性,请
点按这里。有关java选项的详细介绍,请参阅Solaris
java指南或Win32
java指南。如果在运行本例程序时发生问题,请参阅 RMI
and Serialization FAQ ( RMI及系列化问答)。
注: 只有当本地计算机不存在存根类,且 已经在类文件所驻留的服务器上正确设置了java.rmi.server.codebase
属性时,才将存根类动态地下载到客户机的虚拟机上。
同一个指令行需要包含4项内容:"java"指令、两个属 性name = value对(对于codebase属性,注意从“-D”直 到“/”都没有空格),服务器程序完全合格的包名字。
在单词"java"之后应该有一个空格,在两个属性之间也应 该有空格,在单词"examples" (在浏览器或纸面上如果以文 本方式查阅,这个单词很难看清楚)之前也有空格。下
列指令表示如何启动HelloImpl服务程序,指定java.rmi.server.codebase 和java.security.policy属性:
java -Djava.rmi.server.codebase = http://myhost/'myusername/myclasses/
-Djava.security.policy=$HOME/mysrc/policy examples.hello.HelloImpl
为了在您的系统上运行这个程序,您需要将policy文件 的地址修改为您系统中已经安装了本例源代码的目录。 注:在本例中,为简单起见,我们将使用允许任何
地方的任何人访问的策
略文件。不要在运行环境中使用这一策略文件。 有关如何使用java.security.policy文件正确打开许可的详细 介绍,请参阅以下文件:
http://java.sun.com/products/jdk/1.2/docs/guide/
security/PolicyFiles.html
http://java.sun.com/products/jdk/1.2/docs/guide/
security/permission.html
codebase属性将设置为URL,所以,其格式应该是"http://aHost/somesource/" 或"file:/myDirectory/location/",或者根据某些操作系统的要
求,"file:///myDirectory/location/" ("file"单词后应该有三个斜 杠)。
请注意,本例中URL字符串的结尾都有“/”符号。这 个结尾斜杠是java.rmi.server.codebase属性对URL地址的要求 ,这样,实现才能正确地找到您的类定义。
如果您忘记了在codebase属性后面写上结尾斜杠,或者 如果在资源中找不到类文件(这些文件尚未准备好下载 ),或者如果您拼写错了属性名称,您将得到java.lang.ClassNotFoundException
例外。当您试图将远程对象关联到rmiregistry时,或者当 第一台客户机试图存取对象的存根时,就会抛出这样的 例外。如果后一种情形发生,您就会遇到另一个问题,
因为rmiregistry正在其CLASSPATH中寻找存 根。
输出结果应该如下:
HelloServer bound in registry
<