EJB 錯誤?不要慌!
您已經在自己所鐘愛的 Java 書籍中讀過了關於企業 Javabean 技術的那一章,也已經練習過了簡單的 HelloWorld bean,並遵循所建議的部署過程發布了它。現在您得編寫一個客戶機,以便通過這個客戶機來調用這個傑作。因此您寫出了類似清單 1 中的代碼:
清單 1. 一個調用 bean 的非常簡單的客戶機
1 InitialContext ic = new InitialContext();在命令行中運行這個客戶機,使用手頭最方便的一個 Java 安裝 ―― 即應用服務器 href="http://server.it168.com/" target=_blank>服務器使用的那一個。所有事情都很完美!帶著成功的喜悅,您轉移到第二台計算機上運行您的客戶機。這回,您得到了一個可怕的錯誤消息。首先,您可能得到 java.lang.NoClassDefFoundError: Javax/ejb/EJBObject ,然後是一大堆其他的 NoClassDefFoundError s,因為您忘記提交一個帶有必需的 stub 和 tIE 的 JAR 文件,並且沒有提供或者考慮到其他各種 EJB 相關的內容。不過最終,您的客戶機運行到了第一行有意思的代碼( InitialContext ic = new InitialContext(); )。在到達這一行時得到的異常 ―― 您幾乎肯定會得到一個異常 ―― 將會根據您所選擇的特定 上下文 provider而有所不同。
解釋這些術語
在我們繼續往下之前,定義幾個術語會很有幫助。計算世界使用的都是一些奇怪的術語、時髦的語匯和首字母縮寫詞,Java 技術也不例外(也許這應該是 JavaIsNoException ?)。如果您遇到了上面所說的問題,那麼這裡面的術語可能會讓您感到有些無所適從。所以讓我們討論在本文中將會遇到的術語,搞明白它們的意思是一個好主意。
名稱空間、上下文、初始上下文和子上下文 這些術語都是有關位置的 ―― 是從客戶機的角度看時 EJB 組件所在的概念性的位置。將一個 名稱空間 想像為一個城鎮,城鎮中的商店由 EJB home接口(我們將在稍後討論它)表示。 上下文是城鎮中的一個位置。 初始上下文 是您開始時所在的位置 ―― 就像它是到城鎮的道路。而 子上下文是街道名。
home接口(home interface)和遠程接口(remote interface) 企業 JavaBean 組件有三個部分。首先是 bean 代碼本身。然後是 home接口,它定義了創建您自己的 EJB bean 的方法。home接口是在名稱空間中發布的。當您有了home接口後,就可以調用 Create() 以從應用服務器獲得遠程接口。獲得了遠程接口後,就可以調用構成實際的 EJB 代碼的方法了。
如何將這些術語應用到您的城鎮模擬中去呢?到達正確的城鎮並找到正確的地址後,您需要走進商店或者按鈴(調用 Create() )。這個過程對於您要去的所有商店都是一樣的,不過,您所收到的響應取決於是由誰來提供服務 ―― 比如是一位屠夫、一位面包師還是一位燭台制作者。這個響應代表了 遠程接口。每個人都是不同的並且可以要求他提供不同的東西。您必須知道與您交談的人(即 bean)的職業才能提出正確的問題(即調用正確的方法) ―― 向一位屠夫要一條面包可不妥當。
CosNaming、LDAP 和 JNDI Java 命名和目錄接口(Java Naming and Directory Interface JNDI)提供了一個標准接口,它指明您需要如何與名稱空間交互。我們所提到的 LDAP和 CosNaming 就是 JDNI 名稱空間類型。現在擴展我們的比喻:JNDI 是城鎮的模板,而 CosNaming 和 LDAP 是特定的城鎮。它們以相似的方式操作,但是有不同的布局。
屬性提供了一個映射
讓我們看一看如何使用所有這些元素以成功地從遠程計算機上調用我們的 EJB 組件上的方法。為了讓客戶程序連接到您精心打造的 EJB 組件,需要幾樣東西。首先,它需要客戶代碼的所有 JAR 文件、一般性的 EJB 相關 JAR 文件如 J2EE.jar 以及在部署 bean 時生成的 stub 和 tIE。這些文件讓您的客戶機可以一直到達初始上下文。
接下來您的客戶機需要的信息是一些屬性的值。首先,您將需要幾個 Java.naming.factory.initial 的值。該屬性指向一個提供初始上下文工廠的類。該屬性的一個典型值是 com.sun.jndi.cosnaming.CNCtxFactory ,這也是我們在這裡的幾個例子中所使用的值。這個類存在於 rt.jar 中,因而它是基本 JVM 的一部分。工廠是由 CosNaming 命名服務器所使用的,但是 JVM 還包括一個 LDAP 工廠。我們在後面將會看到,不同的應用服務器提供它們自己的初始上下文工廠。
這個類連同命名服務器 URL 和端口號的詳細信息,用於生成與名稱空間交互的 InitialContext 類。不過,如果沒有 provider URL,那麼它將連接到 localhost 的 900 端口(或者您的上下文工廠的其他默認端口)。要連接到遠程服務器,您需要有屬性 Java.naming.provider.url 的一個值。
新程序員對於所有這些覺得很難理解的原因是:不管您在應用服務器本地運行任何東西,這東西通常都會聽話地工作。這是由於環境照管了一切,當您要求一個 InitialContext 時,環境就會給您提供您想要的那個。但是當您將客戶即轉移到不同的計算機上時,就得靠自己了。您需要知道拷貝哪一個 JAR 文件,以及要做哪些設置。我知道有些人為使他們的客戶機正確工作,將應用服務器上的所有 JAR 文件都拷貝到第二台計算機上!
在默認情況下, InitialContext 工廠是在 jndi.propertIEs 中定義的,這個工廠類有默認的服務器 URL 和端口號默認值。這個文件在類路徑中(這一般意味著在本地目錄)或者在您的類路徑中的任何 JAR 中。不同的應用服務器可能在不同的 JAR 文件中提供它們的默認值,WebSphere Application Server 在 namingclIEnt.jar 中儲存一個默認副本。要指定您自己的默認值,只需要編輯在類路徑中的第一個副本。這是配置屬性的一種方法,如果缺少命令行或者代碼驅動的設置,那麼客戶機將使用 jndi.propertIEs 中的值。不過,雖然這可能適合於簡單的設置,但是如果處理多個服務器和名稱空間,那麼您可能希望一個客戶一個客戶地進行配置。
這些屬性是如何根據我們要使用的名稱空間而使用不同的值的呢?正如前面提到的,有兩種形式的 JNDI 名稱空間:CosNaming 和 LDAP。其中每一個都有與之相關聯的傳輸:分別是 IIOP 和 LDAP。一個 LDAP 名稱空間使用 LDAP 傳輸(您將用一個像 ldap://myldapnameserver 這樣的 URL 連接到它),而 CosNaming 使用一個 IIOP 傳輸(您將用一個像 iiop://mycosnamingserver 這樣的 URL 連接到它)。CosNaming 的默認端口號是 900,而 LDAP 的默認端口號是 389。不過,任何給定的名稱空間服務器實現使用的默認值可能是不同的。
用命令行配置屬性
讓我們看一下如何用命令行配置屬性。如果您要在家裡自己練習,進入 JDK 安裝中的 bin 目錄。在這個文件夾中,可以找到一個名為 tnameserv.exe 的程序(對於 Windows)或者只是 tnameserv (對於基於 UNIX 的系統)。通過執行這個程序將會在端口 900 啟動一個示例 CosNmaing 命名服務器。
現在正好可以用一個可以查看 CosNaming 名稱空間的實用工具來裝備您自己。我本人使用 Eclipse 作為開發環境,我在下面的 參考資料 部分中提供了到 JNDI 浏覽器插件的鏈接。理論上,您應該可以將一個名稱空間浏覽器指向自己計算機的端口 900,並看到一個非常無聊的空名稱空間(盡管一些應用服務器在默認情況下會用很多不同的內容填充名稱空間)。為了豐富我們的名稱空間,我們現在將編寫一個簡單的程序以在它裡面放一些內容,如清單 2 所示:
清單 2. 一個簡單的 cosNaming 名稱空間交互
1 package example.publisher;
2
3 import Javax.naming.InitialContext;
4
5 public class Publish {
6
7 public static void main(String[] args) {
8 //
9 //This example creates a subcontext in a namespace
10 //
11 try{
12 InitialContext ic = new InitialContext();
13 ic.createSubcontext("Test");
14 }catch(Exception e){
15 System.out.println(e);
16 e.printStackTrace();
17
18 }
19 }
20 }
21
這個應用程序將假定為得到正確的初始上下文件所需的所有屬性都是可用的。所以現在可以從命令行運行它並在運行時提供這些屬性(其中 URL 要根據您的環境作調整):
1 Java -DJava.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory
2 -DJava.naming.provider.url=iiop://myMachine:900
3 example.publisher.Publish
4
一切正常,我們的客戶會找到示例名稱空間的上下文並創建名為 Test 的子上下文。您可以用名稱空間浏覽器確認這一點。
現在試著在一台計算機上運行命名服務器,用同一個命令行(當然,對 URL 再次做了調整)在另一台計算機上運行清單 2 中的應用程序。它運行起來應該沒有問題(您可能需要修改這個例子以改變所限定的內容,甚至刪除子上下文而不是創建它,這樣在第二次運行時您就可以確信它已經起過作用了)。
在應用程序中配置屬性
那麼,如果不希望在命令行中設置這些屬性怎麼辦?還有另外一個方法。可以在程序中顯式地聲明這些屬性。這意味著您不需要為 Java 命令提供特殊的選項。改變清單 2 中的代碼以顯式地設置所需要的屬性後,它看起來與清單 3 中的代碼一樣:
清單 3. 簡單的 cosNaming 名稱空間交互,在應用程序代碼中設置屬性
1 package example.publisher;
2
3 import Javax.naming.InitialContext;
4
5 public class Publish {
6
7 public static void main(String[] args) {
8 //
9 //This example creates a subcontext in a namespace
10 //
11 try{
12 PropertIEs prop = new PropertIEs();
13 prop.setProperty("Java.naming.factory.initial",
14 "com.sun.jndi.cosnaming.CNCtxFactory");
15 prop.setProperty("Java.naming.provider.url",
16 "iiop://myMachine:900");
17 InitialContext ic = new InitialContext(prop);
18 ic.createSubcontext("Test");
19 }catch(Exception e){
20 System.out.println(e);
21 e.printStackTrace();
22
23 }
24 }
25 }
26
現在這個程序不再需要長長的命令行配置,不過要記住,以這種方式編寫的應用程序硬編碼了這些設置。
尋找通往 bean 的道路
到目前為止,我們已經看到了幾個可以證明我們已連接到遠程名稱空間並完成一些任務的例子,盡管這些任務是相當無聊的 ―― 創建一個子上下文。在實際中,一般是由工具來為您完成所有的創建和發布工作,您 真正 需要的做是查找一個對象。在這一節,我們將在 CosNaming 名稱空間中獲得已發布的 HelloWorld bean 的 Home 接口。然後我們再看一下如何在 LDAP 名稱空間中找到它的 Home 接口。
為了說明問題,我們假設您已經部署了 HelloWorld bean,它的home接口 HelloWorldHome 發布在 example/HelloWorldHome 。
在上一節,我們進行了連接到命名服務器的艱苦工作,現在我們所需要的就只是查詢 EJB 組件了。這需要我們向查詢方法傳遞一個字符串,它表示從 InitialContext (您在城鎮中的出發點)到想要去的 HomeInterface (房屋或者商店)的方向。聽起來簡單 ―― 但是這裡您所選擇的特定上下文工廠就要產生影響了。像 WebSphere 這樣的應用服務器所帶的工廠類並不總是把您放到名稱空間的根上。所以我們為了查詢 HomeInterface 而需要的字符串會根據 InitialContext 將您所放到城鎮中的位置而變化。並且,在本地服務器上,上下文工廠可能將您放到與在遠程服務器上不同的起始位置。
因為這個原因,我建議您不要像在清單 3 中那樣硬編碼所使用的查詢字符串,而是用命令行或者屬性文件傳遞。特別是對於具有多步的體系結構更應如此。例如,您可能有一個調用一個 EJB 組件的客戶機,這個 bean 可能又需要調用也許是在不同的服務器上的第二個 EJB 組件!在這種情況下,屬性應該在每一步中傳遞。這為反復實驗(trial-and-error)查詢提供了一種簡單的機制,並且只需要相對較少的改變就可以得到最終應用程序的靈活性。因此讓我們看一個示例查詢應用程序。在清單 4 中,屬性是在程序中設置的,但是它又以命令行值為依據。這樣命令行與在我們前一個例子中使用的稍有不同,如我們在下面所看到的。
清單 4. 查詢一個home接口
1 package example.lookup;
2 import Java.util.PropertIEs;
3 import Javax.naming.InitialContext;
4 import Javax.rmi.PortableRemoteObject;
5
6 import example.HelloWorld;
7 import example.HelloWorldBean;
8 import example.HelloWorldHome;
9 import Javax.naming.InitialContext;
10
11 public class Lookup {
12
13 public static void main(String[] args) {
14 //
15 //This example looks up the home interface of an EJB to a namespace
16 //
17 try{
18 PropertIEs prop = new PropertIEs();
19 prop.setProperty("Java.naming.factory.initial",args[0]);
20 prop.setProperty("Java.naming.provider.url",args[1]);
21 InitialContext ic = new InitialContext(prop);
22 Object or = ic.lookup(args[2]);
23 if (or != null) {
24 // Narrow the return object to the Home class type
25 HelloWorldHome home =
26 (HelloWorldHome)PortableRemoteObject.narrow(or,
27 HelloWorldHome.class);
28 // Create an EJB object instance using the home interface.
29 HelloWorld hw = home.create();
30 // Invoke the method
31 System.out.Println(hw.hello());
32 }
33 }catch(Exception e){
34 System.out.println(e);
35 e.printStackTrace();
36
37 }
38 }
39 }
40
這個程序是用三個參數調用的:要使用的上下文工廠、provider URL 和包含要查詢的名字的字符串。我們已經知道前兩個是什麼,那麼第三個呢?
如果您仍然使用 tnameserv 作為命名服務器,那麼您很可能將 bean 直接發布到 /example/HelloWorldHome 。在這種情況下,只要將 /example/HelloWorldHome 作為第三個參數傳遞就可以進行成功的查詢。不過,如果您使用的命名服務器有一個更復雜的命名空間,那麼可能會存在由所使用的部署工具增加的額外的層。例如,WebSphere 在默認情況下將 JavaBean 部署到 ejb/ ,但是這不是名稱空間的根,並且只有當使用 WebSphere 的上下文工廠時,通過傳入字符串 /ejb/example/HelloWorldHome 才會使您處於名稱空間中的正確位置。 如果您使用一個與應用服務器提供的不同的上下文工廠(例如在一台只有標准 Java 安裝的計算機上運行客戶機時就需要這樣做)時,這個問題會更加惡化。不過,應用服務器的命名服務器文檔應當說明在查詢 EJB 組件時將會從名稱空間的什麼地方開始。看一下文檔中的例子,再用浏覽器查看名稱空間以確定其客戶機的 InitialContext 會將它們放到什麼地方。名稱空間往往會循環,這樣您就可以試著沿著一個分枝到無限。這意味著從最開始的上下文可以找到一條回家的道路。
總之,下面是傳遞相應參數給清單 4 中的應用程序的命令行:
1 Java Lookup com.sun.jndi.cosnaming.CNCtxFactory
2 iiop://myMachine:900 example/HelloWorldHome
3
在 CosNaming 中,名稱空間子上下文由斜線(/)字符分隔,這與標准 URL 一樣。LDAP 的語法則不同,我們在下面將會看到。
介紹 LDAP
現在讓我們再加上 LDAP。就本文的內容來講,LDAP 是另一個 JNDI 名稱空間,但是它的結構的表示方法與 CosNaming 名稱空間的表示方法截然不同。它還需要一個不同的上下文工廠 ―― 但是這不成問題,因為我們總會在命令行指定正確的工廠(在我們的例子中,我們將使用屬於基本 JVM 一部分的工廠,但是要記住不同的應用服務器可能有自己的工廠)。並且它需要一個指向不同命名服務器的指針 ―― 並且,幸運的是,我們也是在命令行中指定它的。當然,表示home接口位置的字符串是不同的,但是您猜如何?是的,我們還是在命令行中指定它。您可以看到使用這些命令行調用的好處:所有要做的只是改變調用我們的測試程序的方式,理論上我們可以到達任何 JDNI 命名服務器,甚至可以順利地從 CosNaming 轉移到 LDAP 而不用改變任何代碼。是的,這是只是理論,當然無論如何,關鍵是要有正確的參數。
一些命名服務器會保護部分命名空間,這意味著只能發布到允許的區域。假設您有一個運行的 LDAP 服務器,它的細節如下:
URL: ldap://myMachine:1389
BaseDN: c=myldap
LDAP 名稱空間中的樹結構一般來說像下面這樣:
1 ibm-wsnName=MyServer,ibm-wsnName=HelloWorldHome不過,當我們將這個字符串傳遞給程序時,我們需要反轉它(不要問我為什麼)。所以我們使用的字符串看起來是這樣的:
1 ibm-wsnName=HelloWorldHome,ibm-wsnName=MyServer,BaseDN 表示在名稱空間中您希望開始的位置。對於給定的 LDAP 命名服務器來說這可以是很多位置,這取決它是如何構造的。在這個例子中,我們直接到 c=myldap 的根。但是如果我們希望跳到名稱空間中的一個樹,那麼可以指定 ibm-wsnTree=myTree,c=myldap 作為 BaseDN 而不是跳到那一點。
這樣,我們將傳遞給程序的命令行參數就像下面這樣:
1 Java Lookup com.sun.jndi.ldap.LdapCtxFactory ldap://myMachine:1389/c=myldap/這裡我們指定一個 LDAP 上下文工廠 ,然後傳遞 LDAP 服務器的名字以及我們想要開始的位置。然後是到要查詢的 EJB 組件的反轉的路徑。我們可以用這個命令行調用在 CosNaming 例子中使用的同一段代碼( 清單 4)。
當然,本文中使用的代碼沒有理由不能構成一個助手類的一個方法 ―― 它帶三個參數,並且返回 Object or ,這個對象是在試圖做任何事情之前調用 ic.lookup(args[2]) 時返回的。然後,當您需要進行一次查詢時,只需使用這個助手類,向它傳遞適合於當前情況的適當參數,取回您所需要的對象引用,並准備將它窄化到實際的類。( 注意:我不保證這種類的性能,而只是提供這段原本就是如此的代碼,我或者 IBM 對此不作任何保證。)當然,可以通過反射實現一種完全通用的方式,但這會使事情復雜得多,也超出了本文的范圍。
在我們結束之前還有最後一件事要考慮。您可以編寫一個結合了我們在清單 3 和 4 中使用的技術的客戶機。它會檢查命令行中是否給出了一個值;如果有,它就設置這些值,如果沒有,它就使用硬編碼的值。這樣,在程序中可以有有意義的默認值,但是如果需要,也可以用命令行選項覆蓋它們。只需要對代碼進行微不足道的更改。
安全 href="http://safe.it168.com/" target=_blank>安全到家
作為回顧:下面是在有多個 EJB 查詢和多個應用服務器的情況下,要使任何系統運行而應該有或者應該知道的最重要的四件事情:
在任何給定階段,下一階段的所有 stub 和 tIE 都必須在類路徑上。除非環境知道這個類是什麼樣的,否則您不能窄化一個對象以使用它。
每一階段都需要與 EJB 相關的一般性 JAR 文件,如 J2EE.jar 。
以參數的形式傳遞上下文工廠類型、命名服務器名和 JDNI 查詢字符串。以便能夠輕松順應變化。
知道您的名稱空間。記住您的 JDNI 查詢字符串需要將您從在名稱空間中開始的位置移動到您的對象所儲存的位置。但是您並不總是在同一位置開始!用一個工具浏覽名稱空間,並了解在本地和遠程查詢中是從什麼位置開始的。
對於習慣於編寫全部在同一台計算機上執行的代碼的開發人員來說,浏覽遠程名稱空間可能是一個困難的過程。希望本文的提示和代碼可以幫助您設置並運行您的分布式 EJB 應用程序。當您掌握了 JNDI 名稱空間後,再去看一下 developerWorks 上由 Brett McLaughlin 所寫的 EJB 最佳實踐系列,以獲得用於優化代碼的一些很棒的技巧。