提要:我們可以使用MessageDrivenBean(消息驅動組件),在企業級的應用程序中進行異步的消息傳送。
引言:Enterprise JavaBeans(EJB)1.1版本中定義了兩種組件類型—session組件和entity組件。客戶端對象可以同步調用EJB1.1的這兩種組件的方法,然而,為了繼承Message Oriented Middleware(MOM,面向對象的中間件)和Java Message Service(JMS,Java消息服務)的優點的需要,EJB框架中也相應的應當加入異步的消息通訊機制,所以,在EJB2.0中就定義了第三種組件類型----MessageDrivenBean(消息驅動組件)
MessageDrivenBean兼備EJB和JMS的功能,當然,如果您想要消息操作技術,那麼您大可只使用JMS就行了,但是新的消息驅動組件MessageDrivenBean提供了消息通訊的新的可能性。那麼,這些組件如何整合到一個應用程序服務器框架中?他們的功能又如何擴大了過去JMS服務器的使用范圍?讓我們看下文吧!
EJB和JMS
前面我們已經說過了,EJB1.1種為開發者定義了兩個企業級組件類型-----session和entity組件。session組件通常實現一些商業邏輯並且不能在多客戶端共用。Entity組件則描述一個實體的面向對象的概念,而這個實體往往存在於像數據庫那樣固定的存儲容器中。在這兩種組件模型中,使用本地的或遠程的接口來簡化客戶端的交互作用。按照定義,這種交互作用是嚴格的同步的。舉例來說,通過一個方法調用把一個請求發送給組件,然後服務器對象返回一個響應。(如圖1),
然而,在企業版應用程序的范圍中,也經常需要異步的消息傳遞,比方說,一個客戶可能想發給服務器一條信息,但是並不需要或者不想要服務器做出應答,這時,客戶端就沒有必要等待服務器對象處理請求。對於客戶端應用程序來說,在確保消息最終能夠到達服務器並被正常處理的前提下,提交一條消息然後繼續處理本身的事務,將會在很大的程度上提高效率。
能夠處理異步消息的能力的Java技術可以在Java Message Service(JMS)中找到,JMS原本就是被開發來提供傳統的Message Oriented Middleware(MOM)產品的一個標准Java接口。
現在,一些公司開發出了一整套新一代輕量級高效的純Java的JMS產品,這些產品是開發者能夠建立JMS連接來發布或從其它應用程序組件中接受消息。下面的例程給出了與一個JMS提供者接口的必要步驟:
代碼段一:准備客戶端
客戶端應用程序使用了帶有JMS 主題的MessageListener來接受和處理消息。
import javax.jms.*;
/**
*一個例程,演示如何取得一個JMS
*連接並取得一個消息監聽者。在本例中
*我們將獲取一個與一個JMS主題的連接
*/
public class JMSSample {
public static void main (String args[])
{
InitialContext context = new InitialContext();
// 查找主題
Topic topic (Topic)context.lookup("MyTopic");
file://取得我們創建JMS連接時所要用到的連接創建器
TopicConnectionFactory tcf =
(TopicConnectionFactory)context.lookup(
"TopicConnectionFactory");
// 創建JMS連接
TopicConnection conn = tcf.createTopicConnection();
// 從連接中創建JMS session。
// 這樣我們就可以創建一個非事務處理、AUTO_ACKNOWLEDGE的連接
TopicSession session = conn.createTopicSession(
false, Session.AUTO_ACKNOWLEDGE);
// 創建主題訂閱者
TopicSubscriber subscriber =
session.createSubscriber(topic);
// 監聽者
subscriber.setMessageListener(myListener);
// 指出我們將要接受信息的連接
conn.start();
}
}
除了采用上面的步驟取得JMS MessageConsumer(消息消費者)的連接之外,開發者還可創建並注冊一個或多個使用Message Consumer的JMS Message Listener(消息監聽者)接口。Message Listener總是在一個單獨的控制線程中執行,這就意味著在編寫消息監聽者時,開發者不需要擔心並發性問題的出現,見圖2。
下面我給出了一個典型的JMS 消息監聽者實現的代碼。
代碼段2:
/**
* 這個類是JMS MessageListener的一個實現
* 用來處理包含股票報價的消息
*/
class MyListener implements MessageListener {
/**
* 從收到的信息中取出股票報價
* 並且把它放入標准輸出流中並顯示。
*/
public void onMessage(Message message) {
// 從消息對象中取出報價
// 我們知道消息產生者發送TextMessages
try
{
String quote = ((TextMessage)message).getText();
System.out.println("股票報價: " + quote);
}
catch(JMSException e)
{
System.out.println(
"錯誤處理消息: "+message);
}
}
}
在這個實現中,MessageListener接收到的消息中包含了股票報價,消息監聽者只是簡單的從消息體中取得股票報價並把它輸出到標准輸出流中。
開發一個健壯的JMS客戶端程序可能是非常困難的,程序員必須要考慮可能會同時接受多個消息,此外還有交易安全性、並發性消息處理、對象生命周期、容錯性和可擴展性,這些都是開發者急切地想從EJB服務器中找到的功能。不過直到現在,程序員們還不得不自己動手把這些技術結合在一起應用。
為了整合EJB1.1和JMS,JMS監聽者必須要使用我們在代碼段2中描述的方法來建立。JMS客戶端程序必須參考一個stateless(無狀態)的用於響應處理JMS消息的session組件,然後,JMS消息要傳遞給EJB。然而,JMS消息並不要求被序列化,這就意味著這條消息在傳遞到遠程的EJB實例之前必須被轉換成為有序的消息類或在消息監聽者中部分地解構。而且,應用程序開發者還有責任管理JMS服務器之間的事務聯系,以及處理EJB、消息和並發性,這些都是非常復雜的事情。
即使一個應用程序開發者能把上面的這些都完成,並且也有能力訪問JMS提供者並取得消息,但顯然,他需要編寫一大堆的代碼,這對於我們這些常人往往是不大可能實現的。EJB2.0解決了這個問題,它通過擴展EJB組件類型,為需要異步消息支持的組件開發者提供簡化的解決方案——新的MessageDriven組件類型。
MessageDrivenBean組件
MessageDrivenBean被部署成為總是扮演信息消費者角色的客戶端。MessageDrivenBean沒有客戶端視圖,這就意味著其本地和遠程調用接口都是不可用的。一個消息產生者發信息給一個主題或隊列並且沒有認識到一個事實--MessageDrivenBean正扮演著消息消費者的身份。這就導致了在基於系統的 JMS之間的寬松聯結,並更多的考慮到了在集合一個分布式計算環境時應有的靈活性。
MessageDriven 組件沒有對話狀態,其實所有的組件實例當它們在沒有處理消息時都是等價的。這有點和無狀態的session組件的狀態特征有些類似。把組件實例集中起來是管理MessageDriven組件實例的普遍而又有效的辦法。
MessageDriven 組件必須以直接或間接的方法從接口javax.ejb.MessageDrivenBean中取得,而這個接口類則是從javax.jms.MessageListener接口得來並添加了兩個方法。onMessage()方法是從javax.jms.MessageListener接口中繼承來的,這個方法有唯一的參數,就是javax.jms.Message,可以是任何有效的JMS消息類型。這個方法顯然不包括throw(拋出)子句,所以在處理消息時不會拋出任何應用程序異常。
當這個容器接收到消息,它首先從一個可用實例池中取得一個MessageDriven組件(見圖4)然後把部署描述器中制定的任務與執行線程聯系起來,使其能夠傳播安全上下文。此外,如果部署描述器需要事務上下文的話,容器也會設置與之的關聯。
一旦完成了管理任務,接收到的消息酒杯傳送到MessageDrivenBean實例的onMessage()方法中,而一旦這個方法完成後,消息所載的事務就會被執行或返回,然後組件重新返回可用實例池中。
當MessageDrivenBean實例被從容器中(通常從實例池中)的任何強的參考中逐出,都會調用ejbRemove()方法。ejbRemove()方法將釋放任何被組件實例占用的資源。setMessageDrivenContext()方法有一個參數--javax.ejb.MessageDrivenContext類的一個實例。MessageDrivenContext類與定義在EJB1.1中的entity和session類有點類似,當一個組件實例被創建,容器就把它傳遞進一個實例占用的上下文中,這個類有取得環境信息的方法也有相應的方法取得JTA UserTransaction類(用於管理事務定界的組件)。
此外,組件提供者還應當在EJB2.0服務器中可攝制的組件提供一個沒有參數的ejb.Create()方法。這個組件實例可以獲得任何在ejb.Create()用於進行處理的所需要的資源,比如說,在這一點上,MessageDrivenBean實例可以取得一個數據庫連接,如果ejb.Remove()方法被調用的話,它將關閉或釋放。
值得注意的是,MessageDrivenBean現在已經大大的簡化了創建JMS消息消費者的過程,下面的代碼段3就創建並配置了一個EJB容器所委托創建的JMS消息消費者。開發者現在可以很容易的實現MessageDrivenBean接口,並可以把它配置在EJB服務器中且可以用來創建一個可收集消息的商業組件。
代碼段3:
/**
*MessageDrivenBean接口由每一個消息驅動企業級組件類實現。
*這個容器使用MessageDrivenBean 方法來通知
*企業級Bean實例的實例生命周期事件
*/
public interface javax.ejb.MessageDrivenBean extends
javax.jms.MessageListener
{
/**
* 傳送一個消息給監聽者
*
* 參數 message :Message對象。
*/
public void onMessage(javax.jms.Message message);
/**
*容器在結束消息驅動對象的生命周期之前,調用這個方法。
*/
public void ejbRemove();
/**
*設置相關聯的消息驅動上下文。
*容器在創建了實例後調用這個方法。
* 企業版 Bean 實例將保存context對象的參考到一個實例對象中
*/
public void
setMessageDrivenContext(
javax.ejb.MessageDrivenContext context);
}
在代碼段4中給出了一個MessageDrivenBean實例的實現,在個組件從一個JMS TextMessage中取得一條字符串,並輸出,它是根據代碼段2種的JMS消息監聽者程序改編的。
代碼段: 4
/**
* 這個類是 MessageDrivenBean的一個實現。
*/
public class MyListenerMDB implements MessageDrivenBean
{
/**
* 這是一個無參數構造器,這樣 EJB容器可以使用Class.newInstance()方法來創建組件實例
*/
public MyListenerMDB()
{
}
/**
*這個方法接受消息實例並執行消息處理過程。
*
* 參數:message 。Message對象
*/
public void onMessage(Message message)
{
// onMessage 實現仍然未變:
// 從message對象中取出股票報價。
// StockQuoteProducer 發送 TextMessages
// 並在適合的時候放出該對象。
try
{
String quote = ((TextMessage)message).getText();
System.out.println("股票報價: " + quote);
}
catch(JMSException e)
{
System.out.println(
"不能處理消息: " + message);
}
}
/**
* 當MessageDrivenBean實例被從容器中拋出,該方法就被調用。
*/
public void ejbRemove() throws javax.ejb.EJBException
{
System.out.println(
"StockListenerMDB: ejbRemove被調用。");
}
/**
* 設置MessageDrivenContext實例。本方法將在組件實例化時被調用
*消息驅動上下文允許組件開發者訪問EJB容器的工具
*
* 參數ctx : 消息驅動上下文
*/
public void setMessageDrivenContext(
MessageDrivenContext ctx) throws javax.ejb.EJBException
{
System.out.println(
"StockListenerMDB: setMessageDrivenContext 被調用。");
}
/**
* ejbCreate with no args required by spec, though not
* enforced by interface
*/
public void ejbCreate()
{
System.out.println(
"StockListenerMDB: ejbCreate called.");
}
}
部署描述器
MessageDrivenBean可以使用XML部署描述器來指出受EJB服務器信息控制的運行時間動作,下面是部署描述器中定義MessageDrivenBean的有效的DTD元素。
<!ELEMENT message-driven (description?,
display-name?, small-icon?,
large-icon?, ejb-name?, ejb-class,
transaction-type, transaction-scope?,
jms-message-selector?, jms-acknowledge-
mode?, message-driven-destination?,
env-entry*, ejb-ref*, security-
identity?, resource-ref*, resource-
env-ref*)>
需要注意的是,部署描述器包含所有除用來部署MessageDrivenBean組件的目標名以外的所有信息,目標名被設置在一個應用程序服務器提供商指定的配置文件中或作為一個系統屬性。
在部署描述器中,配置器可以指定組件是傾向於用於主題還是用於隊列,並且,如果傾向於用於主題那麼組件是否應該擔當持久的簽署者( durable subscriber)的身份。像隊列一樣,持久的主題保證監聽者將接收到所有發布到這個主題的消息,即使監聽者可能一段時間都不可用。 持久的主題對應用程序的可靠性很重要。
我們的給出的MessageDrivenBean的部署描述器(見代碼段5)告訴容器這個組件是特意偵聽一個不持久主題。這個組件有使用 NotSupported方法事務屬性的容器管理事務限定。
代碼段5:
<ejb-jar>
<enterprise-beans>
<message-driven>
<ejb-name>MessageListenerMDB</ejb-name>
<ejb-class>messageListenerMDB</ejb-class>
<transaction-type>Container</transaction-type>
<transaction-scope>Local</transaction-scope>
<jms-acknowledge-mode>auto-acknowledge</jms-
acknowledge-mode>
<message-driven-destination>
<jms-destination-type>javax.jms.Topic</jms-
destination-type>
<jms-subscription-durability>nondurable</jms-
subscription-durability>
</message-driven-destination>
</message-driven>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>MessageListenerMDB</ejb-name>
<method-name>onMessage</method-name>
<method-params>
<method-param>javax.jms.Message</method-param>
</method-params>
</method>
<trans-attribute>NotSupported</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
使用MessageDrivenBean組件的另一個好處就是它的部署的簡潔性。 典型情況下,一個應用服務器供應商將提供定義組件部署描述器的工具,產生 EJB jar文件,並且部署組件。 一旦組件部署好了, EJB服務器將處理 EJB容器類的注冊並且開始 JMS連接。 因為 JMS定義了一個標准,一個基於 JNDI的機制,用於獲得 JMS主題,隊列,連接以及一個能夠使用任何JMS供應商實現的高端的應用程序服務器的引用。 這允許設計者在開發商業程序時最充分的利用應用程序服務器和JMS服務器。
MessageDrivenBean容器服務
EJB容器提供了下列服務,它們都是低級的EJB支持的服務,不能直接被使用EJB1.1的JMS開發者使用。
1.管理生命周期:
MessageDrivenBean的生命周期與配置它的EJB服務器的壽命是一致的,因為MessageDrivenBean是無狀態的,所以組件實例通常被EJB服務器集中起來,並且當一條消息變得對於作為消息消費者的主題或隊列可用的時候,會被容器取回,見圖5。
2、處理異常:
MessageDrivenBean組件當處理消息的時候,可能不會拋出應用程序異常,這就意味著可能被MessageDrivenBean拋出的唯一的異常指明一個系統錯誤的運行時間異常。容器將會處理這個異常,方法是刪除這個組件實例,並返回任何組件實例或容器啟動的事務處理。
3、線程或並發性:
MessageDrivenBean實例是在一個單獨的控制線程中執行,這將大大的簡化開發者的任務。EJB服務器將確保這一特性,另外,EJB服務器可能提供一種操作模式允許多消息被單獨的組件實例同時處理,這個配置選項實用定義在JMS規范中的“expert level”類。JMS提供者並不一定帶有這些類,所以EJB服務器可能不能利用每一種JMS實現的這種功能。
4、事務處理:
就像使用entity或session組件一樣,MessageDrivenBean組件也可以有“contain-or-bean-managed”(容器或組件管理)事務處理。一個事務屬性可以設置為MessageDrivenBean組件的onMessage()方法。因為沒有客戶端事務處理,所以只有一個事務處理屬性的子集考慮到entity和session組件與MessageDrivenBean組件有關系。使用了容器管理事務處理。容器將能支持MessageDrivenBean的Required和NotSupport事務處理屬性。一個帶有組件管理事務處理的MessageDrivenBean組件可以使用JTA UserTransaction對象。這個MessageDrivenBean組件在從onMessage()方法返回之前,必須先結束事務處理。onMessage()方法非常簡單,它帶有容器管理事務界限和NotSupported事務屬性。容器將不會創建事務並且組件開發者被禁止訪問 UserTransaction對象。
更有趣的事,當onMessage()方法被指定了Required事務屬性,這時容器將創建一個全局事務處理收集任何可參考的資源並被傳遞到任何其他的正在處理消息的EJB服務器上。這是唯一一種JMS主題或隊列接口包含於EJB事務處理中的情景。
對於使用Required事務屬性的組件,JMS服務器將成為XAResource,如果JMS服務器提供者不支持XA事務處理,JMS session通常將與容器的全局事務處理的結果同步。這樣,容器將不能包括用於管理分布事務的兩方面的委托處理的JMS服務器。JMS session的一個 rollback(反轉)將警告 JMS服務器,消息應當被重新發送。
5、消息確認:
容器總是處理MessageDrivenBean組件的消息收到的確認,對於組件來說,使用定義在 JMS規范中的客戶端消息收到確認方法是非法的。消息收到確認可以被設置為 DUPS_OK_ACKNOWLEDGE或 AUTO_ACKNOWLEDGE,前者允許在一次失敗之後投遞消息的副本,而後者提供一個嚴格的保證機制,確保消息只能被投遞一次。
6、安全性
因為MessageDrivenBean組件沒有客戶端,所以在接受消息的時候容器基本上不會產生安全問題。EJB2.0規范中為組件方法執行一個聲明指定的功能提供了方便。因此,MessageDrivenBean組件可以被設置來確保用於傳送到其他正在處理消息的EJB服務器的安全。這就可以使MessageDrivenBean組件維護方法級安全性了。
應用程序服務器框架
當 MessageDrivenBean提供的功能在應用程序服務器內被部署好後,它就會以指數形式膨脹開來。一個高端的應用程序服務器提供可擴展性,負載平衡,動態應用程序啟動,附加的 EJB服務器實例的動態配置以及容錯性,這些都是企業級應用程序的核心要素。
1、可擴展性: 企業級應用程序服務器的一個關鍵元素就在它的提供一個結構來適應不斷增加的處理負載的能力。 隨著被 MessageDrivenBean處理的消息的數量的增加,應用程序服務器的 EJB服務器必須使適應這些增加而不會明顯的增加處理時間。一般是提供給多應用程序服務器用於協調處理受到的消息的增多,這樣應用程序服務器就有了可擴展性了。
2、負載平衡:當一個請求發送到應用程序服務器,一個負載平衡元素可以在實例之間平均分配負載(見圖六)。舉例來說,應用程序服務器每個實例都可能包含一個EJB服務器的實例。使用應用程序服務器的負載平衡特性,受到的消息就可以平均分配到所有的EJB服務器實例上了。
3、動態應用程序啟動:應用程序服務器應當可以在負載增多時啟動附加的預先設置的實例。
4、動態設置額外的EJB服務器實例:即使一個應用程序被設定了固定的用於處理應用程序負載的實例數,但是也有可能信息流的激增造成系統處理速度變慢,為了處理這種情況,應用程序服務器必須具有配置附加應用程序實例的特性。比如說,應用程序服務器可以允許系統管理員動態定義附加的EJB服務器實例並把這些附加實例部署到EJB服務器中。負載平衡元素將利用這些補充的實例來處理消息。
5、容錯性:一個企業級的應用程序服務器必須也具有能夠適應出錯的情況,像網絡或硬件上的問題,但是前提是不能明顯降低系統性能或是丟失數據,應用程序服務器使用的一種方法就是通過設置多應用程序服務器實例來分配狀態信息,收到的消息通過智能化的負載平衡元素被分配到實例上。如果有一個或多個實例不能到達,負載平衡元素只要簡單的把消息重新分配給各個能用的實例就可以了。此外,負載平衡元素還可在管理組件的參與下重啟那些因為某些原因而不能使用的實例。
超越JMS
EJB的MessageDrivenBean組件部分給企業級服務器領域又添了一員猛將,它允許消息經由JMS接受在被一個簡單但又強有力的組件處理。然而,MessageDrivenBean的EJB組件事實上有能力變成能夠處理任何消息的組件模型,而不單是JMS服務器發來的消息。
JMS消息通過JMS實現被變成可交互操作,這就意味著JMS消息私地下是某一種特定的的實現,但是內容可以完全透明的被轉換成另一種特定的JMS實現而不會在任何方面影響消息消費者。因此任何消息格式都可以轉換成為JMS消息並被傳遞到MessageDrivenBean組件中。換句話說,一個MessageDrivenBean組件可以處理電子郵件、HTTP、FTP或其他任何協議發來的消息,以及又具有把這些協議轉化成為JMS消息的應用程序服務器提供的消息。這就開啟了一個標准、簡單、輕便且能夠處理任何協議發過來的任何消息的能力的組件模型之門。如果消息被定義成為可開發的、可擴展的語言向XML這樣的及其強大的可交互操作在寬松連接系統以一種每個人都能理解的模式完成。下面我想簡單以一個獨立於協議以外的消息處理的例子來驗證一下這種技術的強大威力。
一個B2B的例子
一個典型的B2B情景就像一個“中樞-輪輻”形,多個寬松連接的商家(即“輪幅”)與一個大型企業(即“中樞”)交互連接,在某種情況之下,當執行交互操作(像發送大的訂單這樣的事件)可以經由JMS應用程序在大企業與商家之間正常相互作用。對於一些小型商家來說,采用純JMS可能在成本上太高而負擔不起,它可能會采用通過Web浏覽器中的HTTP或發送電子郵件的形式向大企業發送訂單。為了向客戶開放B2B系統,大型企業級服務器就必須能夠處理這一系列不同的協議。
而且,作為中樞的大型企業級服務器雖然接受來自email或來自JMS等不同類型格式的訂單,但是只有必要為每一種方式分別編寫處理程序。使用MessageDrivenBean組件,任何協議都可以發送以XML格式描述的訂單,如下面的例子。MessageDrivenBean組件只需要把注意力集中在處理來自應用程序服務器發送的JMS TextMessage中的XML就可以了,而不要去關心到底所處理的消息時直接來自JMS服務器還是作為一條來自郵件服務器的SMTP消息。
代碼段六:
<purchase_order>
<client_number>101</client_number>
<date>August 8, 2001</date>
<billing_address>
<name>Wayne Zheng</name>
<address>xyz street</address>
<city>Hefei</city>
<province>Anhui</province>
<zip>230027</zip>
</billing_address>
<shipping_address>
<name>Zhang Tao</name>
<address>Huangshan Road</address>
<city>Hefei</city>
<state>Anhui</state>
<zip>230026</zip>
</shipping_address>
<items>
<item>
<quantity>1</quantity>
<product_number>324</product_number>
<description>Java Book</description>
<unitcost>19</unitcost>
</item>
<item>
<quantity>1</quantity>
<product_number>532</product_number>
<description>Java VM</description>
<unitcost>56</unitcost>
</item>
</items>
</purchase_order>
小結
EJB2.0提供了一種新的組件模型-MessageDrivenBean組件-用來處理異步消息,MessageDrivenBean組件可以簡單的開發和利用與其他EJB組件模型相同的容器服務,這種新的組件模型允許整合面向對象的中間件(MOM)的、優秀的以及新一代純Java的JMS。並且,MessageDrivenBean組件還有可能成為處理任何異步消息,特別是基於XML消息的工業標准。因此,MessageDrivenBean組件是EJB2.0中的非常重要的一員,本文只是初探其面目,以後我還會更加深入的探討其的功用。