消息服務是一種在分布式應用之間提供消息傳遞服務的軟件,具有可靠、異步、寬松結 合、語言中立、平台中立的特點,而且通常是可配置的。它的實現原理是:對發送者和 接收者之間傳遞的消息進行封裝,並在分布式消息客戶程序結合的位置加上一個軟件處 理層。消息服務為消息的客戶程序提供了一個接口,這個接口隔離了底層的消息服務, 使得各種不同的客戶程序能夠通過一個友好的編程接口方便地通信。 Java消息服務(Java Message Service,JMS)是一個Java API,它定義了消息的客戶程 序如何以一種標准化的形式與底層的消息服務提供者交互。JMS提供了一種接口,底層消 息服務提供者通過該接口向客戶程序提供JMS消息服務。JMS提供了點對點消息模式(Po int-to-Point)和發布-訂閱消息模式(Publish-Subscribe)。點對點消息模式通過一 個消息隊列實現,消息的生產者向隊列寫入消息,消息的消費者從隊列提取消息。發布 -訂閱消息模式通過一個話題(Topic)節點構成的層次結構實現,消息的生產者向這個 層次結構發布消息,消息的消費者向這個結構訂閱消息。 點對點消息模式具有如下特點: 每一個消息只有一個消費者。 消息的接收者和發送者之間不存在時間上的依賴關系。不論發送者發送消息時接收者是 否在運行,接收者都可以提取信息。 接收者對於成功處理的消息給出回執。 發布-訂閱消息模式具有如下特點: 每一個消息可以有多個消費者。 向某個話題訂閱的客戶程序只能收到那些在它訂閱之後發布的消息。為了接收到消息, 訂閱者必須保持活動狀態。因此,發布者和訂閱者之間存在時間上的依賴關系。 JMS API在一定程度上放寬了對這種依賴關系的要求,允許創建持久性訂閱(Durable S ubscription)。有了持久性訂閱,當訂閱者不活動時發送的消息也能接收到。 EJB 2.0規范定義了一種新的EJB類型,即消息驅動的EJB(Message-Driven EJB,簡稱M DB),它能夠以EJB的形式實現JMS消息的接收者。消息驅動的EJB實現一組新的接口,這 組接口使得EJB能夠異步地接收和處理JMS消息生產者發送到隊列或話題的消息。EJB客戶 程序的構造方式與普通JMS消息生產者的構造方式完全一樣,也就是說,JMS消息生產者 不必知道消息的消費者是一個EJB。 相對於會話Bean和實體Bean而言,消息驅動的Bean最大的特點是客戶程序不通過接口訪 問Bean。與會話Bean和實體Bean不同,消息驅動的Bean只有一個Bean類。從某些方面看 ,消息驅動的Bean類似於無狀態會話Bean: 消息驅動的Bean不為特定的客戶保留數據或對話狀態。 一個消息驅動Bean的所有的實例都是等價的,這使得容器能夠把消息指派給任意一個消 息驅動Bean的實例。容器能夠建立消息驅動Bean的緩沖池,實現消息的並發處理。 一個消息驅動的Bean能夠處理來自多個客戶程序的消息。 消息驅動Bean的實例變量可以在處理客戶消息期間包含一些狀態信息,例如JMS連接、打 開的數據庫連接,或者是對EJB對象的引用。當一個消息到達,容器調用消息驅動Bean的 onMessage()方法處理消息。onMessage()方法通常把消息定型(cast)成為五種JMS消息 類型之一,然後按照應用的業務邏輯的要求處理消息。 傳遞給消息驅動Bean的消息可能處於一個事務之內,這時,onMessage()方法內的所有操 作都屬於該事務的一部分。如果消息處理結果被回退,則系統將再次投遞該消息。 哪些時候應該使用消息驅動的Bean呢?會話Bean和實體Bean能夠發送JMS消息,能夠同步 接收消息,但不能異步接收。一些時候,為防止過多地占用服務器資源,在服務器端的 組件中,我們想要避免阻塞,這時,我們可以用消息驅動的Bean異步接收消息。 二、MDB體系結構 圖一描述了消息驅動的Bean組件的基本體系結構。 在圖一中,位於頂端的是javax.ejb.EnterpriseBean接口,它是所有EJB的基礎接口。E nterpriseBean接口派生出了javax.ejb.MessageDrivenBean接口,所有消息驅動的EJB類 必須實現javax.ejb.MessageDrivenBean接口。此外,消息驅動的Bean必須實現javax.j ms.MessageListener接口。公用的、非最終的、非抽象的消息驅動的EJB,比如圖一顯示 的MyMessageDrivenEJBean,必須同時實現MessageListener接口和MessageDrivenBean接 口。消息驅動的EJB與其他類型的EJB不同,它們不把業務方法導出給客戶程序,它們關 心的只是遵從EJB容器的接口要求。由於這個原因,消息驅動的Bean必須有一個不需要參 數的公用構造方法(ejbCreate()方法),而且不應該實現finalize()方法。 2.1 MDB接口 在消息驅動的Bean中,setMessageDrivenContext()方法用來把一個MessageDrivenCont ext的對象實例傳遞給EJB,它是MessageDrivenBean接口定義中容器調用的第一個方法。 MessageDrivenContext對象封裝了一個EJB消息驅動容器上下文的接口,支持消息驅動的 EJB實例訪問容器提供的運行時消息驅動上下文 對於消息驅動的EJB來說,關鍵之一是要實現一個沒有參數的ejbCreate()方法。當EJB容 器准備創建消息驅動EJB的實例時,它將調用這個方法。容器之所以決定創建某個EJB的 實例,可能是因為它要構造一個Bean實例的緩沖池,也可能是因為它接收到了客戶的請 求。這個ejbCreate()方法和其他Bean上的EJB構造方法類似,屬於EJB實現的一種特殊的 構造函數或初始化方法。 當EJB容器准備不讓Bean實例繼續處理客戶程序的請求時,它就會調用消息驅動Bean的e jbRemove()方法。何時在消息驅動的Bean上調用ejbRemove()方法由EJB容器單獨決定, 不受EJB客戶程序的任何約束。應當注意的是,容器並不保證一定調用ejbRemove()方法 。在正常操作時,容器會調用ejbRemove()方法;但是,當消息驅動的Bean向容器拋出了 系統異常時,不能保證ejbRemove()方法一定會被調用。由於這個原因,Bean開發者必須 按時檢查和清除Bean分配的所有資源。 對於Bean開發者來說,最重要的任務也許是實現onMessage()方法。當一個異步消息必須 由Bean實例處理時,容器將調用onMessage()方法。onMessage()方法的參數是一個普通 的JMS javax.jms.Message的實例,消息驅動的EJB實例從這個Message的實例提取待處理 的數據完成消息處理。 2.2 JMS消息接口 那麼,在onMessage()方法調用傳入的 JMS消息中,消息驅動的Bean如何提取信息,可以 提取哪些信息呢?圖二描述了基本JMS消息類型的核心接口和概念。在一個以JMS為基礎 的消息系統中,Message接口是在系統中傳遞的所有消息的最基本的接口(或稱之為根接 口,Root Interface)。Destination接口描述了消息傳遞的一個終端;類似地,由於消 息有一個傳遞模式,所以圖二還顯示了Message接口與DeliveryMode接口的概念上的關系 。 JMS消息的頭信息可以通過一組標准的方法設置或提取,這組標准方法的名字為getJMSX XX()或setJMSXXX()形式(下面我們分別稱之為get方法和set方法),其中XXX是消息頭 信息中的屬性名字,例如getJMSDeliveryMode()方法。在Message接口中,通過get方法 和set方法操作的標准頭信息屬性包括:唯一的消息ID,時標(Timestamp),答復和目 標地址,消息傳遞模式,消息類型,以及消息的優先級。 在JMS消息中,JMS容器提供者特有的屬性可以通過getXXXProperty()方法提取,或通過 setXXXProperty()方法設置,其中XXX表示屬性的類型,例如byte getByteProperty(ja va.lang.String name)。每一個屬性有一個通過String對象指定的名字和相應的值。名 字以JMSX前綴開頭的屬性作為標准JMS屬性保留。 與消息正文數據(或稱之為消息體,與消息頭相對而言)的五種類型對應,五種消息類 型擴展了Message接口,如圖三所示。Byte數據由BytesMessage封裝,Serializable對象 由ObjectMessage封裝,String消息由TextMessage封裝,鍵-值對由MapMessage封裝,I /O流由StreamMessage封裝。 這些派生消息類型上的方法為特定類型的消息正文定義了get和set操作,而在Message基 礎接口內,有一個通用的clearBody()方法,這個方法清除消息的正文,並把它置入只寫 模式。clearBody()方法只清除消息正文,不清除消息頭或屬性。如果消息正文是只讀的 ,調用該消息後,消息正文的狀態將和新消息的空白正文狀態一樣。 2.3 MDB客戶程序接口 消息驅動Bean的客戶程序並不知道接收端實際處理消息的將是一個EJB。事實上,消息驅 動Bean的客戶程序的構造方法與普通JMS客戶程序的構造方法完全一樣。 JMS的核心體系如圖四所示。從圖中我們可以看出,JMS ConnectionFactory(連接工廠 )初始上下文通過Java名稱和目錄接口(Java Naming and Directory Interface,JND I)創建。隨後,連接工廠將用來創建與JMS服務提供者的連接。有了JMS連接,我們就可 以獲得創建消息生產者和消息消費者的會話(Session)。實際上,消息驅動Bean的客戶 程序就是消息的生產者,它發送的消息將由消息驅動的Bean(即消息消費者)接收。 三、點對點消息隊列模式 圖五顯示了在一個支持點對點消息隊列的系統中JMS的基本體系結構。消息隊列體系實際 上是核心JMS體系的一個擴展,特別地,它加入了對消息隊列功能的支持。連接工廠、連 接、會話、消息生產者、消息消費者等都用點對點消息隊列形式的接口進行了擴展。 JMS客戶程序利用JNDI獲得一個QueueConnectionFactory對象的引用。隨後,我們用Que ueConnectionFactory.createQueueConnection()方法之一創建一個QueueConnection對 象的實例。調用createQueueConnection()方法時可以指定一個用戶名字和密碼,或者, 我們也可以使用該方法不帶參數的版本,此時假定使用默認用戶身份。 QueueConnection接口是Connection接口的一種子類型,它代表著一個與JMS點對點消息 隊列服務的連接。JMS客戶程序調用createQueueSession()方法創建QueueSession的實例 ,createQueueSession()方法調用中一個boolean類型的參數指定了QueueSession對象是 否要提供事務支持。另外,回執的模式也在createQueueSession()調用中通過參數指定 ,這個參數的值可以是三個靜態的標識符之一:AUTO_ACKNOWLEDGE,CLIENT_ACKNOWLED GE,DUPS_OK_ACKNOWLEDGE。 QueueSession.createQueue()方法返回一個Queue對象的實例,調用Queue.getQueueNam e()方法可以返回隊列的名字。 QueueSession.createSender()方法創建一個QueueSender消息生產者,利用QueueSende r可以把消息發送到Queue。消息可以通過各種不同的QueueSender.send()方法發送到Qu eue,這些不同的send()方法能夠把消息發送給QueueSender對象關聯的Queue對象,或者 發送給send()方法調用中指定的Queue對象。消息遞送模式、優先級、消息的有效時間都 可以在調用QueueSender.send()方法時指定。 發送給Queue的消息可以用Session接口中定義的各種消息構造方法創建。 四、發布-訂閱消息模式 圖六顯示了在一個支持發布-訂閱消息模式的系統中JMS的基本體系結構。發布-訂閱消息 機制也是核心JMS機制的一種擴展,增加了一些適合發布-訂閱消息模式的功能。連接工 廠、連接、會話、消息生產者、消息消費者等都用發布-訂閱形式的接口進行了擴展。 JMS客戶程序通過JNDI獲得一個TopicConnectionFactory對象的引用。TopicConnection Factory.createTopicConnection()方法用來創建TopicConnection對象的實例。調用cr eateTopicConnection()方法時可以指定一個用戶名字和密碼,或者,我們也可以使用該 方法不帶參數的版本,此時假定使用默認用戶身份。 TopicConnection接口是Connection接口的一種子類型,它代表著一個與JMS發布-訂閱消 息服務的連接。JMS客戶程序調用TopicConnection.createTopicSession()方法創建Top icSession的實例。會話的事務支持和回執模式也在創建TopicSession時指定。 TopicSession.createTopic()方法返回一個Topic對象的實例。Topic接口封裝了一個話 題目的地,發布者向該目的地發送消息,訂閱者從該目的地接收消息。不同的服務提供 者按照不同的方式實現話題名稱的層次結構,調用Topic.getTopicName()方法可以獲得 話題的String形式的描述。 TopicSession.createPublisher()方法創建一個TopicPublisher消息生產者,它用來把 消息發布到Topic。消息可以通過各種不同的TopicPublisher.publish()方法發布到Top ic,這些不同的publish()方法能夠把消息發送給TopicPublisher對象關聯的Topic對象 ,或者發送給publish()方法調用中指定的Topic對象。消息遞送模式、優先級、消息的 有效時間都可以在調用TopicPublisher.publish()方法時指定。 發送給Topic的消息可以用Session接口中定義的各種消息構造方法創建。 五、實例 本示例應用是一個消息驅動Bean應用的簡單例子,由以下兩部分構成: SimpleMessageClIEnt:J2EE應用客戶程序,向隊列發送消息。 SimpleMessageEJB:一個消息驅動的Bean,異步地接收和處理由客戶程序發送到隊列的 消息。 圖七描述了這個應用的結構。客戶端應用把消息發送到隊列,隊列由管理員通過j2eead min命令創建。JMS提供者(這裡是J2EE服務器)把消息傳遞給消息驅動Bean的實例,由 Bean的實例處理消息。 該示例應用的完整代碼可以從本文最後下載。 5.1 客戶端 SimpleMessageClIEnt把消息發送到SimpleMessageBean監聽的隊列。客戶程序首先確定 連接工廠和隊列: queueConnectionFactory = (QueueConnectionFactory) jndiContext.lookup ("java:comp/env/jms/MyQueueConnectionFactory"); queue = (Queue) jndiContext.lookup("java:comp/env/jms/QueueName"); 接下來,客戶程序創建隊列連接、會話和一個消息發送器: queueConnection = queueConnectionFactory.createQueueConnection(); queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); queueSender = queueSession.createSender(queue); 最後,客戶程序把幾個消息發送到隊列: message = queueSession.createTextMessage(); for (int i = 0; i < NUM_MSGS; i++) { message.setText("我是" + msgArray[i] ); System.out.println("Sending message: " + message.getText()); queueSender.send(message); } 5.2 MDB組件 SimpleMessageEJB類闡明了編寫消息驅動Bean類的要求: 實現MessageDrivenBean接口和MessageListener接口。 類定義為public類型。 類不能定義成abstract或final。 實現一個onMessage()方法。 實現一個ejbCreate()方法和一個ejbRemove()方法。 包含一個public類型的不需要參數的構造方法。 不能定義finalize()方法。 與會話Bean和實體Bean不同,消息驅動的Bean不定義客戶程序訪問的接口。客戶程序不 是先定位消息驅動的Bean,再調用這些Bean上的方法。雖然消息驅動的Bean沒有業務方 法,但它們可以包含由onMessasge()方法內部調用的輔助方法。 當隊列接收到一個消息,EJB容器將調用消息驅動Bean的onMessage()方法。在SimpleMe ssageBean類中,onMessage()方法把接收到的消息定型(cast)成為TextMessage類型, 然後顯示出文本信息: public void onMessage(Message inMessage) { TextMessage msg = null; try { if (inMessage instanceof TextMessage) { msg = (TextMessage) inMessage; System.out.println("MESSAGE BEAN:收到消息: " + msg.getText()); } else { System.out.println("消息類型錯誤: " + inMessage.getClass().getName()); } } catch (JMSException e) { e.printStackTrace(); mdc.setRollbackOnly(); } catch (Throwable te) { te.printStackTrace(); } } 消息驅動Bean的ejbCreate()方法和ejbRemove()方法必須符合以下要求: 訪問控制修飾符必須是public。 返回值類型必須是void。 不能有static和final修飾符。 throws子句不能定義任何應用自定義的異常。 不能帶有參數。 在SimpleMessageBean類中,ejbCreate()方法和ejbRemove()方法都是空的,不執行任何 有實際意義的操作。 5.3 打包 接下來我們要把上面的應用打包成一個J2EE EAR文件。首先要把SimpleMessageEJB打包 成Jar文件。通常,打包過程可以通過工具完成,但理解模塊部署描述器仍是必要的。在 EJB應用模塊部署描述器中,頂級元素下面包含元素。下面可以包含一組元素(按照EJB 2.0新規范),每一個元素描述一個消息驅動Bean的配置和部署。 SimpleMessageEJB的ejb-jar.XML文件如下所示。元素內定義了消息驅動Bean的配置和部 署信息,例如唯一的Bean名字、Bean類的名字、配置參數、安全信息、事務信息、消息 目的地類型等。 ...... SimpleMessageJAR SimpleMessageEJB SimpleMessageEJB SimpleMessageBean Container javax.jms.Queue SimpleMessageEJB Bean onMessage Javax.jms.Message Required 除了ejb-jar.XML部署描述器之外,通常還要有面向特定平台和環境的部署描述器。大多 數時候,這種描述器可以用GUI工具編寫。請參見下載代碼中提供的例子。 打包好各個模塊之後,接著還要把J2EE應用打包成EAR文件。有關這一步驟的詳細說明, 請參見開發平台的相關文檔。本文以後有關部署和運行的說明,就以打包後的EAR文件為 基礎。 5.4 部署和運行 假設我們在Sun的J2EE參考實現上部署和測試這個示例應用。為便於查看消息驅動Bean的 輸出,我們必須以-verbose模式啟動服務器: j2ee -verbose 用下面的j2eeadmin命令創建隊列: j2eeadmin -addJmsDestination jms/MyQueue queue 驗證隊列已經創建成功: j2eeadmin -listJmsDestination 啟動deploytool,選擇菜單“File-->Open”,打開SimpleMessageApp.ear文件。接著, 選擇菜單“Tools --> Deploy”,部署應用。出現部署提示時,選中“Return Client JAR”檢查框。 在一個命令窗口中,進入EAR文件(SimpleMessageAppClient.jar文件)所在目錄,把環 境變量APPCPATH設置為SimpleMessageAppClient.jar。然後,執行下面的命令: runclient -client SimpleMessageApp.ear -name SimpleMessageClIEnt -textauth 在登錄提示中,輸入用戶名字j2ee,輸入密碼j2ee。此時,客戶程序將輸出以下內容: Sending message: 我是老大孫悟空 Sending message: 我是老二豬八戒 Sending message: 我是老三沙和尚 在啟動J2EE服務器的命令窗口,我們可以看到如下輸出