# 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 推薦讀物