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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章