本文探究了面向服務的體系結構中各種形式的松耦合,并重點介紹什么時候是使用消息驅動 Bean 所提供的異步處理能力的最佳時機。
摘自 IBM WebSphere 開發者技術期刊。
在每個專欄中,EJB 倡導者都采用獨特的前后銜接的對話方式與實際客戶和開發人員進行交流,并在期間針對某一大家關注的設計問題推薦解決方案。其中并沒有介紹任何確定性的細節,也沒有提出“新穎的”或專有的體系結構。要了解更多信息,請參見 EJB 倡導者簡介。
您對松耦合的定義是否太過狹窄呢?
由于這是 2005 年的最后一篇文章,而且這次交流的是 Java? Platform、Enterprise Edition (Java EE) 組件而非會話和實體 Bean,所以可以通過這篇文章來很好地總結本專欄所進行的一年之久的討論,并將所有的組件一起放到完整的面向服務的體系結構中。
問題:太過于關注 SOA 中的會話和實體
親愛的 EJB 倡導者:
到目前為止您的專欄全部都是關于服務的會話和實體 EJB,它對于直接連接 Java 應用程序是很重要的,例如基于 Web 客戶端的 HttpServlets 或富(我們不喜歡說“胖”)客戶端的 Swing 應用程序。但我們曾聽說面向服務的體系結構都是與松耦合有關的。這不就意味著要盡可能地使用 SOAP 來提供使客戶端和服務器應用程序能夠獨立運行的語言中立性和異步協議?換句話說,為什么您對 JMS 和消息驅動 Bean 討論得不多呢?
署名:
Feeling Disconnected
對松耦合有許多方面要考慮
親愛的 Disconnected:
EJB 倡導者關注的是應用程序的服務層,而對客戶端介紹得很少,因為我對古諺語形式追隨功能 (form follows function) 深信不疑。
其原因在于,許多進行 SOA 的項目之所以失敗,是因為它們沒有首先建立定義服務的良好模型就開始著手實現細節。
這種傾向相當正常,因為我在 SOA 項目中接觸到的大多數人都是架構師和編程人員,他們知道棘手的始終是在細節上,想要盡快解決它們。所以,一旦我們認同好服務的特征是粗粒度、無狀態、可通過中介傳遞和適應的(請參閱 Is it ever best to use EJB components without facades in service oriented architectures?),很明顯帶有數據傳輸對象的會話 Bean 就會在實現中扮演重要角色。
但上期專欄介紹了使用實體 Bean 及其 Home 方法代替會話 Bean 的可能性,從而使人們對會話 Bean 是否真的是必需的產生了疑問。圖 1 顯示了同時使用的兩種方法。
圖 1. 用有待使用的會話和實體 EJB 實現的服務
圖 1 顯示了當使用傳遞過去的會話 Bean 時,純實體方法(自 EJB 2 開始可用)如何才能有更少的組件和更短的路徑長度。其中使用綠色框表示客戶端,藍色框表示各種接口和 Facade 類。橙色框是最常訪問的實體 Bean。框和框之間使用雙向箭頭連接,箭頭標有在組件之間通信所使用的協議;藍色箭頭表示相同 JVM 中的 Java 調用,紅色箭頭表示遠程連接(在本例中使用 RMI/IIOP)。為了表示端到端的流向,對流程箭頭進行編號,其中 A1-A10 表示從 Java 客戶端經過會話 Bean 到實體和返回的流程,B1-B4 表示從客戶端直接到實體 Bean 和返回的流程。
客戶端用于檢索服務接口的編程模型也很簡單,雖然在圖中沒有表示出來。檢索會話 Bean 接口需要在 JNDI 上下文中查找會話 Home 并用它創建一個會話;實體 Home 僅僅需要一次查找,其方法可以直接調用而不需要創建對 EJB Object 的引用。以下兩段代碼示例顯示了它們之間的區別。
清單 1. 定位和調用遠程會話 EJB 方法
Context initCtx = new InitialContext();
Object obj = initCtx.lookup("java:comp/env/ejb/OrderEntry");
OrderEntryHome home = (OrderEntryHome)PortableObjectRemote(
obj, OrderEntryHome.class
);
OrderEntry ref = home.create();
// Method must be invoked on a session reference
CustomerData data = ref.getOpenOrderForCustomer(cID);
|
清單 2. 定位和調用等效遠程實體 EJB Home 方法
Context initCtx = new InitialContext();
Object obj = initCtx.lookup("java:comp/env/Customer");
CustomerHome ref = (CustomerHome)PortableObjectRemote(
obj, CustomerHome.class
);
// Note how the method is invoked directly
CustomerData data = ref.getOpenOrder(cID);
|
圖 1 和相關的代碼示例顯示了 Java EE 編程模型的真正好處,不管您是否選擇使用實體 Home 方法。編程模型逐漸改進并提供向后兼容。簡而言之,您的最佳實踐能夠得以發展而不必強制更改現有的應用程序。另外,JNDI 上下文提供了一個不容忽視的松耦合相關方面:實現獨立性。
會話或實體 EJB 組件的遠程接口提供了位置獨立性。使用遠程接口使得在相同的 JVM 中部署客戶端應用程序和服務組件成為可能,其中它對系統(例如使用 HttpServlets 的 Web 應用程序)的響應時間、吞吐量和可維護性目標有意義。圖 2 顯示的正是這樣的配置,其中企業質量應用服務器(如 IBM? WebSphere? Application Server)在聯合部署了客戶端和服務組件時“短路”遠程接口以使用 Java 并按引用傳遞(不管服務實現為實體 Home 方法還是會話 Bean)。流程 A1-A6 顯示了與服務組件聯合部署的 HttpServlet 的使用。流程 B1-B4 顯示了它如何被遠程富客戶端 Java EE 應用程序重用。
圖 2. 部署到本地 Web 應用程序和遠程富客戶端的服務
但這聽起來像是您已確定松耦合的最重要方面是語言中立性和異步操作。并且您認為需要異步操作會使得必須在服務器端使用消息驅動 Bean (MDB) 和 JMS。
在實現 MDB 時,許多編程人員使用的方法是通過其遠程接口調用表示服務的 EJB 組件,以使上述位置獨立性最大化。然后,不管服務實現是遠程還是本地部署,MDB 的目的都是作為簡單的適配器,將 MQ 層連接到承載服務的 EJB 容器。JMS 可能由異步客戶端應用程序(如果它是 Java)或 MDB 使用(通常用于應答隊列)。圖 3 顯示了服務實現的另一個輸入通道的這一配置。
圖 3. 用提供異步客戶端通道的 MDB 部署的服務
流程 C1-C2 與 D1-D6 分別顯示,以闡釋客戶端和服務器流程的獨立性。C2 和 D6 只是對 Writer 是否寫入消息進行“確認”,并不意味著等待。清單 3 顯示了 MDB 的典型結構,該結構應該有助于闡明它必須做的處理:
清單 3. 典型的消息驅動 Bean 實現
public class OrderSubmitMsgHandler implements MessageDrivenBean {
private transient MessageDrivenContext context = null;
// Interface supported by either the session Bean or entity Home
private transient OrderEntry ref;
public void ejbCreate() {
// Cache the home according to either snippet one or two
}
public void ejbRemove() {}
public void setMessageDrivenContext(MessageDrivenContext mdc) {
context = mdc;
}
// Message acknowledge and database update participate
public void onMessage(Message inMessage) {
// Parse the customer from the message
try {
ref.submit(customer);
}
catch (CustomerDoesNotExist e) {
// Send JMS message to reply queue from exception
}
catch (OrderNotOpen e) {
// Send JMS message to reply queue from exception
}
catch (OrderHasNoItemsException e) {
// Send JMS message to reply queue from exception
}
}
}
|
EJB 倡導者現在將回到您提到過的語言中立性問題。看起來您已經將語言中立性的要求與異步處理的要求緊密聯系起來了。沒有理由不能將這些考慮分開;解析 SOAP 消息和用它調用會話 Bean 的能力應該與該消息的處理是異步(通過 MQ 或者傳遞 JMS 等效消息的另一個協議)還是同步(例如通過 HTTP,甚至 IIOP)無關。事實上,Java EE 應用程序上的一些早期“發明”的 Web 服務使用 HttpServlet 來解析通過 HTTP 傳遞的 XML 消息。這種方法最終發展為 SOAP/HTTP。圖 4 顯示可以在 EJB 組件所實現的服務之上提供的另一個路徑。
圖 4. 將語言中立性和異步性的考慮分開
Web Service Servlet 和 Message Driven Bean 可以共享對來自從消息串或 HttpServletRequest 提取的流的數據傳輸對象進行解析的代碼。類似地,響應或應答可以共享從數據傳輸對象(它可以是 Exception 的一個實例)生成流的代碼。
希望這有助于您理解 Java EE 組件的布置,所有這些都將提供某種對面向服務的體系結構很基本的松耦合形式。
對話到此結束,
您的 EJB 倡導者
仍然有太多選項要選擇
親愛的 EJB 倡導者:
謝謝。
我以前不認為像 JNDI 這樣的服務和遠程接口會提供松耦合。我還可以了解如何將 SOAP 和 MQ 的概念“緊耦合”(按照您提到的方法),以及應該如何盡可能將它們分開。所以將解析和生成 SOAP 消息看作由 Web 服務 Servlet 和 MDB 重用的服務本身是很有意義的。
但謝謝,我不要這樣。
在這次討論之前,SOA 看似非常簡單:每個服務都公開一個 SOAP/MQ 接口。現在看起來有好多選擇要考慮,并且既然將 SOAP 消息的解析和生成看作服務,那么為什么不想建立一個獨立的會話 Bean 來封裝它們以便如圖中所顯示的那樣進行重用呢?
再次打擾您,我還是:
Still Disconnected
并非一切都是服務:要使用業務模型加以判定
親愛的 Disconnected:
這次討論是當過快要著手實現細節時會出現的情況的一個很好的例子。EJB 倡導者專欄 Which type of EJB component should assemble the data returned by a service? 非常重要,因為它描述了一種定義服務的自頂向下方法,這種方法將服務直接與業務流程模型相關聯。這里總結這種方法的基本細節:
-
開發一個業務流程模型,顯示重要域對象的生命周期中的主要里程碑:
-
我們使用狀態轉換關系圖來描述這一模型,其中狀態代表里程碑,轉換代表促使變成該狀態的事件。轉換可以看作應用程序所提供的服務(有關示例請參見圖 5)。
-
我們使用 UML "Actor" 符號來擴展狀態轉換關系圖,以顯示當處于該狀態時對象的所有者。狀態的所有者負責發起轉換,從而驅動應用程序的安全模型(也請參見圖 5)。
-
對于業務流程中的每個狀態,我們也對每個需要支持轉換動作的域對象的屬性和它們之間的關系進行建模。我們通常使用的符號是 UML 類關系圖(有關示例,請參見圖 6)。為每個狀態建立獨立的關系圖可以讓我們隨時對變化的對象“形狀”進行建模。這些關系圖驅動持久性數據。
-
開發一個用戶界面流程模型,顯示在典型“會話”期間來自業務流程的給定參與者如何與系統相交互。
-
與業務流程模型一樣,我們也使用狀態轉換關系圖,其中狀態代表屏幕和對話框,而轉換代表實際的 UI 事件,如選擇菜單項和按下按鈕。根據業務流程轉換指定與轉換相關的動作。
-
還是和業務流程模型一樣,我們構建一個類關系圖來顯示必須在每個狀態中可見的數據以支持各種選擇。這意味著該數據必須可以從用戶輸入派生,并以自頂向下的方式驅動服務上的讀取操作。
-
與業務流程模型不同,我們不需要用 Actor 符號擴展狀態關系圖,因為關系圖本身可以視為單個用戶角色的“生命中的一天 (day in the life)”。
圖 5. 顯示訂單生命周期的狀態轉換關系圖示例
圖 6. 顯示 open 訂單“形狀”的類關系圖示例
這種綜合方法可以確保:
-
將正確的操作分組到一個服務(與業務流程生命周期中的一個狀態相關聯的所有轉換和 read 方法)。
-
每個服務的用途很好理解(根據相關業務對象來指定動作)。
-
調用服務需要的前置條件和會產生的后置條件是相通的(當前狀態和可選的監護、以及下一狀態都是通過轉換指定的)。
-
標識了負責調用服務的用戶角色(參與者與每個狀態相關聯)。
不管是否實現,方法簽名都不會提供此信息,但此信息對于好的 SOA 來說是很關鍵的。否則,編程人員將陷入另一種傾向:一旦有懷疑,就再次構建。因為 SOA 的開發成本比較高(要為圖 4 所示的完全松耦合提供各種中介器和適配器),所以這種反對重用的傾向可能會導致獲得好處最少。
對您關于簡單性的答復中提出的問題的回答是,此信息一點都沒有強制您以某種方式公開接口,EJB 倡導者知道簡單只存在于旁觀者眼中。如果您想要使服務開發人員不必進行選擇,只需為每個服務提供圖 5 中所示的所有“藍色”組件即可:
-
遠程服務接口,用于提供同步 Java EE 客戶端訪問服務操作的位置獨立性。
-
與每個操作相關聯的 MDB,通過遵循 JMS 的 MQ 實現來提供異步非 Java 客戶端訪問。您可以選擇編寫此 MDB 或不同的 MDB 以期待 JMS 客戶端的 Java 消息(從而避免 HTTP 解析開銷)。
-
與每個操作相關聯的 Web 服務 Servlet,用于提供同步 SOAP over HTTP 客戶端訪問。
因為有人會擔心這種方法將生成大量未使用的組件,所以另一種簡單的方法是,應用 EJB 倡導者喜歡的內容來調用面向客戶端的體系結構 (COA);以對客戶端最自然的方式給它們提供使用服務正好需要的內容。
這種 COA 方法需要考慮業務流程和 UI 模型的細節,以便為每種方法挑選最可能的候選者。例如:
-
業務流程生命周期中的狀態之間的轉換很可能是異步服務的候選者,因為它們將是相關域對象的“所有者”變體。例如,在 open 訂單應用程序(我們在上述示例中調用此 OrderEntry)中,submit 方法可以簡單地將訂單的狀態更改為“submitted”,并發送一條 JMS 消息來將其復制到已提交訂單應用程序中(我們將這稱為一次 OrderFulfillment)。
-
狀態間的轉換通常應該是同步的,因為所有者沒有改變。舉個例子來說明為什么這些操作不應該是異步的,設想一下,如果您進入一位書商的 Web 應用程序,還必須輪詢或等待發布-訂閱事件來顯示目錄或將物品添加到購物車中!對于想要通過異步通道使用偽同步方式的讀者,請查閱 Bobby Woolf 關于設計消息傳遞系統的書籍。
-
只有對于沒有相關的非 Java 客戶端和服務的集成場景,才提供 SOAP over HTTP 或 MQ。
使用 COA 方法,可以“準時 (just-in-time)”開發組件,這就是 EJB 倡導者喜歡推薦它的原因。
對您的問題(為什么不將所有的東西都看作是服務,甚至是與中介器和適配器相關聯的轉換)的回答的最后一點是:簡單的回答超出了“過猶不及”這一事實。在開發 SOA 或 COA Java EE 應用程序時,最好將服務視為業務流程模型上的操作。服務與應用程序的功能需求有關。中介和相關聯的轉換與非功能需求有關,例如可靠性、可用性、有效性、可維護性和可移植性。如果您將中介或相關聯的轉換視作第一個類服務,則最終將使應用程序的真實用途變得模糊。
我知道這其中有很多方面的內容需要理解,所以當您應用這些方法時,遇到相關細節問題不要嫌麻煩,盡管與我聯系。
對話到此結束,
您的 EJB 倡導者。
結束語
這些交流中談論了 Java EE 如何為采用面向服務的體系結構的應用程序提供完整的實現框架,其中每個組件或 API 都在一些“松耦合”方面扮演重要角色:
-
操作系統獨立性是由 Java 本身提供的,因為 Java 為組件提供“一次編寫,到處運行”的語言,使得您的代碼與基礎操作系統相分離。
-
實現獨立性是由 Java Naming 和 Directory Interface (JNDI) 提供的,它具備在運行時將邏輯名稱綁定到實現的能力。
-
位置獨立性是由遠程接口使用 RMI over IIOP 提供給無狀態會話 Bean 或封裝服務的實體 Home 方法的。RMI/IIOP 是一個相當快的有狀態連接,但擴展性不那么好。
-
Web 服務器獨立性是由 HttpServlets 提供的,它可以響應同步 HTTP 協議。與 RMI/IIOP 不同的是,HTTP(通常)是一個擴展性很好的無狀態協議,但由于消息大小的限制,而且每次都需要在客戶端和服務器之間建立和取消連接,所以不能執行得很好。
-
應用獨立性是由異步 Java Messaging Service(或非 Java 客戶端的 MQ)和消息驅動 Bean 提供的(用于隊列處理程序)。
-
語言獨立性是由標準消息格式(如 SOAP)提供的,不管它是在 RMI/IIOP、HTTP 還是 MQ 上傳遞。因此,SOAP 可以被每個組件使用,但只有在需要時才使用它,這一點很重要。
相信您可以找到其他方法,我們樂意看到這樣的結果。困難的是需要處理與每種方法相關聯的權衡。這應該能夠讓您忙到明年。