ActiveMQ 二

本章內容

·         企業消息傳送及面向消息中間件 

·         理解Java消息服務JMS

·         使用JMS APIs發送和接收消息

·         一個消息驅動bean例子

    爲了幫助你理解ActiveMQ的意義,瞭解企業消息傳送背景和歷史是很重要的。討論完企業消息傳送,你將可以通過一個小例子瞭解JMS及其使用。這章的目的是簡要回顧企業消息傳送及JMS規範。如果你已經熟悉這些主題,你可以跳過直接到下一章去。

    軟件開發者經常需要在兩個系統之間交流或搬運數據。這種問題有很多解決辦法。但限於你的條件和需求,選擇一種解決方案是一個大決定。商業需求往往有嚴格的限制條件,直接影響你的決定的有性能,擴展性,穩定性等。我們日常使用的很多系統都有這樣的要求,比如ATM系統,航班預訂系統,信用卡系統,單點銷售系統,通信系統等。如果我們的日常生活沒有這些系統會怎樣?

    用一小會的時間想一下這些服務給我們的生活帶來了怎樣的便利。這些及其它類似應用能夠方便我們的生活在於他們的穩定和安全性。在這些應用場景背後,是很多應用組合起來的,通常是分佈式的,它們之間相互傳遞事件或消息。即使是最複雜的金融交易系統也是用這種方式集成的,完全通過消息傳送來發送和接收商業信息。

    由於各種各樣的理由,很多產品都提供消息傳送。必要性是發明之母,這就是消息中間件產生的原因。當時需要一種軟件,能夠在各種不同的數據類型,操作系統,協議甚至是不同的編程語言中交流數據。並且,複雜的消息路由和轉移也成爲這種解決方案的一部分必備能力。

    ActiveMQ就是一個MOM產品,它爲上面提到的商業系統提供異步消息傳送。ActiveMQ通過實現JMS規範提供了這樣一種可靠性和擴展性。

 

2.1介紹企業消息傳送

    向上面提及的大多數系統是由許多大型的計算機一起構建的,並且到今天仍然在使用。那麼,這些系統怎麼可靠地運作呢?要回答這個問題,我們先簡要回顧下這種解決方案的歷史及企業消息傳送是怎麼產生的。

    從上世紀60年代開始,許多大型的組織使用大型機來處理一些諸如數據處理,財務處理,靜態分析等關鍵功能。大型機爲商業提供包括高可用性,冗餘備份,可靠性,可擴展性,無中斷升級等許多關鍵特性。雖然這些系統很強大,但是對這些系統的訪問時被嚴格控制的,只有很少的輸入選擇。而且,系統間的內部聯繫還沒有發明,也就是說併發處理是不可能的。

    2.1展示了終端設備是如何連接上大型機的。在19世紀70年代,人們開始使用終端連接到大型機。這種方式使得大量的使用者可以同時訪問一個大型機。也就在這時,網絡產生了,大型機之間的交互成爲可能。到了80年代,不只是有了圖形界面的終端,PC機也被髮明瞭。互聯性也越來越重要,因爲本來需要連接到大型機上的應用已經被開發到可以在PC和工作站上運行。圖2.2展示了對大型機的各種類型的連接。這些擴展到連接帶來了額外的平臺和協議,同時也帶來了很多需要解決的問題。

連接兩個系統並不是一件簡單的事,因爲它們的數據格式,硬件,協議都需要不同的適配器。但適配器數量增長,版本也隨之增多,維護非常困難。所以需要將適配器的維護獨立於各系統。這也就是企業消息傳送的用途。

    企業消息傳送到目的就是在系統間傳遞數據。這些年來已經有各種不同的技術可以進行消息傳送,如下列表所示。

·         通過遠程過程調用(RPC)的解決方案,例如COMCORBA,DCEEJB

·         使用事件通知,內部交互,消息隊列的,例如FIFO緩衝,消息隊列,管道,信號,socket等。

·         使用異步可靠消息隊列的中間件的,例如IBM WebSphere MQ, SonicMQ, TIBCO Rendezvous,
and Apache ActiveMQ
,它們都可用在企業消息集成。

    最後一個要討論的是消息傳送中間件。那麼什麼是面向消息的中間件?

2.2什麼是面向消息中間件

    面向消息中間件(MOM)爲分佈式系統提供異步,解耦,穩定,可擴展和安全的行爲。MOM在分佈式計算領域是一個重要的概念。它允許應用使用代理器API在分佈式環境實現各種功能。

    總之,MOM的設計原理就是作爲消息發送者和接收者的中間人使用。這個中間人提供了一個高級別的解耦。圖2.3演示了ActiveMQ作爲中間人,不只是可以聯繫應用和大型機,還可以實現應用間的交互。
 SHAPE  \* MERGEFORMAT 
 
