在本文中,我將概述支持 1.4 版 J2EE 規范引入的消息所需的改變。我將特別分析 JMS 1.1 的需求以及使用時的新限制、與消息目標有關的新概念、以及 EJB 規范 2.1 版本為消息驅動 bean 所帶來的顯著變化。這些內容對於這些開發人員和管理人員特別有用:他們熟悉 J2EE 1.3 中的消息、並且希望編寫新的應用程序或者將現有消息應用程序移植到兼容 J2EE 1.4 的應用服務器上。
JMS 1.1
兼容 J2EE 的應用服務器現在需要支持 1.1 版的 Java 消息服務(Java Message Server,JMS)規范,這也許是新版本 J2EE 規范中最明顯的改變。JMS 1.1 完全向後兼容 J2EE 1.3 規范所要求的 JMS 1.1,所以應當不需要改變現有的應用程序。JMS 1.1 引入了統一消息域,在 Bobby Woolf 的 這篇文章 中對它做了詳細的討論。值得強調的是,除非需要向後兼容性,否則沒有理由用老的隊列和主題接口編寫新的 JMS 應用程序。新的應用程序應當只使用新的統一接口,如清單 1 中的例子所示。
清單 1. 展示統一 JMS 接口的例子
InitialContext context = new InitialContext();
ConnectionFactory factory =
(ConnectionFactory) context.lookup("java:comp/env/jms/cf");
Destination source =
(Destination) context.lookup("java:comp/env/jms/source");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(source);
Message message = consumer.receive();
connection.close();
正如 Bobby 在他的文章中所說的,統一接口不僅簡化了消息編程模型,它們還使應用程序可以用一個事務 Session 接收來自隊列的消息,並向主題發送消息(或者相反)。這意味著可以在同一個事務工作單元中進行發送和接收,而無需借助於 bean 或者容器管理的事務。
J2EE 對 JMS 使用的限制
像以前版本的 J2EE 規范一樣,1.4 版對 JMS 的使用有一些限制。我將在本節分析這些限制。
限制的接口
盡管許多開發人員可能不知道,但是 J2EE 規范對於 J2EE 應用程序如何使用 JMS API 總是有一些限制。例如,以下接口用於 JMS 提供者與應用服務器之間的集成(它們是在 JMS 規范中描述的 Application Server Facilities 的一部分),因而不能被應用程序使用:
javax.jms.ServerSession
javax.jms.ServerSessionPool
javax.jms.ConnectionConsumer
All javax.jms.XA interfaces
限制的方法
對於那些“應用程序組件在阻止創建線程的容器中不能執行”的方法,J2EE 1.3 規范給出了讓人困惑的聲明。EJB 容器不允許應用程序創建線程,但是一個 Web 容器 可以 允許應用程序這樣做。結果,在 J2EE 1.3 中,根據所使用的 Web 容器,可以調用或者不可以調用這些方法。幸運的是,J2EE 1.4 規范刪除了這些令人困惑的說法,只是說明下列方法只能被運行在客戶容器中的應用程序所使用——換句話說,它們不會在 Web 或者 EJB 容器中調用:
javax.jms.ServerSession method setMessageListener()
javax.jms.ServerSession method getMessageListener()
javax.jms.Session method run()
javax.jms.QueueConnection method createConnectionConsumer()
javax.jms.TopicConnection method createConnectionConsumer()
javax.jms.TopicConnection method createDurableConnectionConsumer()
javax.jms.MessageConsumer method getMessageListener()
javax.jms.MessageConsumer method setMessageListener()
javax.jms.Connection method setExceptionListener()
javax.jms.Connection method stop()
javax.jms.Connection method setClientID()
這些方法中的前六個也屬於 Application Server Facilities,因此將它們排除在外是合理的,但是其他的方法呢?禁止 setMessageListener() 和 getMessageListener() 的決定會給大多數應用程序開發人員帶來最大的問題。這些方法用於注冊 MessageListener ,這樣當消息到達目標時就會調用其 onMessage() 方法。在 Web 和 EJB 容器中不允許它的原因是因為 上下文。調用 EJB 和 servlet 方法時,容器會保留與線程相關聯的上下文,如當前事務和安全 principal。JMS 提供者調用 MessageListener 時,應用服務器沒有辦法攔截這個調用並添加適當的上下文。應當用同步方法 receive() 輪詢消息或者考慮使用消息驅 bean 替代這些方法。
盡管上面列出的最後三個方法在以前版本的 J2EE 應用程序也是禁止的,但是您仍然想知道為什麼不允許它們。這要追溯到應用服務器管理連接的方式。它可能希望在不同的應用程序之間共享同一個連接,如果應用程序可以調用改變連接的狀態的方法,那麼它就不能這樣做了。只有在使用 MessageListener 時 setExceptionListener() 和 stop() 方法才是有用的,因此,不能調用它們不是個問題,而應當能夠在管理式地定義連接工廠時設置一個客戶標識符。
每個 Connection 有一個 Session
還有一個新限制可能使現有的應用程序無法使用。J2EE 規范現在規定一個應用程序中,對每一個 Connection 只能有一個活動的(未關閉) Session 。換句話說,調用了 createSession() 後,如果在關閉原來的 Session 之前再次調用它,就會拋出一個異常。
規范沒有解釋為什麼會增加這個限制,不過我將給出我的理論。最新版本的 Java Connector Architecture (JCA) 規范建議 JMS 提供者可以實現為 JCA 資源適配器。在 JCA 編程模型中,就像 JDBC 一樣,只有兩個對象( ConnectionFactory 和 Connection ),而在 JMS 中有三個( ConnectionFactory 、 Connection 和 Session )。更重要的是,在 JCA 中,與事務相關聯的是 Connection ,而在 JMS 中是 Session 。因而,如果在 JMS 中允許每個 Connection 有多於一個 Session ,那麼 Connection 實際上就會與多個事務相關聯。總之,這種限制使得 JMS 提供者更容易實現為 JCA 資源適配器。
那麼所有這些對應用程序開發人員有什麼意義呢?在過去,您也許曾經試圖利用 JMS Connection 對象的多線程本性,並將它們緩沖到 EJB 組件或者 servlet 的靜態變量中。與之相反,現在應當只是將 Connection 做為 EJB 組件的一個實例變量緩沖,並保證在任何時刻只有一個會話,或者只在每次需要 Session 時創建一個 Connection 。大多數應用服務器會對對象實現某種緩沖池,所以它沒有您所擔心的那麼昂貴(只是要記住在完成 Connection 時關閉它們)。
消息目標
使用 JMS 的 J2EE 應用程序通常分為兩個陣營:
使用 JMS 與後端系統通信的應用程序。
使用 JMS 提供與應用程序的不同部分異步通信的應用程序。
在這一節中,我將描述在 J2EE 規范中的一個改變,它使第二種類型的應用程序更容易部署。作為例子,我將展示兩個 EJB 組件,它們通過彼此發送消息進行通信。在 J2EE 1.3 中,每個 bean 都要在其部署描述符中定義一個 resource-env-ref ,它在以後要在其本地命名空間中查詢它。在清單 2 所示的例子中, SenderEJB 將從 java:/comp/env/jms/target 中查詢其目標,而 ReceiverEJB 將從 java:/comp/env/jms/source 中查詢其目標。因為引用在不同的本地命名空間中,所以沒有一種機制讓應用程序匯編器(application assembler )向部署人員表明這些 resource-env-ref s 實際上應當綁定到同一個目標。
清單 2. 顯示 resource-env-ref 的部署描述符代碼片段
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>SenderEJB</display-name>
...
<resource-env-ref>
<resource-env-ref-name>jms/target</resource-env-ref-name>
<resource-env-ref-type>javax.jms.Queue</resource-env-ref-type>
</resource-env-ref>
...
</session>
<session>
<ejb-name>ReceiverEJB</display-name>
...
<resource-env-ref>
<resource-env-ref-name>jms/source</resource-env-ref-name>
<resource-env-ref-type>javax.jms.Queue</resource-env-ref-type>
</resource-env-ref>
...
</session>
</enterprise-beans>
</ejb-jar>
在 J2EE 1.4 中,仍然可以使用 resource-env-ref 定義目標,不過,還有一個與 resource-env-ref 非常類似的新元素 message-destination-ref ,它還有兩個子元素。其中第一個是 message-destination-usage ,正如它的名字所表明的,它用於向部署人員表明應用程序准備如何使用目標。它可以取以下的值中的一個:
Produces
Consumes
ProducesConsumes
因而部署人員通常會將 Produce 的引用和另一個 Consume 的引用綁定到同一個目標上。
增加的第二個元素是 message-destination-link 。它可用於將兩個或者更多 message-destination-ref 聯系到一起。在 link 中包含的名字必須與在另一個新元素—— message-destination ——中給定的名字相匹配。部署應用程序後,部署人員將一個 message-destination 綁定到一個 JMS 目標,所有 message-destination-ref s 都使用這同一個綁定。
因此在 J2EE 1.4 中,清單 2 中的例子現在可以改寫為如清單 3 所示的形式。
清單 3. 帶有 message-destination-ref 和 message-destination 元素的部署描述符代碼片段
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>SenderEJB</display-name>
...
<message-destination-ref>
<message-destination-ref-name>jms/target</message-destination-ref-name>
<message-destination-type>javax.jms.Queue</message-destination-type>
<message-destination-usage>Produces</message-destination-usage>
<message-destination-link>destination</message-destination-link>
</message-destination-ref>
...
</session>
<session>
<ejb-name>ReceiverEJB</display-name>
...
<message-destination-ref>
<message-destination-ref-name>jms/source</message-destination-ref-name>
<message-destination-type>javax.jms.Queue</message-destination-type>
<message-destination-usage>Consumes</message-destination-usage>
<message-destination-link>destination</message-destination-link>
</message-destination-ref>
...
</session>
</enterprise-beans>
<assembly-descriptor>
...
<message-destination>
<message-destination-name>destination</message-destination-name>
</message-destination>
...
</assembly-descriptor>
</ejb-jar>
為了強調這兩種新元素所造成的不同,請看以下兩圖,在每個圖中,EAR 文件用藍顏色遮蔽,表示應用程序匯編器的影響范圍。應用程序部署人員添加紅色箭頭以將在應用程序中定義的資源綁定到實際的 JMS 隊列。在圖 1 中,可以看到應用程序匯編器有兩個 resource-env-ref ,每個 EJB 組件有一個,並必須保證部署人員知道它們應當綁定到同一個 JMS 目標。
圖 1. 使用 resource-env-ref 的示例應用程序
與此相反,在圖 2 中,應用程序匯編器可以通過將兩個 message-destination-ref 鏈接到同一個 message-destination ,而清楚地表明他或者她的意圖。之後部署人員只需要添加一個綁定。
圖 2. 使用 message-destination 的示例應用程序
與 ejb-link 使用的語法相同,還可以鏈接到在同一應用程序中不同 JAR 文件中定義的 message-destination 。例如, <message-destination-link> ../other/other.jar#destination </message-destination-link> 會用相對路徑 ../other/other.jar 上的名字 destination 鏈接到 message-destination 。還可以設置 message-destination-link 表明消息驅動 bean 應當在其中使用消息的目標,我將在下一節中更詳細地討論這些內容。
消息驅動 bean
本文介紹的 J2EE 消息系統的最後一個改變是消息驅動 bean (MDB) 的改變。在 EJB 2.0 規范中,MDB 必須實現接口 javax.jms.MessageListener 。在 EJB 2.1 規范中,已經不再嚴格要求了,因此 MDB 可以實現 任何接口,只要有人准備好向它傳送消息!對於 JMS 提供者,一般仍然實現 javax.jms.MessageListener ,的確不需要改變 MDB 代碼就可以接納新的規范。不過為了支持其他接口,部署描述符有了很大改變。
清單 4 展示了 EJB 2.0 應用程序的 MDB 部署描述符的一個典型例子。清單 5 展示了同一 MDB 用在 EJB 2.1 應用程序中時,部署描述符會是什麼樣子。
清單 4. EJB 2.0 部署描述符
<ejb-jar>
<enterprise-beans>
<message-driven>
<ejb-name>DurablePubSubMDB</ejb-name>
<ejb-class>example.ExampleMDB</ejb-class>
<transaction-type>Bean</transaction-type>
<acknowledge-mode>Auto-acknowledge</acknowledge-mode>
<message-driven-destination>
<destination-type>javax.jms.Topic</destination-type>
<subscription-durability>Durable</subscription-durability>
</message-driven-destination>
<message-selector>
JMSType = 'person' AND gender = 'male'
</message-selector>
</message-driven>
</enterprise-beans>
</ejb-jar>
清單 5. EJB 2.1 部署描述符
<ejb-jar>
<enterprise-beans>
<message-driven>
<ejb-name>DurablePubSubMDB</ejb-name>
<ejb-class>example.ExampleMDB</ejb-class>
<messaging-type>javax.jms.MessageListener</messaging-type>
<transaction-type>Bean</transaction-type>
<activation-config>
<activation-config-property>
<activation-config-property-name>
acknowledgeMode
</activation-config-property-name>
<activation-config-property-value>
Auto-acknowledge
</activation-config-property-value>
</activation-config-property>
<activation-config-property>
<activation-config-property-name>
destinationType
</activation-config-property-name>
<activation-config-property-value>
javax.jms.Topic
</activation-config-property-value>
</activation-config-property>
<activation-config-property>
<activation-config-property-name>
subscriptionDurability
</activation-config-property-name>
<activation-config-property-value>
Durable
</activation-config-property-value>
</activation-config-property>
<activation-config-property>
<activation-config-property-name>
messageSelector
</activation-config-property-name>
<activation-config-property-value>
JMSType = 'person' AND gender = 'male'
</activation-config-property-value>
</activation-config-property>
</activation-config>
</message-driven>
</enterprise-beans>
</ejb-jar>
首先要注意的是新的 messaging-type 元素,它給出了 MDB 實現的接口類的名字。其次是刪除了 acknowledge-mode 、 destination-type 、 subscription-durability 和 message-selector 。取而代之的是一組通用的名-值對。對於 JMS 提供者,JCA 規范建議支持表 1 中給出的名字和值。
表 1. 建議的 JMS activation-config-property 值 activation-config-property-name activation-config-property-value destinationType javax.jms.Queue or javax.jms.Topic acknowledgeMode Auto-acknowledge (default) or Dups-ok-acknowledge subscriptionDurability NonDurable (default) or Durable messageSelector String selector
不幸的是,這些屬性只是建議性的,不是每一個 JMS 提供者都一定支持它們。如何知道所使用的提供者支持哪些屬性呢?在 JCA 規范的“Message Inflow”一章中,規定了 JMS 提供者與向 MDB 發送消息的應用服務器之間的協議。作為這個協議的一部分,JMS 提供者實現一個稱為 ActivationSpec 的 JavaBean 組件,它的屬性包含那些提供者發送消息所需要的信息。管理員可以為這些屬性定義默認值,但是部署包含 MDB 的應用程序時,在 MDB 的部署描述符中定義的任何 activation-config-property 元素都會覆蓋它們。因而遵守該建議的 JMS 提供者在 ActivationSpec 上有名為 destinationType 、 acknowledgeMode 、 subscriptionDurability 和 messageSelector 的屬性。
注意在這個新模型中,可以不讓屬性出現在部署描述符中,而管理式地定義它。另一方面,可能有另外一些特定於提供者的屬性,以前必須管理式地定義,現在則可加入到 MDB 的部署描述符中(盡管這顯然降低了應用程序的可移植性)。
建議 JMS ActivationSpec 具備的另一個屬性是 destination 。不幸的是,JCA 規范沒有定義這個屬性的類型或者它的意義。例如,它可以是要求部署人員為目標指定某個特定於提供者的名字的字符串。一種特殊的情況是屬性的類型是 javax.jms.Destination 。在這種情況下,應用程序匯編器可以在 MDB 的部署描述符中指定一個 message-destination-link ,以表明 MDB 應當從中收取消息的 message-destination 。這個鏈接不是 message-destination-ref 的一部分,它與 message-destination-type 都定義為 message-driven 元素的直接子元素,如清單 6 的示例部署部署描述符所示。在運行時,應用服務器可以將鏈接解析為一個 JMS 目標對象、並將它設置到 ActivationSpec 的 destination 屬性中。
清單 6. 顯示一個 MDB message-destination-link 的 EJB 2.1 部署描述符
<ejb-jar>
<enterprise-beans>
<message-driven>
<ejb-name>DurablePubSubMDB</ejb-name>
<ejb-class>example.ExampleMDB</ejb-class>
<messaging-type>javax.jms.MessageListener</messaging-type>
...
<message-destination-type>
javax.jms.Queue
</message-destination-type>
<message-destination-link>
destination
</message-destination-link>
...
</message-driven>
...
</enterprise-beans>
...
</ejb-jar>
結束語
在 J2EE 1.4 規范中,JMS 應用程序有許多改變和新功能。有一些改變,比如在 Web 容器上使用 setMessageListener() 的更嚴格限制,限制每個 Connection 有一個 Session ,以及有很大變化的 MDB 部署描述符,它們將要求修改現有的應用程序。其他改變,如統一的 JMS 1.1 API,則會簡化新應用程序的編程並擴展現有的功能。不論是否正在移植 J2EE 1.3 應用程序或者有機會編寫新的應用程序,我希望隨著越來越多的 J2EE 1.4 應用服務器出現,您會發現本文會很有用。