引言
J2EE 應用程序由多種不同類型的組件構成,例如 servlet、EJB 組件和 J2EE 客戶端 ,這些組件可以封裝在不同的模塊中。隨著 J2EE 應用程序的日趨成熟,也就有必要不斷 使用新的應用程序功能。普遍的做法是通過小規模的增強或者對組件接口(或者其實現) 加以改進來獲得新應用程序功能,而不是對整個企業應用程序進行大規模的改變。因此, 在許多情況下,對每個應用程序組件的不同版本加以支持(同時可以重用其他相關的組件 ,而這些相關的組件不受新版本變化的影響)是人們所期望的事情。
為了實現上述功能,有必要對需要升級的應用程序組件進行增量式,而不是使用一種 “大手筆”的方法對整個企業應用程序進行更換。要想實現這種增量式,就有 必要使同一個應用程序服務器能夠支持企業應用程序組件的多個不同的版本。
本文論述和講解為了支持在一個 WebSphere Application Server 實例中同時托管 J2EE 應用程序的多個版本所需要解決的關鍵問題。盡管其中的一些問題可以在部署階段 解決,但是也有一些問題需要在應用程序設計和開發的階段加以解決。我們在此使用一個 小型的 J2EE 應用程序(由 EJB 組件、servlet、JSP 以及 HTML 構成)對如何解決這些 關鍵問題進行了示范。
有不同的方法可以使企業應用程序從一個版本完全轉換到另外一個版本,例如在同一 WebSphere 網絡域中使用不同的單元。這些方法不在本文的討論范圍之內,本文只在 EJB 組件、servlet 或靜態內容的層面上以較細的粒度論述應用程序的版本。
同時托管的沖突和問題
同時托管的 J2EE 應用程序的不同版本需要使用共同的應用程序服務器資源,如類加 載器、JNDI 名稱空間、應用程序 URL 和其他的外部資源,這些因素會產生沖突。我們將 這些沖突列舉如下(並且在本文的後面將其進行討論)。
類加載沖突
不同版本的 J2EE 組件類需要加載到同一個應用程序服務器的 Java 虛擬機進程中。 通過在 WebSphere 應用程序服務器中使用多種類加載器,可以避免這些沖突。
servlet 路徑沖突
不同版本的 Web 應用程序組件(例如 servlet、JSP 和 HTML 模塊)會有沖突的 URL 。這些沖突可以通過對每個版本使用不同的上下文根來解決。
JNDI 名稱空間沖突
J2EE 應用程序組件可能會引用遠程和(或)本地的持久性對象,這可能包括 EJB 本 地接口。多個版本的 J2EE 應用程序組件不能在同一應用程序服務器域的同一個 JNDI 名 稱空間注冊相同的名稱。解決名稱空間沖突的方法是避免在應用程序代碼中使用硬編碼的 JNDI 名稱空間,而依賴於 java:comp/env 名稱空間中的應用程序環境項(正如 J2EE 1.2 規范中所定義的)。通過這種方式,在部署階段可以為不同的版本指定實際的 JNDI 名稱空間。
外部資源沖突
不同版本的 J2EE 應用程序可能會使用相同的外部資源的不同版本,例如數據庫結構 。可以通過資源管理器(例如 JDBC 數據源)使不同版本的應用程序使用同一外部資源的 不同版本,並且通過應用程序的 java:comp/env 環境來訪問資源管理器。通過這種方式 ,直到部署階段才將外部資源和使用它的應用程序代碼綁定起來,因而可以在部署階段指 定外部資源的不同版本。
圖 1 突出顯示了在同時托管兩個不同版本的 J2EE 應用程序時發生沖突的部分。
圖 1. 在同一個應用程序服務器上同時托管的不同版本的 J2EE 應用程序之間的沖突 。
另外,除了以上的各種沖突,也要考慮下面的應用程序設計和開發問題:
應用程序設計問題
為了能夠支持多個版本的分布式 J2EE 組件,在設計組件接口時要特別加以注意,例 如 EJB 的遠程接口及其對應的實現類、客戶端類(包括使用接口的類)。在設計可串行 化的或者用於傳遞對象或值對象(這些對象在組件接口之間傳遞)的可外化的類(請參見 參考資料)時,也要特別加以注意。
同一個服務器同時托管不同版本的應用程序組件的一種方法是,將不同版本的應用程 序組件類放在不同的 JAR 文件中,並且不同版本的應用程序使用不同的類加載器,但這 種方法的價值不大。這種方法只有在所有的版本變化僅僅局限於類的實現時才是一種適用 的方法。如果組件接口變化了(例如 EJB 接口),那麼相應版本的所有客戶端類也要具 有不同的版本,即使客戶端不會使用任何改動的功能也是如此。因此,對 J2EE 組件接口 (例如 EJB 接口)做出很小的改動,就會牽扯到改變大量的客戶端類。通過應用面向對 象的設計方法來管理單個的 J2EE 組件實現類及接口如何變化,可以更好地對此進行處理 。
使用面向對象方法的其他好處包括可以重用不同版本類的通用功能。使用不同的包名 ,可以避免不同版本之間的類命名沖突。這也可以允許一個組件訪問其他組件的不同版本 (例如,同一個 EJB 組件的不同版本可以被同一個引用它的 servlet 支持)。在後面描 述的示例 J2EE 應用程序中,我們將看到如何改變組件的接口以便同時托管。
封裝選擇
J2EE 應用程序類可以封裝在 EJB 模塊( .jar )或 Web 模塊( .war )中。封裝企 業應用程序不同版本的一個最簡單的方法就是,將每個版本的所有 EJB 和 Web 模塊封裝 到它們自己的企業應用程序模塊中( .ear )。盡管這樣做可能不會帶來太多的困難,但 是這或許不是最好的解決方案,因為企業應用程序模塊通常是由大量的 EJB 和 Web 模塊 組成的,並且所有的組件不會同時從一個版本升級到另一個版本,而實際上可能會是一次 升級只涉及到單個的組件。在本文的後面提供了一種封裝選擇,這種選擇可以得到較細粒 度的應用程序版本。
跟蹤分布式組件的變化
EJB JAR 和 WAR 清單文件(manifest file)提供了跟蹤不同包版本的方法。可以在 運行時利用版本信息來檢查 J2EE 應用程序分布式組件之間的兼容性。盡管在運行時使用 J2EE 規范可以表達和檢索單個組件的版本信息,但是沒有一種方法來表達一個版本模塊 對另一個版本模塊的依賴性。檢查和執行這種依賴性必須通過運行時的應用程序邏輯來完 成。
會話對象的不兼容性
如果應用程序持續使用可串行化的對象,並且會對這些可串行化的對象的類做出改變 ,那麼在做出這些改變的時候要加以考慮。
樣本 J2EE 應用程序
為了展示如何解決上面列出的問題,我們將考慮一個小型的 J2EE 應用程序,我們將 其稱作為 MyBank。
圖 2. 樣本 J2EE 應用程序:MyBankk
MyBank 應用程序被封裝在 MyBankCMP.ear 文件中,並且由下面列出的 J2EE 組件和 模塊組成(如圖 2 所示):
EJB 模塊(以綠色顯示): MyBankCMPEJB.jar
包名: com.ibm.mybank.ejb ,由以下元素組成:
Account ——是一個實體 bean,表示一個用戶的帳號,並且允許用戶查看 帳號信息、提款和存款,而且還可以讓用戶查看以及設置帳號類型。
Transfer ——是一個會話 bean,這個會話 bean 可以使用戶查看帳號余 額,並且可以將資金從一個帳號轉到另一個帳號。
AccountValue ——是一個傳遞對象,這個對象表示從 Account 實體 bean 遠程接口獲取的帳號信息。
Web 模塊(以黃色顯示): MyBankCMPWeb.war
包名: com.ibm.mybank.web ,由以下 Web 組件組成:
CreateAccount ——這是一個 servlet,這個 servlet 調用 Account 本 地接口的創建操作,並且通過一個新構建的 AccountInfo 值對象來傳遞這個 servlet。
Create ——這是一個靜態的 HTML 頁面,這個頁面提供用於接受用戶輸入 的表單,並通過調用 CreateAccount servlet 來創建一個 Account。
CreateAccountJSP ——這是一個 JSP,用來顯示創建帳號操作的結果,並 且具有輸入表單。
TransferFunds ——這是一個 servlet,這個 servlet 調用 Transfer bean 中的轉帳及查看帳號余額的操作。
Transfer ——這是一個 HTML 頁面,這個頁面提供用於接受從一個帳號到 另一個帳號轉帳的輸入表單,並且可以查看帳號余額。
TransferFundsJSP ——這是一個 JSP 頁面,這個頁面顯示轉帳及查看帳 號余額操作的結果,並且具有輸入表單。
圖 3 中的類結構圖顯示了應用程序 MyBank 中的類。
圖 3. MyBank 應用程序的類結構圖
在上面的示例場景中,讓我們來考慮一下同時托管兩個不同版本的 MyBank J2EE 應用 程序的問題。我們可以通過添加一個新的帳號持有人名稱字段,使我們的應用程序從版本 1 變化到版本 2。在圖 2 中,我們用陰影標出了在此改變中受影響的組件。在隨後的部 分中,我們將詳細討論因為這種變化而帶來的同時托管問題。
類加載沖突
J2EE 應用程序組件的各個版本中的相同類的不同版本之間的類加載沖突,可以通過在 相同的 WebSphere 應用程序服務器 JVM 進程中使用不同的類加載器來解決。
圖 4 顯示了 WebSphere Application Server 的類加載器的層次。(請參見 參考資 料。)
圖 4. WebSphere 類加載器層次
應用程序類加載器負責加載 EJB JAR 文件中的 EJB 模塊以及封裝在相關的 JAR 文件 中的實用程序類。不同的應用程序加載類可以使相同的 EJB 類以及 EJB 類使用的實用程 序類的不同版本得以分離。J2EE 應用程序中的 Web 模塊通過 WAR 類加載器來加載,這 個類加載器是企業應用程序中的應用程序類加載器的一個子類。可以設置使用不同的 WAR 類加載器來加載不同版本的 Web 組件類。可以使用不同的封裝選擇來分隔不同版本的 J2EE 組件,並且這種做法本身就意味著要使用不同的類加載器。(請參見 封裝選擇。)
服務器端組件類的不同版本應該盡量使用不同的類加載器使其分離開來。然而,有時 有必要共享客戶端類(例如當 EJB stub 屬於不同版本的情況),這樣可以使不同版本的 應用程序訪問同一個組件,例如一個 servlet。這種情況可以在應用程序開發階段通過使 用不同的包名來解決。(請參見 應用程序設計問題。)
servlet 路徑沖突
Web 組件的不同版本可能具有不同的 servlet 上下文根,這就可以使 Web 容器識別 出同一個 Web 組件的不同版本,並且可以同時使用不同的 WAR 類加載器加載相同 servlet 類的不同版本。通過這種方式,應用程序版本的粒度可以降到 Web 模塊(.war 文件)級上。servlet 上下文根是在企業應用程序檔案(.ear)文件中的部署描述符中對 每個 Web 應用程序進行指定的。樣本 1 給出了 EAR 部署描述符指定上下文根的例子。
靜態內容(例如 HTML 頁面和 JSP 頁面)的不同版本也可以通過 Web 應用程序的不 同上下文根來唯一指定。要獲得相關的示例,請參見 Testing Multiple Versions of Web Applications。
樣本 1. 在應用程序部署描述符中為 Web 應用程序的不同版本指定不同的上下文根 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC
"-//Sun Microsystems, Inc.//DTD J2EE Application 1.3//EN"
"http://java.sun.com/dtd/application_1_3.dtd">
<application id="Application_ID">
<display-name>MyBankCMPWebEAR</display-name>
<module id="WebModule_1071087285887">
<web>
<web-uri>MyBankCMPWebv1.war</web-uri>
<context-root>MyBankCMPWebv1</context- root>
</web>
</module>
<module id="WebModule_1071091542238">
<web>
<web-uri>MyBankCMPWebCommon.war</web- uri>
<context-root>MyBankCMPWebCommon</context- root>
</web>
</module>
<module id="WebModule_1071092618175">
<web>
<web-uri>MyBankCMPWebv2.war</web-uri>
<context-root>MyBankCMPWebv2</context- root>
</web>
</module>
</application>
JNDI 名稱空間沖突
EJB 本地接口是在共享的 JNDI 名稱空間中注冊的持久性對象引用。J2EE 應用程序的 多個版本在一個單一的 WebSphere 域中使用共同的 WebSphere 名稱空間。然而,分配給 不同版本 EJB 本地接口的 JNDI 名稱空間之間會產生沖突。為了避免這種沖突,可以直 到部署階段才將 EJB 本地接口對 JNDI 名稱空間的引用綁定。對於 EJB 客戶端(例如 Web 應用程序),可以通過 java:comp/env 來引用 EJB,而不是使用硬編碼的 JNDI 命 名。在 java:comp/env 環境中對 EJB 的引用是在部署描述符中聲明的,並且在部署階段 將其綁定到適當的 EJB 本地接口引用版本。
樣本 2 中的代碼展示了如何在應用程序的 java:comp/env 中使用 EJB 引用。圖 5 展示了在部署階段如何為 EJB 本地接口指定 JNDI 名,圖 6 展示了在部署階段如何將 java:comp/env 引用綁定到特定版本的 JNDI 名。
樣本 2. 使用 java:comp 來定位 EJB 本地接口 Context ctx = new InitialContext();
accountHome = (AccountHome) javax.rmi.PortableRemoteObject.narrow(
ctx.lookup ("java:comp/env/ejb/Account"),AccountHome.class);
圖 5. 在部署階段為 EJB 本地接口指定 JNDI 名
圖 6. 在部署階段將 java:comp/env 綁定到 JNDI 名
下表列出了在樣本應用程序 MyBank 中使用的所有 JNDI 名:
版本 Account EJB 組件 Transfer EJB 組件 最初版本 類名 com.ibm.mybank.ejb.Account com.ibm.mybank.ejb.Transfer JNDI 名 ejb/MyBank/Account ejb/MyBank/Transfer 版本 1 類名 com.ibm.mybank.ejb.v1.Account com.ibm.mybank.ejb.Transfer JNDI 名 ejb/MyBank/v1/Account ejb/MyBank/v1/Transfer 版本 2 類名 com.ibm.mybank.ejb.v1.Account com.ibm.mybank.ejb.Transfer JNDI 名 ejb/MyBank/v2/Account ejb/MyBank/v2/Transfer
使用 WebSphere Application Server V5 的名稱空間轉儲實用程序可以找到 WebSphere JNDI 名稱空間的內容。
外部資源沖突
不同版本的 J2EE 應用程序組件可能需要使用不同版本的外部資源,例如不同版本的 數據庫結構(這些不同的數據庫結構用於不同版本的實體 EJB 組件)。為了能夠解決這些 外部資源沖突,就要通過資源管理器(例如 JDBC 數據源)來訪問這些資源。這樣做就能 夠在編寫應用程序代碼時不必考慮部署應用程序時的情況,並且這也可以在部署階段使不 同版本的應用程序綁定到不同的數據源。圖 9 展示了在部署階段如何為不同版本的實體 bean 指定數據源。不同版本的 J2EE 應用程序組件(例如訪問相同外部數據庫的實體 bean)在修改相同的數據表或數據記錄的時候會存在同步問題。當然,應該避免這種情況 的發生。如果無法避免這種情況,那麼就要使用適當的 EJB 緩存策略。(請參見 參考資 料。)
圖 7. 在部署階段通過 JNDI 名指定外部資源
應用程序設計問題
將應用程序版本粒度降低到單個的 J2EE 應用程序組件通常會對應用程序的設計做出 改變。通過不同的應用程序類加載器使不同版本的服務器端類(例如 EJB tie 及實現類 )得以分離,但是不同版本的客戶端類可能需要同一個類加載器來加載。當改變 EJB 組 件接口,並且不同版本的 EJB 組件需要同一個客戶訪問時,這種情況變得尤為突出。為 了解決這種情況,可以使不同版本的客戶端類(包括接口)分隔到不同的包中。不同版本 的 EJB 接口的之間的公共部分可以通過使用相同的父接口抽象出來。另外,可以通過修 改 Service Locator模式來訪問合適版本的 EJB 本地及遠程接口。
在樣本應用程序中,需要做出以下改變以適應附加的字段 accountHolderName:
Account 實體 bean 需要有一個附加的 CMP 字段,名為 accountHolderName。
AccountInfo 值對象需要有一個附加的字段和 accountHolderName 的訪問器方法。
CreateAccount servlet 需要能夠為 AccountHome.create() 方法提供附加的數據 (accountHolderName)。
Create HTML 頁面表單和 CreateAccountJSP 應該有新的 accountHolderName 的文本 輸入。
Transfer EJB 組件、Transfer servlet 和 TransferFundsJSP 依賴於 Account EJB 組件,但是它們並不需要使用 accountHolderName 屬性。因此,這些組件不受添加這個 字段的影響。在與此類似的情況下,可以應用 Open-Closed 原則(請參見 [Martin])以 這種方式對 Account EJB 組件的變化進行建模,這樣,像 Transfer EJB 這樣的組件就 不受 Account EJB 變化的影響,除非這些組件會使用後者的新功能。在我們的樣本應用 程序中,Account EJB 的基本功能可以抽象成一個基本接口(Account),並且提供一個 基本實現(AccountBean),這些基本接口及基本實現存放在包 com.ibm.mybank.ejb 中 。特定於版本的接口和實現可以擴展基本接口和基本實現。
不同的包應該用於不同的版本,例如:
com.ibm.mybank.ejb.v1 可以用於封裝 Account EJB 組件在版本 1 中的接口及實現 ,這個版本的 Account EJB 組件中包含的方法不希望在將來的版本中使用。
com.ibm.mybank.ejb.v2 可以用來封裝改變了的 Account EJB 接口及實現,例如,新 字段 accountHolderName 的訪問器。
com.ibm.mybank.ejb 可以用來封裝和版本無關的 Account EJB 組件的基本接口,這 個包也可能包含其他的 EJB 組件,例如 Transfer EJB,這個組件在後面的版本中不受任 何影響。
每個版本的 Account 本地接口只能在特定於版本的包中,因此其本地接口的 create 方法會返回一個相應版本的新創建的 AccountEJB。圖8顯示了一個類結構圖,其中包含形 成 Account EJB 不同版本的抽象。
CreateAccount servlet 需要創建特定於版本的 Account,以及特定於版本的 AccountValue 對象。因此,在特定於版本的包中需要 CreateAccount servlet 的特定於 版本的實現。例如, com.ibm.mybank.web.v1. CreateAccount 應該創建類型為 com.ibm.mybank.ejb.v1. Account 的 Account。TransferFunds servlet 使用 Transfer EJB,而 Transfer EJB 在這兩個版本中沒有變化。因此,TransferFunds servlet 可以 使用兩者中任一版本的 Transfer EJB。在部署階段,改變 Transfer EJB 的版本,就如 同將 EJB 引用( java:comp/env/ejb/Transfer )映射到任一版本(也就是說, /ejb/MyBank/v1/Transfer 和 /ejb/MyBank/v2/Transfer 中的任一個)的 Transfer EJB 的 JNDI 一樣簡單。由於 Transfer EJB 組件不需要使用 accountHolderName,因此它只 使用基本接口 com.ibm.mybank.ejb.Account 。
圖 8. MyBank 應用程序的多版本類結構圖
為了確定 Account EJB 接口的適當的版本,Transfer EJB 組件可以使用 AccountServiceLocator( Service Locator模式)。樣本 3 給出了 ServiceLocator 接 口的例子,並且樣本 6 給出了 ServiceLocator 模式的實現,這個實現可以用來確定 Account EJB 的版本 1。在部署階段可以通過使用一個環境項(如樣本 5 所示)將適當 版本的 ServiceLocator 實現提供給 Transfer EJB 實現。樣本 4 展示了可以如何使 ServiceLocator 用於 Transfer EJB 組件。
EJB stub、tie 和可串行化值/傳遞對象組成了 J2EE 應用程序的客戶和服務器間的接 口。EJB stub 和 tie 是在部署階段從應用程序代碼中的 EJB 接口生成的,可串行化值 對象完全是通過應用程序代碼來實現的。如果您想將不同版本的客戶和服務器組件混合使 用並且使其兼容,那麼 EJB stub、tie 和可串行化值對象就需要具備版本兼容性。
樣本 3. Account ServiceLocator 接口 package com.ibm.mybank.service;
import javax.ejb.FinderException;
import com.ibm.mybank.ejb.AccountLocal;
public interface AccountServiceLocator {
AccountLocal findAccountLocal(int accountId) throws FinderException;
}
樣本 4. Transfer EJB 組件中用於得到 AccountServiceLocator 適當版本的代碼
private AccountServiceLocator getAccountServiceLocator()
throws Exception {
InitialContext initCtx = new InitialContext();
String acctServLocClassName = (String)initCtx.lookup(
"java:comp/env/AccountServiceLocatorClassName");
return (AccountServiceLocator) Class.forName(
acctServLocClassName).newInstance();
}
樣本 5. 指定 ServiceLocator 適當版本的環境項 <session id="Transfer">
<ejb-name>Transfer</ejb-name>
<home>com.ibm.mybank.ejb.TransferHome</home>
<remote>com.ibm.mybank.ejb.Transfer</remote>
<local- home>com.ibm.mybank.ejb.TransferLocalHome</local-home>
<local>com.ibm.mybank.ejb.TransferLocal</local>
<ejb-class>com.ibm.mybank.ejb.TransferBean</ejb- class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<env-entry>
<description>
The name of the AccountServiceLocator class
</description>
<env-entry-name>
AccountServiceLocatorClassName
</env-entry-name>
<env-entry-type>java.lang.String</env-entry- type>
<env-entry-value>
com.ibm.mybank.service.v1.AccountServiceLocator
</env-entry-value>
</env-entry>
</session>
樣本 6. Account ServiceLocator 接口的版本 1 的實現 package com.ibm.mybank.service.v1;
import javax.ejb.FinderException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import com.ibm.mybank.ejb.Account;
import com.ibm.mybank.ejb.AccountLocal;
import com.ibm.mybank.ejb.v1.AccountKey;
import com.ibm.mybank.ejb.v1.AccountLocalHome;
public class AccountServiceLocator implements
com.ibm.mybank.service.AccountServiceLocator {
private AccountLocalHome acctLocalHome = null;
public AccountServiceLocator() throws Exception {
acctLocalHome = getAccountHome();
}
public AccountLocal findAccountLocal(int accountId)
throws FinderException {
AccountKey key = new AccountKey(accountId);
return acctLocalHome.findByPrimaryKey(key);
}
private AccountLocalHome getAccountHome() throws Exception {
try {
InitialContext initCtx = new InitialContext();
Object objref =
initCtx.lookup ("java:comp/env/bank/Account");
return (AccountLocalHome) objref;
} catch (NamingException ne) {
ne.printStackTrace();
throw new Exception(
"Error looking up AccountHome object: " +
ne.getMessage());
}
}
}
Java Product Versioning Specification中描述了如何開發向後兼容的可串行化值對 象。目前,還沒有用來開發向後兼容的 EJB stub 和 tie 的規范。因此,必須使用於每 個版本的 J2EE 客戶和服務器組件保持兼容。這可以通過使用適當的封裝以及運行時檢查 很方便地得以實現,如下所述。
封裝選擇
通常有兩種應用程序封裝選擇:
將每個版本的 J2EE 應用程序中的所有 EJB 和 Web 模塊封裝成獨立的自包含應用程 序 EAR 文件,並且沒有外部依賴性。 每個應用程序 EAR 文件可以單獨地進行部署。在 部署階段,類加載沖突通過對不同的企業應用程序模塊使用不同的應用程序類加載器得以 解決。JNDI 名稱空間沖突可以在應用程序裝配階段通過使用 <ejb-link> 描述符 ,並且使用無沖突的 JNDI 名而得以解決。servlet 路徑沖突和外部資源沖突可以通過對 Web 應用程序的每個版本使用不同的上下文根而得以解決。盡管通過這種方式可以很簡單 地部署多個版本的應用程序,但是這種做法在組件模塊改變時需要重新部署組件模塊。圖 9 顯示了 EAR 文件的組成:一個 EJB JAR 文件( MyBankCMPEJB.jar )和一個 WAR 文 件( MyBankCMPWeb.war )。這是一個自包含的應用程序,因為這個應用程序所需要的所 有組件都包含在這個 .ear 文件中。所有其他的版本也要和這個 EAR 文件一樣包含相同 的文件。而且,多個版本的組件(例如 EJB Account)不能從公共的組件中訪問,因為類 名沖突在此情況中沒有辦法解決。因此,這種應用程序版本的粒度太大。
圖 9. MyBank 應用程序的自包含 EAR 文件的組成部分
在同一個應用程序中混合使用不同版本的 EJB 和 Web 組件模塊。通過這種方式,單 個的模塊可以獨立地進行升級。Web 模塊應用程序可以在不需要改動 EJB 模塊的情況下 進行升級,並且可以在不影響現有版本的情況下添加新版本的 EJB 模塊。為了做到這一 點,您必須將應用程序分成多個 .ear 文件。在我們的樣本應用程序中,圖 9 顯示的 EAR 文件被分成下面幾個部分:每個版本的 EJB 有一個 .ear 文件(請參見圖 10 和 11 )、每個版本的 Web 應用程序的 .ear 文件包含多個版本的 .war 文件、每個版本的 Web 應用程序(請參見圖 12)。
圖 10 給出了 Account EJB 版本 1 的 .ear 文件。注意其中有兩個 JAR 文件:
MyBankCMPEJBv1 包含 Account EJB 版本 1 的特定接口和實現。
MyBankCMPEJBCommonUtility 包含版本 1 和版本 2 中共用的接口和實現,以及 Transfer EJB,並且 Transfer EJB 在從版本 1 到版本 2 的變化中不會發生任何變化。
類似的,圖 11 給出了 Account EJB 版本 2 的 .ear 的組成。相同的 .jar 文件 MyBankCMPEJBCommonUtility 被封裝到 MyBankCMPEJB [v1|v2] EAR 的兩個版本中。
圖 12 給出了 Web .ear 文件的組成,以及 MyBankCMPWebv1.war 和 MyBankCMPWebv2.war (包括特定於版本的 CreateAccount servlet)和 MyBankCMPWebCommon (包括和版本無關的 TransferFunds servlet)。
圖 10. 應用程序 EAR EJB 版本 1 的組成
圖 11. 應用程序 EAR EJB 版本 2 的組成
圖 12. Web EAR 的組成
通過這種方法拆分應用程序可以使不同版本的 EJB 組件隔離開來,這種隔離是通過使 用不同的應用程序類加載器實現的,一個 EAR 文件對應一個類加載器。不同版本的 Web 組件模塊通過不同的 WAR 類加載器而隔離開來,一個 Web 模塊對應一個 WAR 類加載器 。由於 WAR 類加載器是應用程序類加載器的子類,所以所有版本的 Web 模塊都可以封裝 在同一個企業應用程序中。為了使應用程序類加載器或 Web 類加載器可以加載通用的實 用程序類 JAR 文件,就必須在類路徑頭部指定這些 JAR 文件依賴於 EJB JAR 或 WAR 清 單文件(manifest file)。樣本 7 給出了 MyBankCMPEJBv1.jar 中的清單文件的例子; 樣本 8 給出了 MyBankCMPWebv1.war 中的清單文件的例子。
樣本 7. MyBankCMPEJBv1.jar 中用於指定相關 JAR 文件的清單文件 Manifest-Version: 1.0
Class-Path: MyBankCMPEJBCommonUtility.jar
樣本 8. MyBankCMPWebv1.war 中用於指定相關 JAR 文件的清單文件 Manifest-Version: 1.0
Class-Path: MyBankCMPEJBCommonUtility.jar MyBankCMPEJBv1Utility.jar My
BankCMPWebCommonUtil.jar
圖 13. 在 WebSphere 應用程序服務器 V5 中配置共享類庫
為了使 Web 應用程序能夠調用 EJB JAR 組件,需要將 Web 應用程序中使用的 java:comp/env EJB 引用綁定到適當版本的 EJB 本地接口的 JNDI 名。如果 Web 應用程 序組件和 EJB 組件通過不同的類加載器來加載,那麼 EJB 組件的本地接口是不能使用的 。您必須使 EJB 遠程接口的客戶端類對應用程序類加載器可用。這可以通過兩種方式來 得以實現:
EJB 遠程接口客戶端類可以作為實用程序 JAR 文件封裝到 Web EAR 文件中,並且可 以通過修改用於每個 Web 應用程序 WAR 文件的清單文件,在類路徑頭部下面添加 EJB 客戶端類實用程序 JAR 文件。(請參見樣本 8)。
將 EJB JAR 作為共享的類庫添加到 Web WAR EAR 應用程序中。
跟蹤分布式組件的變化
當將多個版本的 J2EE 應用程序組件部署到同一個 WebSphere 域中時,為了安全起見 ,應該進行一些運行時檢查。另外,保留對部署模塊版本變化的跟蹤記錄也是一種可行的 做法。 Java Product Versioning Specification 將 Java 包定義為一個可以開發、封 裝、驗證、升級及分布式的一致性單元。JAR 文件的清單文件(manifest file)包含指 定包版本的屬性。樣本 9 給出了在清單文件中指定 J2EE 應用程序組件版本的例子。在 運行時,J2EE 組件(包含在一個包中)的版本可以從 java.lang.Package 接口得到。樣 本 10 給出了獲得在清單文件中指定的版本代碼的例子。包的版本信息可以在運行時用來 檢查分布式應用程序組件的兼容性。例如,根據 Java 版本規范可以從 EJB JAR 清單文 件中取得 EJB 的版本信息。客戶端的版本信息可以通過客戶端 JAR 文件獲得,服務器端 版本信息可以從服務器端 JAR 文件獲得,並且通過 EJB 組件的遠程接口來查詢,從而可 以在運行時檢查版本的兼容性。
樣本 9. 在 EJB JAR 清單文件中指定的包版本信息
Manifest-Version: 1.0
Name: com/ibm/mybank/ejb/v2/
Specification-Title: "EJB"
Specification-Vendor: "Sun Microsystems, Inc.".
Specification-Version: "2.0"
Implementation-Title: "MyBank EJBs"
Implementation-Vendor: "IBM"
Implementation-Version: "1.4"
樣本 10. 從 JAR 清單文件中獲得包版本的應用程序代碼
public String getImplVersion() {
Package p = this.getClass().getPackage();
String specTitle = p.getSpecificationTitle();
String specVersion = p.getSpecificationVersion();
String implTitle = p.getImplementationTitle();
String implVersion = p.getImplementationVersion();
String messageLine = "Server Specification: " + specTitle +
", specification version: " +
specVersion +
", Implementation: " + implTitle +
", implementation version: " +
implVersion + "\n";
return messageLine;
}
會話對象的不兼容性
Java 產品版本控制規范(Java Product Versioning Specification)討論了可以對 可串行化類做出哪些改變,或者不可以作出哪些改變,以保持其和以前版本的兼容性。對 會話對象類的改變也是要遵循相同的法則。另外,自定義的 readObject 和 writeObject 方法可以加到該類的各個版本中。
結束語
最近,應用程序的版本問題引起了廣泛的注意。對待版本問題的傳統做法往往是輕率 地使用“大手筆”的方法使應用程序從一個版本轉變到另外一個版本,然而, 也可以在較小粒度的水平上進行增量升級,獲得使 J2EE 組件的不同版本得以共存。本文 提出了在滿足同時托管 J2EE 組件的多個版本的要求時所面臨的挑戰,並且提供了一些方 法(既考慮到了部署階段又考慮到了設計階段)來幫助您解決這些問題。