學習使用RMI
- 編寫Java源代碼和HTML文件的步驟。
- class文件和HTML文件的編譯和部署步驟 。
- 啟動RMI注冊表、服務程序和小應用程序的 步驟。
- Java遠程接口-- Hello.java
- 實現examples.hello.Hello接口的Java遠程對象-- HelloImpl.java
- 調用遠程方法sayHello的Java小應用程序-- HelloApplet.java
- 引用該小應用程序的主頁的HTML代碼-- hello.html
對于本教學課程中所用的所有源代碼,您可選擇以下 列文件格式下載:
- 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 mysrcexamples
mkdir mysrcexampleshello
在這部分要完成三項工作:
- 將遠程類的功能定義為Java接口
- 編寫遠程接口實現和服務器類
- 編寫一個使用遠程服務的客戶程序
在Java中,遠程對象是實現遠程接口的類的實例。您 的遠程接口將聲明您希望遠程調用的每個方法。遠程接 口具有下列特點:
- 遠程接口必須被公開聲明。否則,如果一個客戶機程序 與該遠程接口不在同一個包內,那么該客戶機程序在試 圖裝載實現該遠程接口的一個遠程對象時就會得到一個 錯誤。
- 該遠程接口繼承于java.rmi.Remote接口。
- 除與應用程序相關的某些異常(Exception)之外,每個方法 都必須在其throws子句中聲明java.rmi.RemoteException (或一個 RemoteException的父類)。
- 作為一個參數或返回值傳遞(直接或嵌入在本地對象內 )的任何一個遠程對象的數據類型都必須聲明為該遠程 接口類型(例如: Hello),而不能是實現類(HelloImpl)。
package examples.hello;因為遠程方法調用失敗的方式可能與本地方法調用非常 不同(因與網絡相關的通信問題和服務程序問題),遠程 方法可能會拋出一個java.rmi.RemoteException以報告通信失敗 。如果您希望詳細了解有關分布式系統的失敗和恢復問 題,請閱讀“ A Note on Distributed Computing”(《分布式計算初步》)一 書。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String sayHello() throws RemoteException;
}
<編寫遠程接 口實現和服務器類
實現一個遠程對象類至少必須:
- 聲明它至少實現一個遠程接口
- 為該遠程對象定義構造方法
- 為可被遠程調用的方法提供實 現
在本例中,該main方法是examples.hello.HelloImpl的一部分 。服務器程序必須:
- 創建并安裝一個安全管理器。
- 創建某遠程對象的一個或多個實例 。
- 為了進行引導,在RMI遠程對象 注冊表中至少注冊一個遠程對象。
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
public HelloImpl() throws RemoteException {請注意:
super();
}
- super方法所調用的是輸出遠程對象的java.rmi.server.UnicastRemoteObject 的無參數構造程序。
- 該構造程序必須拋出java.rmi.RemoteException例外,因為如果 沒有通信資源,則RMI在構造期間輸出一個遠程對象的努 力就會失敗。
<為每個遠程方法提供實 現
一個遠程對象的實現類包含實現在遠程接口中所定義 的所有遠程方法的程序代碼。例如,下面就是sayHello方 法的實現,它向調用程序返回字符串"Hello World !":
public String sayHello() throws RemoteException {向遠程方法傳送的參數或來自遠程方法的返回值可以是 包括對象在內的任何Java類型,但條件是這些對象實現 了java.io.Serializable接口。java.lang和java.util中的核心Java 類大多實現了Serializable接口。在RMI中:
return "Hello World !";
}
- 缺省情況下,本地對象通過復制傳輸,除了那些標記為 static或transient的元素以外,一個對象的所有數據元素 (或字段)均被復制傳輸。有關如何改變缺省序列化行為 的介紹,請參閱Java Object Serialization Specification ( Java對象序列化技術規范) 。
- 遠程對象傳輸其引用(reference)。某個遠程對象的一個引 用實際上就是一個存根的引用,而該存根則是該遠程對 象客戶程序方面的代理。RMI 技術規范中對存根有詳細介紹。我們將在本教學課程 中的“使用rmic生成存根及框架”部分創 建它們。
創建和安裝安全管理程序
服務器的main方法首先需要創建和安裝一個安全管理 程序:或者是RMISecurityManager,或者是您自己定義的程序 。例如:
if (System.getSecurityManager() == null) {必須運行安全管理程序,因為它能確保所裝載的類不會 執行不允許執行的操作。如果不指定安全管理程序,那 么除了能在本地CLASSPATH中找到的以外,RMI客戶機或服 務器就不允許裝載其它的類。
System.setSecurityManager (new RMISecurityManager());
}
<創建遠程對象的一個或多個實 例
服務器的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;
- 首先,該小應用程序從服務器主機的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!"在小應 用程序的繪圖區顯示出來。
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);
}
}
引用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目錄以上的目錄,則您可 能就需要使用諸如“http://www.zhujiangroad.com/”的相對路徑。
- 小應用程序的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。
在使用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文件移動到部署目錄
- 為運行設置路徑
在進行編譯之前,必須確保部署目錄$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客戶端的所有類型的遠程對象
例如,要為HelloImpl遠程對象實現創建存根和框架,可 按照如下方式運行rmic:
rmic -d $HOME/public_html/myclasses examples.hello.HelloImpl
-d選項表示存放編譯后的存根和框架類文件的根目錄 。所以,上述指令可在目錄$HOME/public_html/myclasses/examples/hello 中創建下列文件:
HelloImpl_Stub.class所創建的存根類實現了與遠程對象完全相同的遠程接口 集。即,客戶機可使用Java語言內置的運算符進行運算 和類型檢查。這還表明,Java遠程對象支持真正面向對 象的多機組合形式。
HelloImpl_Skel.class
<將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文件正確打開許可的詳細 介紹,請參閱以下文件:
請注意,本例中URL字符串的結尾都有“/”符號。這 個結尾斜杠是java.rmi.server.codebase屬性對URL地址的要求 ,這樣,實現才能正確地找到您的類定義。
如果您忘記了在codebase屬性后面寫上結尾斜杠,或者 如果在資源中找不到類文件(這些文件尚未準備好下載 ),或者如果您拼寫錯了屬性名稱,您將得到java.lang.ClassNotFoundException 例外。當您試圖將遠程對象關聯到rmiregistry時,或者當 第一臺客戶機試圖存取對象的存根時,就會拋出這樣的 例外。如果后一種情形發生,您就會遇到另一個問題, 因為rmiregistry正在其CLASSPATH中尋找存 根。
輸出結果應該如下:
HelloServer bound in registry
<