在一個較高級別看,消息就是一個商業信息單元,它通過MOM從一個應用發送到另一個應用。應用使用目標(destinations)來發送和接收消息。 消息將被投遞到destinations,然後發送給連接或訂閱該destinations的接收者。這個機制能夠解耦消息的發送者和接收者,因爲它們在 發送或接收消息的時候並不需要同時連接ActiveMQ。發送者不瞭解接收者,接收者也不瞭解發送者。這個機制就叫做異步消息傳送。

    MOMs添加了很多原來緊耦合系統不可能實現的特性,例如消息持久化,對於緩慢和不穩定連接的健壯性,複雜消息路由,消息轉移等。消息持久化能減輕緩慢或不穩定連接,或者使得接收者接收消息失敗時不會影響發送者的狀態。複雜的消息路由使很多東西都成爲可能,包括單一消息對應多個接收者,通過屬性或者內容選擇路由等。消息轉移允許擁有不同消息格式的兩個應用通過自定義的消息格式進行交流。

    目前市場上的MOMs提供一系列預製的連接協議。被支持的協議一般有HTTP/SmulticastSSLTCP/IPUDP等。一些提供商甚至提供多種編程語言支持,這大大降低了在不同環境下使用MOMs的難度。ActiveMQ提供上述所有的特性,而且更多。

    一般地,MOM會提供一些API來發送,接收消息及和MOM交互。多年來,MOM提供商爲它們選擇的語言提供專有的API。直到JMS規範到來才改變這種情況。

2.3什麼是Java消息服務

    JMS是在MOM供應商核心API基礎上發展的,它用來提供企業消息傳送。JMS的目標是爲Java提供一個標準的API來發送和接收消息,並使之成爲供應商天生行爲。JMS最小化了Java程序員開發企業消息應用的複雜性,同時還保留在不同JMS提供者之間移植的可能性。

    JMS並不是一個MOM。它是一個API,抽象了客戶端和MOM的交互,就像JDBC抽象與數據庫的交互一樣。圖2.4展示了客戶端是如何通過JMS提供的API和特定JMS提供者交互。特定的JMS提供者使用供應商制定的APIMOM交互。不只是圖示的四種,對於其它JMS提供者也是相同的。

   

爲了聯合企業消息傳送市場上的各廠商,Sun1998年頒佈了JMS規範的第一個版本。最後一個版本是2002年發佈的,對一些必要的東西進行了改進。JMS 1.1版本整合了兩種不同的消息傳送領域提供了不同的API,所以現在在不同領域的工作也都使用相同的API。這是API的一個巨大的改變。不過,舊的API仍然會被支持。

    爲了規範APIJMS爲消息傳送定義了很多概念:

·         JMS客戶端----100%Java編寫的發送和接收消息的應用。

·         Non-JMS客戶端----使用JMS提供者特定的客戶端API而不是JMS API來發送和接收消息的應用。

·         JMS producer----創建和發送JMS消息的客戶端應用。

·         JMS consumer----接收和處理JMS消息的客戶端應用。

·         JMS provider----100%使用Java編寫的JMS接口的實現。

·         JMS message----JMS最基礎的概念;被JMS客戶端發送和接收。

·         JMS domains----兩者類型的消息傳送,包括點對點(point-to-point)和發佈/訂閱(publish/subscribe)模式。

·         Administered objects----預配置的JMS對象,包含provider特定的配置信息。客戶端通過JNDI來訪問這些數據。

·         Connection factory----客戶端使用連接工廠來連接JMS provider

·         Destination----消息被投遞的地方,以及接收者消息接收的來源。

除此之外,還有其它一些同樣重要的概念。下一部分將深入這些概念並描述它們怎麼構建整個JMS

2.4 JMS規範

    就像前面提到的,JMS規範定義了兩種客戶端--JMS客戶端和非JMS客戶端。它們之間的區別必須簡單討論下。

 

2.4.1 JMS客戶端

    JMS客戶端使用JMS APIJMS提供者交互。就像使用JDBC API去訪問關係數據庫一樣,JMS客戶端使用JMS API作爲消息驅動服務的標準訪問方式。許多JMS提供者(包括ActiveMQ)包含了很多超出JMS規範要求的特性。值得一提的是一個100%JMS客戶端只能使用JMS提供的API,必須避免使用額外的特性。不過一般選擇哪個JMS provider通常是由它提供的額外特性決定的。所以,一個使用了額外特性的JMS客戶端,如果不重構,可能就不能用於其它JMS provider

   JMS clients使用MessageProducer(消息生產者)和MessageConsumer(消息消費者)接口。JMS provider必須提供這些接口的實現。JMS client如果發送消息,那麼它就是producer(生產者),如果接收消息,那麼它就是Consumer(消費者)。JMS client也有可能同時發送和接收消息。

 

    JMS PRODUCERS

    JMS clients使用JMS MessageProducer類來發送消息到一個destination。當調用Session.createProducer產生一個producer時,將會有默認的destination。不過這個可以通過重寫MessageProducer.send()方法來改變。MessageProducer接口如下所示。

Listing 2.1 The MessageProducer interface

public interface MessageProducer {
    void setDisableMessageID(boolean value) throws JMSException;


    boolean getDisableMessageID() throws JMSException; 


    void setDisableMessageTimestamp(boolean value) throws JMSException;


    boolean getDisableMessageTimestamp() throws JMSException;


    void setDeliveryMode(int deliveryMode) throws JMSException;


    int getDeliveryMode() throws JMSException;


    void setPriority(int defaultPriority) throws JMSException;


    int getPriority() throws JMSException;


    void setTimeToLive(long timeToLive) throws JMSException;


    long getTimeToLive() throws JMSException;

 

    Destination getDestination() throws JMSException;


    void close() throws JMSException;


    void send(Message message) throws JMSException;

 

    void send(Message message, int deliveryMode, int priority,
        long timeToLive)
        throws JMSException;


    void send(Destination destination, Message message)
        throws JMSException;

