# 15.8 远程方法
为通过网络执行其他机器上的代码,传统的方法不仅难以学习和掌握,也极易出错。思考这个问题最佳的方式是:某些对象正好位于另一台机器,我们可向它们发送一条消息,并获得返回结果,就象那些对象位于自己的本地机器一样。Java 1.1的“远程方法调用”(RMI)采用的正是这种抽象。本节将引导大家经历一些必要的步骤,创建自己的RMI对象。
## 15.8.1 远程接口概念
RMI对接口有着强烈的依赖。在需要创建一个远程对象的时候,我们通过传递一个接口来隐藏基层的实现细节。所以客户得到远程对象的一个引用时,它们真正得到的是接口引用。这个引用正好同一些本地的根代码连接,由后者负责通过网络通信。但我们并不关心这些事情,只需通过自己的接口引用发送消息即可。
创建一个远程接口时,必须遵守下列规则:
(1) 远程接口必须为`public`属性(不能有“包访问”;也就是说,它不能是“友好的”)。否则,一旦客户试图装载一个实现了远程接口的远程对象,就会得到一个错误。
(2) 远程接口必须扩展接口`java.rmi.Remote`。
(3) 除与应用程序本身有关的异常之外,远程接口中的每个方法都必须在自己的throws从句中声明`java.rmi.RemoteException`。
(4) 作为参数或返回值传递的一个远程对象(不管是直接的,还是在本地对象中嵌入)必须声明为远程接口,不可声明为实现类。
下面是一个简单的远程接口示例,它代表的是一个精确计时服务:
```
//: PerfectTimeI.java
// The PerfectTime remote interface
package c15.ptime;
import java.rmi.*;
interface PerfectTimeI extends Remote {
long getPerfectTime() throws RemoteException;
} ///:~
```
它表面上与其他接口是类似的,只是对`Remote`进行了扩展,而且它的所有方法都会“抛”出`RemoteException`(远程异常)。记住接口和它所有的方法都是`public`的。
## 15.8.2 远程接口的实现
服务器必须包含一个扩展了`UnicastRemoteObject`的类,并实现远程接口。这个类也可以含有附加的方法,但客户只能使用远程接口中的方法。这是显然的,因为客户得到的只是指向接口的一个引用,而非实现它的那个类。
必须为远程对象明确定义构造器,即使只准备定义一个默认构造器,用它调用基类构造器。必须把它明确地编写出来,因为它必须“抛”出`RemoteException`异常。
下面列出远程接口`PerfectTime`的实现过程:
```
//: PerfectTime.java
// The implementation of the PerfectTime
// remote object
package c15.ptime;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.*;
public class PerfectTime
extends UnicastRemoteObject
implements PerfectTimeI {
// Implementation of the interface:
public long getPerfectTime()
throws RemoteException {
return System.currentTimeMillis();
}
// Must implement constructor to throw
// RemoteException:
public PerfectTime() throws RemoteException {
// super(); // Called automatically
}
// Registration for RMI serving:
public static void main(String[] args) {
System.setSecurityManager(
new RMISecurityManager());
try {
PerfectTime pt = new PerfectTime();
Naming.bind(
"//colossus:2005/PerfectTime", pt);
System.out.println("Ready to do time");
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
在这里,`main()`控制着设置服务器的全部细节。保存RMI对象时,必须在程序的某个地方采取下述操作:
(1) 创建和安装一个安全管理器,令其支持RMI。作为Java发行包的一部分,适用于RMI唯一一个是`RMISecurityManager`。
(2) 创建远程对象的一个或多个实例。在这里,大家可看到创建的是`PerfectTime`对象。
(3) 向RMI远程对象注册表注册至少一个远程对象。一个远程对象拥有的方法可生成指向其他远程对象的引用。这样一来,客户只需到注册表里访问一次,得到第一个远程对象即可。
(1) 设置注册表
在这儿,大家可看到对静态方法`Naming.bind()`的一个调用。然而,这个调用要求注册表作为计算机上的一个独立进程运行。注册表服务器的名字是`rmiregistry`。在32位Windows环境中,可使用:
```
start rmiregistry
```
令其在后台运行。在Unix中,使用:
```
rmiregistry &
```
和许多网络程序一样,`rmiregistry`位于机器启动它所在的某个IP地址处,但它也必须监视一个端口。如果象上面那样调用`rmiregistry`,不使用参数,注册表的端口就会默认为1099。若希望它位于其他某个端口,只需在命令行添加一个参数,指定那个端口编号即可。对这个例子来说,端口将位于2005,所以`rmiregistry`应该象下面这样启动(对于32位Windows):
```
start rmiregistry 2005
```
对于Unix,则使用下述命令:
```
rmiregistry 2005 &
```
与端口有关的信息必须传送给`bind()`命令,同时传送的还有注册表所在的那台机器的IP地址。但假若我们想在本地测试RMI程序,就象本章的网络程序一直测试的那样,这样做就会带来问题。在JDK 1.1.1版本中,存在着下述两方面的问题(注释⑦):
(1) `localhost`不能随RMI工作。所以为了在单独一台机器上完成对RMI的测试,必须提供机器的名字。为了在32位Windows环境中调查自己机器的名字,可进入控制面板,选择“网络”,选择“标识”卡片,其中列出了计算机的名字。就我自己的情况来说,我的机器叫作`Colossus`(因为我用几个大容量的硬盘保存各种不同的开发系统——`Clossus`是“巨人”的意思)。似乎大写形式会被忽略。
(2) 除非计算机有一个活动的TCP/IP连接,否则RMI不能工作,即使所有组件都只需要在本地机器里互相通信。这意味着在试图运行程序之前,必须连接到自己的ISP(因特网服务提供者),否则会得到一些含义模糊的异常消息。
⑦:为找出这些信息,我不知损伤了多少个脑细胞。
考虑到这些因素,`bind()`命令变成了下面这个样子:
```
Naming.bind("//colossus:2005/PerfectTime", pt);
```
若使用默认端口1099,就没有必要指定一个端口,所以可以使用:
```
Naming.bind("//colossus/PerfectTime", pt);
```
在JDK未来的版本中(1.1之后),一旦改正了`localhost`的问题,就能正常地进行本地测试,去掉IP地址,只使用标识符:
```
Naming.bind("PerfectTime", pt);
```
服务名是任意的;它在这里正好为`PerfectTime`,和类名一样,但你可以根据情况任意修改。最重要的是确保它在注册表里是个独一无二的名字,以便客户正常地获取远程对象。若这个名字已在注册表里了,就会得到一个`AlreadyBoundException`异常。为防止这个问题,可考虑坚持使用`rebind()`,放弃`bind()`。这是由于`rebind()`要么会添加一个新条目,要么将同名的条目替换掉。
尽管`main()`退出,我们的对象已经创建并注册,所以会由注册表一直保持活动状态,等候客户到达并发出对它的请求。只要`rmiregistry`处于运行状态,而且我们没有为名字调用`Naming.unbind()`方法,对象就肯定位于那个地方。考虑到这个原因,在我们设计自己的代码时,需要先关闭`rmiregistry`,并在编译远程对象的一个新版本时重新启动它。
并不一定要将`rmiregistry`作为一个外部进程启动。若事前知道自己的是要求用以注册表的唯一一个应用,就可在程序内部启动它,使用下述代码:
```
LocateRegistry.createRegistry(2005);
```
和前面一样,2005代表我们在这个例子里选用的端口号。这等价于在命令行执行`rmiregistry` 2005。但在设计RMI代码时,这种做法往往显得更加方便,因为它取消了启动和中止注册表所需的额外步骤。一旦执行完这个代码,就可象以前一样使用`Naming`进行“绑定”——`bind()`。
## 15.8.3 创建根与干
若编译和运行`PerfectTime.java`,即使`rmiregistry`正确运行,它也无法工作。这是由于RMI的框架尚未就位。首先必须创建根和干,以便提供网络连接操作,并使我们将远程对象伪装成自己机器内的某个本地对象。
所有这些幕后的工作都是相当复杂的。我们从远程对象传入、传出的任何对象都必须`implement Serializable`(如果想传递远程引用,而非整个对象,对象的参数就可以`implement Remote`)。因此可以想象,当根和干通过网络“汇集”所有参数并返回结果的时候,会自动进行序列化以及数据的重新装配。幸运的是,我们根本没必要了解这些方面的任何细节,但根和干却是必须创建的。一个简单的过程如下:在编译好的代码中调用`rmic`,它会创建必需的一些文件。所以唯一要做的事情就是为编译过程新添一个步骤。
然而,`rmic`工具与特定的包和类路径有很大的关联。`PerfectTime.java`位于包`c15.Ptime`中,即使我们调用与`PerfectTime.class`同一目录内的`rmic`,`rmic`都无法找到文件。这是由于它搜索的是类路径。因此,我们必须同时指定类路径,就象下面这样:
```
rmic c15.PTime.PerfectTime
```
执行这个命令时,并不一定非要在包含了`PerfectTime.class`的目录中,但结果会置于当前目录。
若`rmic`成功运行,目录里就会多出两个新类:
```
PerfectTime_Stub.class
PerfectTime_Skel.class
```
它们分别对应根(`Stub`)和干(`Skeleton`)。现在,我们已准备好让服务器与客户互相沟通了。
## 15.8.4 使用远程对象
RMI全部的宗旨就是尽可能简化远程对象的使用。我们在客户程序中要做的唯一一件额外的事情就是查找并从服务器取回远程接口。自此以后,剩下的事情就是普通的Java编程:将消息发给对象。下面是使用`PerfectTime`的程序:
```
//: DisplayPerfectTime.java
// Uses remote object PerfectTime
package c15.ptime;
import java.rmi.*;
import java.rmi.registry.*;
public class DisplayPerfectTime {
public static void main(String[] args) {
System.setSecurityManager(
new RMISecurityManager());
try {
PerfectTimeI t =
(PerfectTimeI)Naming.lookup(
"//colossus:2005/PerfectTime");
for(int i = 0; i < 10; i++)
System.out.println("Perfect time = " +
t.getPerfectTime());
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
ID字符串与那个用`Naming`注册对象的那个字符串是相同的,第一部分指出了URL和端口号。由于我们准备使用一个URL,所以也可以指定因特网上的一台机器。
从`Naming.lookup()`返回的必须转换到远程接口,而不是到类。若换用类,会得到一个异常提示。
在下述方法调用中:
```
t.getPerfectTime( )
```
我们可看到一旦获得远程对象的引用,用它进行的编程与用本地对象的编程是非常相似(仅有一个区别:远程方法会“抛”出一个`RemoteException`异常)。
## 15.8.5 RMI的替选方案
RMI只是一种创建特殊对象的方式,它创建的对象可通过网络发布。它最大的优点就是提供了一种“纯Java”方案,但假如已经有许多用其他语言编写的代码,则RMI可能无法满足我们的要求。目前,两种最具竞争力的替选方案是微软的DCOM(根据微软的计划,它最终会移植到除Windows以外的其他平台)以及CORBA。CORBA自Java 1.1便开始支持,是一种全新设计的概念,面向跨平台应用。在由Orfali和Harkey编著的《Client/Server Programming with Java and CORBA》一书中(John Wiley&Sons 1997年出版),大家可获得对Java中的分布式对象的全面介绍(该书似乎对CORBA似乎有些偏见)。为CORBA赋予一个较公正的对待的一本书是由Andreas Vogel和Keith Duddy编写的《Java Programming with CORBA》,John Wiley&Sons于1997年出版。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物