    void send(
        Destination destination,
        Message message,
        int deliveryMode,
        int priority,
        long timeToLive)
        throws JMSException;
    }

MessageProducer提供了發送消息的和設置消息頭部的方法。消息頭部設置包括JMSDeliveryMode(投遞類型),JMSPriority(優先級),JMSExpiration(有效期,通過get/setTimeLive()設置)以及一個同時設置前面三種消息頭的方法send()。這些消息頭部將在2.4.5講解。

 

JMS CONSUMERS

JMS客戶端使用JMS MessageConsumer類從一個destionation消費消息。它可以通過receive()方法同步地消費消息,也可以通過提供一個MessageListener實現來異步地消費消息。MessageListener.onMessage()方法會在消息到達destination時被調用。MessageConsumer接口如下所示。

Listing 2.2 The JMS MessageConsumer interface

public interface MessageConsumer {
    String getMessageSelector() throws JMSException;


    MessageListener getMessageListener() throws JMSException;


    void setMessageListener(MessageListener listener) throws JMSException;


    Message receive() throws JMSException;


    Message receive(long timeout) throws JMSException;


    Message receiveNoWait() throws JMSException;


    void close() throws JMSException;
}

接口中沒有方法爲MessageConsumer設置destination。事實上,destination會在使用Session.createConsumer()創建Consumer的時候被設置。

 

2.4.2 Non-JMS clients

    就像先前提到的Non-JMS客戶端使用JMS provider本地的客戶端API,而不是使用JMS API。這是個重要的區別,因爲本地的客戶端API可能提供一些不同的特性。這類本地non-JMS API能夠在Java RMI上面使用CORBA IIOP協議或其它一些本地協議。在JMS規範出來之前的一些消息provider通常有本地的客戶端API,之後,也有很多JMS provider提供本地的API

 

2.4.3 The JMS provider

    JMS provider是一個由供應商提供的JMS API實現。它們提供標準的JMS API來訪問MOM。(這和JDBC類似)

 

2.4.4 The JMS message

    JMS消息是JMS規範最重要的一個概念。JMS規範所有其它概念都是圍繞消息處理而建立的,因爲消息是商業數據和事件的載體。JMS消息能夠傳輸任何的數據,包括文本,二進制數和以及在消息頭的信息等。如何2.5JMS消息包含兩部分,包括消息頭和負載(payload)。消息頭爲客戶端和JMS provider提供消息的元數據。payload事實上就是消息體,可以通過各種消息類型,存放文本和二進制數據。

    JMS消息被設計爲容易理解和變通。所有複雜的東西都留在消息頭部。

2.4.5 JMS消息內部

    就像上面提到的,JMS消息複雜部分在它的頭部。有兩種頭部,它們都是基於相同的邏輯概念,但是有很大的不同。除了一系列標準的頭部和方法,還有properties方法。poperties是基於Java類型,用來處理自定義頭部。

JMS消息頭部

    如圖2.5所示,JMS消息支持一系列標準的消息頭部,並且爲之提供JMS API。很多頭部會自動被賦值。接下來將描述這些頭部,並看看它們 是怎麼被賦值。

 通過send()方法自動被賦值的頭部:

·         JMSDestination----消息被髮送到的目的地。這個對於從多個目標中接收消息的客戶端有用。

·         JMSDeliveryMode----JMS支持兩種消息發送模式:持久化和非持久化。默認是持久化的消息。不同的模式有不同的負載,也提供不同級別的可靠性。

          持久的(Persistent----JMS提供者持久化消息,所以當provider失敗時消息不會丟失。JMS提供者必須投遞每個持久化消息一次並且只有一次。也就是說,即使JMS提供者失敗,消息不會丟失也不會被投遞多於一次。  由於需要存儲消息及提高穩定性,所以持久化消息會要求更多的開銷。

          非持久的(Nonpersistent----JMS提供者不要持久化消息。JMS對於非持久化消息最多投遞一次。也就是說,如果消息投遞失敗,則會丟失。非持久化消息開銷較小,同時穩定性也較差。

          消息投遞模式是設置在生產者上面的,它對該生產者生產的所有消息有效。不過,消息投遞模式也可以在單獨的發生某個消息時被重寫。

·         JMSExpiration----JMS消息的過期時間。這個頭部會阻止發送一條已經過期的消息。有兩種方法設置過期時間。一種是通過MessageProducer.setTimeToLive()方法,此方法設置的消息存在時間對所有由該生產者生產的消息有效;一種是通過MessageProducer.send()方法,此方法設置的消息有效時間對本次發送的消息有效。

    JMSExpiration頭部是通過time-to-live時間加上當前時間計算出來的。默認地,time-to-live時間是零,這意味着消息永遠不會過期。如果time-to-live被明確賦值爲0,效果也是一樣的。

    這個頭部對於時間敏感的消息有用。不過要注意,雖然JMS提供者不會發送過期消息,JMS客戶端自己也必須保證不處理過期消息。    

·         JMSMessageID----JMS提供者產生的消息唯一標識,它必須以提供者的id開頭。消息id可以用在消息處理,也可以儲存下來用於追蹤歷史信息。由於生成id會給JMS提供者帶來一些開銷,所以生產者可以通過MessageProducer.setDisableMessageID()方法,告訴JMS提供者,應用不需要JMSMessageID。如果JMS提供者接受請求,則消息ID會被設置爲null。不過要注意,JMS也可能忽略該請求,仍然提供MessageID

·         JMSPriority----用於指明消息的重要級別。這個也是設置在消息生產者上面的,並且對該生產者生產的所有消息有效。也可以對單獨的消息重寫這個優先級。JMS定義了0-9十個基本的優先級。

         Priorities 0-4 ----屬於通常級別。

         Priorities 5-9 ----屬於加快級別。

         JMS提供者並沒有被要求實現消息順序,不過大多數都會提供該功能。它們可能簡單地先發送高優先級的消息再發

         送低優先級的消息。

·         JMSTimestamp----顯示消息是何時從生產者發到消息提供者的。這個值使用Java標準毫秒數。就像JMSMessageID一樣,生產者也可以建議JMS提供者不用提供這個值。設置的方法是MessageProducer.setDisableMessageTimestamp()。如果JMS提供者接受該建議,則這個值被設置爲0.

客戶端可選的頭部 

·         JMSCorrelationID----用來關聯當前消息和之前的消息。最常用在關聯一條響應消息和它的請求消息。這個值可以是:

          * 一個提供者指定的消息ID

          * 一個應用指定的字符串。

          * 一個提供者本地的字節數組

          由提供者指定的消息ID都會帶有ID前綴,所以,應用指定的字符串不應帶有ID前綴。如果一個JMS提供者支持本地

          correlation ID,那麼JMS客戶端可能需要對correlation ID賦值以匹配非JMS客戶端,不過這個並非強制要

          求。

·         JMSReplyTo----用來指定一個響應發生的目標。這個值一般用在請求/響應的消息傳送風格中。使用該頭部的消息表明它期望得到響應,不過這並非強制要求。由客戶端決定是否響應。

·         JMSType----語義上的消息類型。只有很少供應商使用該頭部,而且它和消息的負載類型(持久化/非持久化)無關。

JMS提供者可選頭部

·         JMSRedelivered----用來指出一條消息被投遞但沒有收到應答的情況。這種情況可能是消費者應答失敗,或者JMS提供者沒有被通知到(例如異常發生使得應答消息沒有到達JMS提供者)。

JMS消息屬性

    屬性是消息的一些簡單的額外的頭部。JMS提供通用的方法來設置自定義頭部。這些通用方法提供對各種Java原始類型的支持,包括BooleanbyteshortintlongfloatdoubleString對象。詳情請看下面Message接口方法清單:

Listing 2.3 The JMS Message interface

public interface Message {
...
boolean getBooleanProperty(String name) throws JMSException;


byte getByteProperty(String name) throws JMSException;


short getShortProperty(String name) throws JMSException;


int getIntProperty(String name) throws JMSException;


long getLongProperty(String name) throws JMSException;


float getFloatProperty(String name) throws JMSException;


double getDoubleProperty(String name) throws JMSException;


String getStringProperty(String name) throws JMSException;


Object getObjectProperty(String name) throws JMSException;
...
Enumeration getPropertyNames() throws JMSException;


boolean propertyExists(String name) throws JMSException;
...


void setBooleanProperty(String name, boolean value) throws JMSException;


void setByteProperty(String name, byte value) throws JMSException;


void setShortProperty(String name, short value) throws JMSException;


void setIntProperty(String name, int value) throws JMSException;


void setLongProperty(String name, long value) throws JMSException;


void setFloatProperty(String name, float value) throws JMSException;


void setDoubleProperty(String name, double value) throws JMSException;


void setStringProperty(String name, String value) throws JMSException;


void setObjectProperty(String name, Object value) throws JMSException;


.. }

    有兩個方法對所有屬性有用,它們是getPropertyNames()和propertyExists()方法。getPropertyName()方法返回一個所有屬性的Enumeration,這使得客戶端可以很容易地遍歷所有屬性。propertyExists()方法是用來測試一個屬性是否存在該消息中。注意這兩個方法是對屬性有用,那些JMS規範指定的頭部(例如MessageID等)是不能用這兩個方法來遍歷或測試的。

    總之,現在有三種類型的屬性,自定義屬性,JMS定義屬性,提供者指定屬性。

自定義屬性

   自定義屬性是任意的,是由JMS應用定義的。應用開發者,可以通過下面的一些通用方法(getBooleanProperty()/
setBooleanProperty(), getStringProperty()/setStringProperty()
等)來定義各種使用java類型的屬性。

JMS定義屬性

    JMS規範保留了“JMSX”作爲屬性名前綴。下面是一些定義了的屬性,這些屬性都是可選的。

·         JMSXAppID----發生消息應用的ID

·         JMSXConsumerTXID----消費這條消息的事務ID

·         JMSXDeliveryCount----消息參與投遞的次數

·         JMSXGroupID----該消息所屬的消息組

·         JMSXGroupSeq----該消息在消息組中所處的序列

·         JMSXProducerTXID----生產這條消息的事務ID

·         JMSXRcvTimestamp----JMS提供者將消息投遞給消費者的時間

·         JMSXState----用來定義提供者指定的狀態

·         JMSXUserID----發送這條消息的用戶

    JMS規範只對JMSXGroupIDJMSXGroupSeq這兩個屬性的用法提供了建議。這兩個屬性可以用在消息分組/帶順序的消息分組。

提供者指定屬性

    JMS預留了JMS_<vendor-name>屬性前綴作爲提供者指定屬性。提供者用這個前綴定義自己的屬性。這些屬性一般用在提供者指定的非JMS客戶端,並且不能用在JMS-to-JMS消息傳送中。

    現在JMS的頭部和屬性已經討論完了。頭部和屬性對於預訂了消息的客戶端很重要,它可以用來幫助過濾消息。

2.4.6消息選擇器

    很多時候,一個JMS客戶端訂閱了一個目標,但是它只想接收特定類型的消息。這種情況,消息頭部和屬性正好派上用場。例如,一個消費者到一個隊列註冊,希望接收到特定股票的消息。只要該消息包含一個股票標識的屬性,那麼這個功能就很容易實現。JMS客戶端可以用消息選擇器告訴JMS提供者它要接收某個屬性值符合要求的消息。

    許多選擇器允許JMS客戶端基於消息頭部的值指定它要接收到消息。選擇器使用SQL92子集的條件表達式。消息選擇器使用消息頭部和屬性值來進行布爾計算。不符合的消息將不會投遞到客戶端。消息選擇器不能作用於消息體。

    條件表達式作爲字符串參數傳遞給javax.jms.Session創建選擇器的方法。這些條件表達式使用包括標識符,字面量和操作符等從SQL92語法繼承的符號。這些東西都在下面的表2.1中定義。

上面的這些東西都是用來對消息頭部和屬性創建查詢條件的。看下面列表定義的消息。這條消息定義了兩個屬性,它們將被用來過濾消息。

Listing 2.4 A JMS message with custom properties

public void sendStockMessage(Session session,
MessageProducer producer,Destination destination,String payload,
String symbol,double price)throws JMSException

{
    TextMessage textMessage = session.createTextMessage();
    textMessage.setText(payload);
    textMessage.setStringProperty("SYMBOL", symbol);
    textMessage.setDoubleProperty("PRICE", price);
    producer.send(destination, textMessage);
}

現在讓我們來看一些使用消息選擇器的例子。

Listing 2.5 Filter messages using the SYMBOL header

...
String selector = "SYMBOL = 'AAPL'";
MessageConsumer consumer =
session.createConsumer(destination, selector);
...

列表2.5的程序定義了一個匹配蘋果公司消息的選擇器。這個消費者只會接收到匹配該選擇器的消息。

 

Listing 2.6 Filter messages using both the SYMBOL and PRICE headers

...
String selector = "SYMBOL = 'AAPL' AND PRICE > "
+ getPreviousPrice();
MessageConsumer consumer =
session.createConsumer(destination, selector);
...

上面的選擇器匹配蘋果公司並且價格高於之前價格的股票消息。這個選擇器將會顯示那些價格在上漲的股票消息。但是,如果你對股票消息的時效性有要求,那麼可以看看下面的例子。

 

Listing 2.7 Filter messages using headers

...
String selector = "SYMBOL IN ('AAPL', 'CSCO') AND PRICE > "
+ getPreviousPrice() + " AND PE_RATIO < "
+ getCurrentAcceptedPriceToEarningsRatioThreshold();
MessageConsumer consumer =
session.createConsumer(destination, selector);
...

最後的例子2.7定義了一個更復雜的選擇器。這個選擇器可以用來匹配蘋果及思科公司那些正在增長的,並且市盈率小於當前可接受值的消息。

    上面的例子對於你開始使用消息選擇器來說已經足夠了。但是如果你想要更多的信息,可以參考JMS消息的Javadoc

 

消息體

消息體,也就是負載,JMS爲它定義了六種Java類型。使用這些Java對象,信息就可以通過消息負載發送出去。

·         Message----基本的消息類型。用來發送沒有負載,只有頭部和屬性的消息。最常用在簡單的事件通知。

·         TextMessage----負載是String類型的消息。用在發送簡單文本或XML數據。

·         MapMessage----使用一系列name/value(名稱/值)做爲它的負載。名稱使用字符串,值是Java原始類型。

·         BytesMessage----包含一個不能被中斷字節數組作爲負載。

·         StreamMessage----包含一些Java原始類型的流,會按順序被填充和讀取。

·         ObjectMessage----用來包含一個序列號Java對象。一般用來存儲複雜Java對象,也支持Java集合。

2.4.7 JMS領域

    就像之前提到的,JMS是團隊成果,這個團隊就包括了消息傳送實現的提供商。JMS定義了兩種類型的消息傳送,這是由現有的消息傳送實現決定的。這兩種風格(也叫做領域domains)是point-to-pointpublish/subscribe。大多數的MOMs已經支持兩種類型的消息傳送風格,所以JMS API也必須同時支持它們。讓我們詳細看下這兩種類型的消息傳送。

點對點領域

    點對點(PTP)消息傳送使用的目標是隊列。通過使用隊列,消息可以被異步或同步地發送和接收。每一條到達隊列的消息將會被投遞到單獨一個消費者一次,並且只有一次。這就好像兩個人之間的郵件發送。消費者可以通過MessageConsumer.receive()方法同步地接收消息或使用MessageConsumer.setMessageListener()方法註冊一個MessageListener實現來異步地接收消息。隊列保存所有的消息直到它們被投遞出去或過期。

    如圖2.6所示,多個消費者可以註冊在一個隊列上,但一條消息只有一個消費者會接收到。然後消費者要決定是否應答這條消息。注意,圖2.6所示的消息是從一個生產者出來的並且只投遞給一個消費者,而不是所有消費者。就像前面提到的,JMS提供者保證消息一次並且只有一次投遞給下一個準備好的消費者。JMS提供者是對所有已註冊的消費者循環發送消息的。

發佈訂閱領域

    發佈訂閱模式的消息使用主題(topics)作爲目標。發佈者發送消息到主題,訂閱者從主題接收消息。發送到主題的消息會自動發給所有的訂閱者。這個消息傳送領域就像預製一個郵件列表,所有郵件列表上的用戶都會受到消息。圖2.7描述了這種情況。

就像PTP消息傳送一樣,訂閱者可以通過MessageConsumer.receive()方法同步地接收消息或使用MessageConsumer.setMessageListener()方法註冊一個MessageListener實現來異步地接收消息。主題並不保存消息,除非顯式地讓它這樣做。這個可以通過使用持久訂閱(durable subscription)來實現。使用持久訂閱,如果一個訂閱者與JMS提供者連接斷開,JMS提供者有責任爲該訂閱者保存消息。重新連上後,訂閱者將收到所有的未過期的消息。持久訂閱允許訂閱者斷開連接而不會丟失任何消息。

 

持久訂閱(durability)和消息持久化(persistence)的區別

    持久訂閱和消息持久化是JMS經常會混淆的兩個概念。雖然它們很類似,但是還是有一些明顯的不同,並且它們的用途也不一樣。消息持久訂閱只在發佈/訂閱領域有效。當客戶端連接到一個主題上,它們可以選擇使用持久或非持久訂閱。考慮這兩種情況的區別。

·         持久訂閱----一個持久訂閱的時間是無限的。客戶端註冊到主題上,並且告訴JMS提供者當訂閱者斷開連接時保持訂閱狀態。如果一個訂閱者的連接斷開了,JMS提供者將保持所有的消息直到訂閱者重新連上或者訂閱者取消訂閱。

·         非持久訂閱----一個非持久訂閱是有限的。客戶端註冊到主題上並且告訴JMS提供者當連接斷開是不用保持訂閱狀態。如果一個訂閱者斷開連接,JMS提供者在斷開的這段時間裏不會保存任何消息。

    消息持久化是獨立於消息領域的。消息持久化是一種服務質量屬性,它用來指出JMS應用處理消息投遞失敗時的能力。就像之前提到的,這個值是通過消息生產者setDeliveryMode方法來設置的,這個方法的輸入參數是JMSDeliveryMode類的變量PERSISTENTNON-PERSISTENT

 

JMS應用的請求/回覆傳送機制

    雖然JMS規範沒有把請求/回覆(request/reply)消息傳送作爲一種正式的消息領域來定義。當它提供了一些消息頭部和許多有用的類來處理請求/回覆消息傳送。請求/回覆消息傳送是一種異步的會話模式,可以在PTPpub/sub領域使用。這種模式會用到JMSReplyToJMSCorrelationID消息頭部及臨時的消息目標。JMSReplyTo指定一個回覆消息投遞的目標,JMSCorrelationID指定回覆消息對應的請求的JMSMessageID。這些頭部用來關聯回覆消息和它(們)的請求消息。臨時的目標只能在連接持續時間裏有效並且只能被創建它的連接使用。這些限制條件使得臨時目標對於請求/回覆模式很有用。

    QueueRequestorTopicRequestor是兩個處理請求/回覆模式的有用的類。這些類提供一個request()方法發送一條請求消息並且通過臨時目標等待回覆。一般地,是一個請求預期會得到一個回覆。圖2.8顯示了一個請求,一個回覆的流程。

2.8通過兩個終端描繪了基本的請求/回覆消息傳送類型。這個過程是使用了JMSReplyTo消息頭部和一個臨時目標。接收者通過臨時目標發送回覆消息,請求者則通過它接收消息。QueueRequestorTopicRequestor這兩個類可以用來處理基本的請求/回覆模式,但不能用來處理複雜的情況,比如一個請求對應多個接收者的多個回覆。這種需求要求你自己開發自定義的JMS客戶端。

 

2.4.8 管理對象(Administered objects

    管理對象包含JMS提供者特定的配置信息,它由JMS管理者創建。因此,管理對象是被JMS客戶端使用的。它們用來隱藏提供者特定的細節並且抽象JMS提供者的管理任務。管理對象可以通過JNDI訪問,但不是必須。最常見的情況是JMS提供者寄居在Java EE容器裏。JMS規範提供兩種類型的管理對象:連接工廠(ConnectionFactory)和目標(Destination)。

連接工廠

    JMS客戶端使用連接工廠來創建到JMS提供者的連接。連接一般就是一個客戶端與JMS提供者之間的TCP連接,所以連接的負載是很大的。使用一個連接池是比較合適的。一個到JMS提供者的連接就像一個到關係數據庫的JDBC連接(JDBC連接是客戶端用來和數據庫交互的)。客戶端使用JMS連接來創建java.jms.Session對象,該對象代表與JMS提供者的一個交互。

目標

    目標封裝了提供者特定的地址,這地址是用來發送和消費信息的。雖然目標是使用session對象創建的,它們的生存時間是和創建session的連接一致。

    臨時目標對於一個連接是唯一的。它們的生命週期和創建它們的連接一致,並且只有創建它們的連接才能爲該目標創建消費者。就像前面提到的,臨時目標是用在請求/回覆消息傳送中的。

2.5使用JMS API創建JMS應用

    因不同的商業要求,創建一個JMS應用可以很簡單也可以很複雜。就像JDBCJNDIEJBsAPI,抽象JMS API,使得JMS代碼和商業邏輯相分離是必須的。這個概念不會在這裏討論,因爲這要涉及到模式和應用架構,不是一兩句話可以說完的。下面是一些簡單的例子,它們向你展示了一個最基本的JMS APIs的使用,

 

2.5.1 一個簡單的JMS應用

   一個JMS應用使用Java語言編寫的,它組合了各個部分來和JMS一起工作。這些部分在2.3節已經討論過。一個簡單的JMS應用會下面的步驟:

1.    請求一個JMS連接工i廠。

2.    是用連接工廠創建連接。

3.    啓動JMS連接。

4.    通過連接創建session

5.    獲取一個目標。

6.    創建一個生產者,或a.創建一個生產者,b.創建一條JMS消息併發送到目標

7.    創建一個消費者,或a.創建一個消費者,b.註冊一個消息監聽器。

8.    發送或接受消息。

9.    關閉所有資源(連接,會話,生產者,消費者等)。

    這些步驟是用來展示使用JMS的一個簡單流程。下面的列表用來展現創建一個生產者併發送消息的代碼。

Listing 2.8 Sending a JMS message

public class MyMessageProducer {
...
    ConnectionFactory connectionFactory;
    Connection connection; 
    Session session;
    Destination destination;
    MessageProducer producer;
    Message message;
    boolean useTransaction = false;
    try {
        Context ctx = new InitialContext();
        connectionFactory =
            (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
        connection = connectionFactory.createConnection();
        connection.start();
        session = connection.createSession(useTransaction,
            Session.AUTO_ACKNOWLEDGE);
        destination = session.createQueue("TEST.QUEUE");
        producer = session.createProducer(destination);
        message = session.createTextMessage("this is a test");
        producer.send(message);
    } catch (JMSException jmsEx) {

        ...
    } finally {
         producer.close();
         session.close();
         connection.close();
    }
}

    列表2.8,首先創建了一個上下文。通常情況下,上下文是通過JNDI路徑獲取的,這個例子只是用來演示的。在初始化的上下文中通過使用連接工廠的唯一名字獲取它。通過連接工廠,JMS連接被創建和啓動。這之後,JMS客戶端可以開始和代理器交互了。通過JMS連接,JMS會話被創建並且使用自動答覆消息類型。JMS隊列通過會話被創建。接下來是使用會話和目標創建生產者。之後通過會話創建了一條簡單的文本消息並由生產者發送出去。最後的一個步驟是關閉所有用到的對象。

    2.8的例子演示了一個最簡單的創建生產者和發送一條消息到目標的動作。注意,有沒有一個消費者在等待這樣一條消息對於生產者是不重要的。MOMs作用就是生產者和消費者的一箇中間調節,這對於創建JMS應用非常有幫助。開發者並不需要考慮如何獲取這樣的中間調節功能,JMS APIs已經提供了。下面的例子則是演示一個創建消費者和接收消息的動作。

Listing 2.9 Receiving a JMS message synchronously

public class MySyncMessageConsumer {
...
    ConnectionFactory connectionFactory;
    Connection connection;
    Session session;
    Destination destination;
    MessageConsumer consumer;
    Message message;
    boolean useTransaction = false;
    try {
        Context ctx = new InitialContext();
        connectionFactory =
            (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
        connection = connectionFactory.createConnection();
        connection.start();
        session = connection.createSession(useTransaction,
            Session.AUTO_ACKNOWLEDGE);
        destination = session.createQueue("TEST.QUEUE");
        consumer = session.createConsumer(destination);
        message = (TextMessage) consumer.receive(1000);
        System.out.println("Received message: " + message);

    } catch (JMSException jmsEx) {
        ...
    } finally {
        producer.close();
        session.close();
        connection.close();
    }
}

2.9的例子和2.8很像,因爲它們都需要相同的步驟直到消費者被創建。之後,消費者被用來從目標接受消息。最後一部分代碼是關閉所有的對象。同樣地,這並不要求現在有一個生產者在發送消息。所有的中間和臨時存儲都是JMS提供者做的。2.9演示的是同步的消息接收。這意味着JMS消費者發送一個請求到JMS提供者,並等待響應。消費者必須通過循環一次次地獲取消息。JMS消費者並非只能通過同步方法獲取消息。

    JMS API同樣提供了異步獲取消息的方法。JMS提供者會將消息推送到消費者。下面是一個異步消息消費的例子。

Listing 2.10 Receiving a JMS message asynchronously

public class MyAsyncMessageConsumer implements MessageListener {
...
    ConnectionFactory connectionFactory;
    Connection connection;
    Session session;
    Destination destination;
    MessageProducer producer;
    Message message;
    boolean useTransaction = false;
    try {
        Context ctx = new InitialContext();
        connectionFactory =
            (ConnectionFactory) ctx.lookup("ConnectionFactoryName");
        connection = connectionFactory.createConnection();
        connection.start();
        session = connection.createSession(useTransaction,
        Session.AUTO_ACKNOWLEDGE);
        destination = session.createQueue("TEST.QUEUE");
        consumer = session.createConsumer(destination);
        consumer.setMessageListener(this);
    } catch (JMSException jmsEx) {
         ...
    } finally {
        producer.close();

        session.close();
        connection.close();
   }
    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            System.out.println("Received message: " + message);
        }
    }
}

2.10的不同之處在於它實現了MessageListener接口的onMessage方法並且將實現類註冊到JMS提供者。異步消息接收是很有用的。這意味着消費者不再需要人工地不停地從提供者那裏拉消息。而是,通過註冊到提供者的MessageListener實現作爲回調,onMessage方法將在消息被投遞的時候自動調用。

 

JMS多線程應用

JMS定義了很多併發的對象,但是隻有一部分支持併發訪問。ConnectionFactoryConnectionDestination對象支持併發訪問;SessionMessageProducerMessageConsumer對象不支持併發訪問。也就是說,SessionMessageProducerMessageConsumer對象不應該在多線程中共享

 

對於JMS API消息消費還有一點要說明。它提供異步的消息消費,這和EJB的消息驅動beanmessage-driven beans)一樣。

2.5.2 消息驅動beans

消息驅動beansMessage-driven beans)在EJB2.0規範的時候誕生。該組件產生的動機是讓EJB能夠與JMS簡單集成,使得EJB異步消息消費和使用標準JMS APIs一樣簡單。通過使用MessageListener接口,EJB通過消息推送自動從JMS提供者那裏接收消息。一個MDB例子如下。

Listing 2.11 A simple message-driven bean example

import javax.ejb.EJBException;
import javax.ejb.MessageDrivenBean;
import javax.ejb.MessageDrivenContext;
import javax.jms.Message;
import javax.jms.MessageListener;
public class MyMessageProcessor
    implements MessageDrivenBean, MessageListener {

    public void onMessage(Message message) {
        TextMessage textMessage = null;
        try {
            if (message instanceof TextMessage) {
                textMessage = (TextMessage) message;
                System.out.println("Received message: " + msg.getText());
                processMessage(textMessage);
            } else {
                System.out.println("Incorrect message type: " +
                    message.getClass().getName());
            }
        } catch (JMSException jmsEx) {
             jmsEx.printStackTrace();
        }
    }
    public void ejbRemove() throws EJBException {
        // This method is called by the EJB container
    }
    public void setMessageDrivenContext(MessageDrivenContext ctx)
         throws EJBException {
         // This method is called by the EJB container
    }
    private void processMessage(TextMessage textMessage) {
        // Do some important processing of the message here
    }
}

注意到例子2.11MyMessageProcessor類實現了MessageDrivenBeanMessageListener接口。MessageDrivenBean接口需要setMessageDrivenContext()ejbRemove的實現方法。這些方法會在創建和銷燬MDB時被EJB容器調用。MessageListener接口只包含單獨的一個方法onMessage()onMessage()方法會在消息到達時被JMS提供者調用。

    MDB除了允許EJB容器管理所需的所有資源,包括Java EE資源(如JDBCJMSJCA連接),安全,事務甚至是JMS消息應答,更重要的一個好處是可以併發地處理消息。普通的JMS客戶端需要自己管理資源和環境,並且它們通常是線性處理消息--一次一條消息(除非特別地爲多線程設計)。而MDB能夠一次處理多條消息,因爲EJB容器可以通過EJB部署描述符創建很多MDBs實例。這樣的配置是在Java EE容器規範定義的。如果你使用Java EE容器,可以查看文檔看是如何通過部署描述符進行配置的。

    MDBs的一個缺點是它們要求完全Java EE容器支持。今天所有EJB容器中,只有那些完全支持Java EE的才能支持MDBsMDBs在一個完全的Java EE容器中非常有用,但是也有一些替代品不要求完全的Java EE容器。使用Spring框架的JMS APIs使得開發消息驅動POJOsmessage-driven POJOs)非常簡單。這些Plain Old Java ObjectsPOJOs)可以作爲消息驅動組件使用。這種開發風格在Java開發領域逐漸流行起來,因爲它避免了使用完整Java EE容器的負載。通過Spring框架的開發將在第七章討論。

不是所有的EJB容器都要求一個完全的Java EE容器----嘗試使用OpenEJB

在寫這本書的時候,幾乎市場上所有的EJB容器都需要一個完全的Java EE容器來支持MDBs。只有Apache OpenEJB是例外。OpenEJBEJB1.1EJB2EJB3都支持MDBs,它可以通過嵌入或者獨立運行方式來支持MDBsOpenEJB可以嵌入在Apache GeronimoApache Tomcat或者你自己的Java應用。

 

2.6總結

企業消息傳送在商業上的影響是明顯的。企業消息傳送及相關的概念影響了很多而外的技術和觀念。沒有企業消息傳送,開發者就不能在同步調用和解偶的應用間自由選擇。SOACEP和其它高層次基於企業消息傳送的觀念也不會產生。更重要的是JMS規範也不會產生。

    JMS規範對於Java世界的影響巨大,它使得消息傳送變成Java的一部分,並且可以讓所有的Java開發者使用。這對於允許Java連接商業上重要的應用非常必須,因爲它爲消息傳送到使用提供一個標準的行爲。這章的例子都非常簡單和短,它們的目的是是你瞭解JMS。本書接下來的部分有更豐富的例子討論,並且可以從網上下載。

    現在我們對JMS及它提供什麼有了一個基本的瞭解,下面將回顧這些例子。第三章提供的一個樣例將會在全書使用

發佈了41 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章