为了帮助你理解ActiveMQ的意义,了解企业消息传送背景和历史是很重要的。讨论完企业消息传送,你将可以通过一个小例子了解JMS及其使用。这章的目的是简要回顾企业消息传送及JMS规范。如果你已经熟悉这些主题,你可以跳过直接到下一章去。
软件开发者经常需要在两个系统之间交流或搬运数据。这种问题有很多解决办法。但限于你的条件和需求,选择一种解决方案是一个大决定。商业需求往往有严格的限制条件,直接影响你的决定的有性能,扩展性,稳定性等。我们日常使用的很多系统都有这样的要求,比如ATM系统,航班预订系统,信用卡系统,单点销售系统,通信系统等。如果我们的日常生活没有这些系统会怎样?
用一小会的时间想一下这些服务给我们的生活带来了怎样的便利。这些及其它类似应用能够方便我们的生活在于他们的稳定和安全性。在这些应用场景背后,是很多应用组合起来的,通常是分布式的,它们之间相互传递事件或消息。即使是最复杂的金融交易系统也是用这种方式集成的,完全通过消息传送来发送和接收商业信息。
由于各种各样的理由,很多产品都提供消息传送。必要性是发明之母,这就是消息中间件产生的原因。当时需要一种软件,能够在各种不同的数据类型,操作系统,协议甚至是不同的编程语言中交流数据。并且,复杂的消息路由和转移也成为这种解决方案的一部分必备能力。
ActiveMQ就是一个MOM产品,它为上面提到的商业系统提供异步消息传送。ActiveMQ通过实现JMS规范提供了这样一种可靠性和扩展性。
2.1介绍企业消息传送
向上面提及的大多数系统是由许多大型的计算机一起构建的,并且到今天仍然在使用。那么,这些系统怎么可靠地运作呢?要回答这个问题,我们先简要回顾下这种解决方案的历史及企业消息传送是怎么产生的。
从上世纪60年代开始,许多大型的组织使用大型机来处理一些诸如数据处理,财务处理,静态分析等关键功能。大型机为商业提供包括高可用性,冗余备份,可靠性,可扩展性,无中断升级等许多关键特性。虽然这些系统很强大,但是对这些系统的访问时被严格控制的,只有很少的输入选择。而且,系统间的内部联系还没有发明,也就是说并发处理是不可能的。
图2.1展示了终端设备是如何连接上大型机的。在19世纪70年代,人们开始使用终端连接到大型机。这种方式使得大量的使用者可以同时访问一个大型机。也就在这时,网络产生了,大型机之间的交互成为可能。到了80年代,不只是有了图形界面的终端,PC机也被发明了。互联性也越来越重要,因为本来需要连接到大型机上的应用已经被开发到可以在PC和工作站上运行。图2.2展示了对大型机的各种类型的连接。这些扩展到连接带来了额外的平台和协议,同时也带来了很多需要解决的问题。
连接两个系统并不是一件简单的事,因为它们的数据格式,硬件,协议都需要不同的适配器。但适配器数量增长,版本也随之增多,维护非常困难。所以需要将适配器的维护独立于各系统。这也就是企业消息传送的用途。
企业消息传送到目的就是在系统间传递数据。这些年来已经有各种不同的技术可以进行消息传送,如下列表所示。
- 通过远程过程调用(RPC)的解决方案,例如COM,CORBA,DCE及EJB
- 使用事件通知,内部交互,消息队列的,例如FIFO缓冲,消息队列,管道,信号,socket等。
- 使用异步可靠消息队列的中间件的,例如IBM WebSphere MQ, SonicMQ, TIBCO Rendezvous, and Apache ActiveMQ,它们都可用在企业消息集成。
最后一个要讨论的是消息传送中间件。那么什么是面向消息的中间件?
2.2什么是面向消息中间件
面向消息中间件(MOM)为分布式系统提供异步,解耦,稳定,可扩展和安全的行为。MOM在分布式计算领域是一个重要的概念。它允许应用使用代理器API在分布式环境实现各种功能。
总之,MOM的设计原理就是作为消息发送者和接收者的中间人使用。这个中间人提供了一个高级别的解耦。图2.3演示了ActiveMQ作为中间人,不只是可以联系应用和大型机,还可以实现应用间的交互。 在一个较高级别看,消息就是一个商业信息单元,它通过MOM从一个应用发送到另一个应用。应用使用目标(destinations)来发送和接收消息。消息将被投递到destinations,然后发送给连接或订阅该destinations的接收者。这个机制能够解耦消息的发送者和接收者,因为它们在发送或接收消息的时候并不需要同时连接ActiveMQ。发送者不了解接收者,接收者也不了解发送者。这个机制就叫做异步消息传送。
MOMs添加了很多原来紧耦合系统不可能实现的特性,例如消息持久化,对于缓慢和不稳定连接的健壮性,复杂消息路由,消息转移等。消息持久化能减轻缓慢或不稳定连接,或者使得接收者接收消息失败时不会影响发送者的状态。复杂的消息路由使很多东西都成为可能,包括单一消息对应多个接收者,通过属性或者内容选择路由等。消息转移允许拥有不同消息格式的两个应用通过自定义的消息格式进行交流。
目前市场上的MOMs提供一系列预制的连接协议。被支持的协议一般有HTTP/S,multicast,SSL。TCP/IP,UDP等。一些提供商甚至提供多种编程语言支持,这大大降低了在不同环境下使用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提供者使用供应商制定的API和MOM交互。不只是图示的四种,对于其它JMS提供者也是相同的。
为了联合企业消息传送市场上的各厂商,Sun在1998年颁布了JMS规范的第一个版本。最后一个版本是2002年发布的,对一些必要的东西进行了改进。JMS 1.1版本整合了两种不同的消息传送领域提供了不同的API,所以现在在不同领域的工作也都使用相同的API。这是API的一个巨大的改变。不过,旧的API仍然会被支持。
为了规范API,JMS为消息传送定义了很多概念:
- 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 API与JMS提供者交互。就像使用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.5,JMS消息包含两部分,包括消息头和负载(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原始类型的支持,包括Boolean,byte,short,int,long,float,double及String对象。详情请看下面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规范只对JMSXGroupID和JMSXGroupSeq这两个属性的用法提供了建议。这两个属性可以用在消息分组/带顺序的消息分组。
提供者指定属性
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-point及publish/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类的变量PERSISTENT或NON-PERSISTENT。
JMS应用的请求/回复传送机制
虽然JMS规范没有把请求/回复(request/reply)消息传送作为一种正式的消息领域来定义。当它提供了一些消息头部和许多有用的类来处理请求/回复消息传送。请求/回复消息传送是一种异步的会话模式,可以在PTP或pub/sub领域使用。这种模式会用到JMSReplyTo和JMSCorrelationID消息头部及临时的消息目标。JMSReplyTo指定一个回复消息投递的目标,JMSCorrelationID指定回复消息对应的请求的JMSMessageID。这些头部用来关联回复消息和它(们)的请求消息。临时的目标只能在连接持续时间里有效并且只能被创建它的连接使用。这些限制条件使得临时目标对于请求/回复模式很有用。
QueueRequestor和TopicRequestor是两个处理请求/回复模式的有用的类。这些类提供一个request()方法发送一条请求消息并且通过临时目标等待回复。一般地,是一个请求预期会得到一个回复。图2.8显示了一个请求,一个回复的流程。
图2.8通过两个终端描绘了基本的请求/回复消息传送类型。这个过程是使用了JMSReplyTo消息头部和一个临时目标。接收者通过临时目标发送回复消息,请求者则通过它接收消息。QueueRequestor和TopicRequestor这两个类可以用来处理基本的请求/回复模式,但不能用来处理复杂的情况,比如一个请求对应多个接收者的多个回复。这种需求要求你自己开发自定义的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应用可以很简单也可以很复杂。就像JDBC,JNDI,EJBs等API,抽象JMS API,使得JMS代码和商业逻辑相分离是必须的。这个概念不会在这里讨论,因为这要涉及到模式和应用架构,不是一两句话可以说完的。下面是一些简单的例子,它们向你展示了一个最基本的JMS APIs的使用,
2.5.1 一个简单的JMS应用
一个JMS应用使用Java语言编写的,它组合了各个部分来和JMS一起工作。这些部分在2.3节已经讨论过。一个简单的JMS应用会下面的步骤:
- 请求一个JMS连接工i厂。
- 是用连接工厂创建连接。
- 启动JMS连接。
- 通过连接创建session。
- 获取一个目标。
- 创建一个生产者,或a.创建一个生产者,b.创建一条JMS消息并发送到目标
- 创建一个消费者,或a.创建一个消费者,b.注册一个消息监听器。
- 发送或接受消息。
- 关闭所有资源(连接,会话,生产者,消费者等)。
这些步骤是用来展示使用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定义了很多并发的对象,但是只有一部分支持并发访问。ConnectionFactory,Connection,Destination对象支持并发访问;Session,MessageProducer,MessageConsumer对象不支持并发访问。也就是说,Session,MessageProducer,MessageConsumer对象不应该在多线程中共享
对于JMS API消息消费还有一点要说明。它提供异步的消息消费,这和EJB的消息驱动bean(message-driven beans)一样。
2.5.2 消息驱动beans
消息驱动beans(Message-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.11的MyMessageProcessor类实现了MessageDrivenBean和MessageListener接口。MessageDrivenBean接口需要setMessageDrivenContext()和ejbRemove的实现方法。这些方法会在创建和销毁MDB时被EJB容器调用。MessageListener接口只包含单独的一个方法onMessage()。onMessage()方法会在消息到达时被JMS提供者调用。
MDB除了允许EJB容器管理所需的所有资源,包括Java EE资源(如JDBC,JMS,JCA连接),安全,事务甚至是JMS消息应答,更重要的一个好处是可以并发地处理消息。普通的JMS客户端需要自己管理资源和环境,并且它们通常是线性处理消息--一次一条消息(除非特别地为多线程设计)。而MDB能够一次处理多条消息,因为EJB容器可以通过EJB部署描述符创建很多MDBs实例。这样的配置是在Java EE容器规范定义的。如果你使用Java EE容器,可以查看文档看是如何通过部署描述符进行配置的。
MDBs的一个缺点是它们要求完全Java EE容器支持。今天所有EJB容器中,只有那些完全支持Java EE的才能支持MDBs。MDBs在一个完全的Java EE容器中非常有用,但是也有一些替代品不要求完全的Java EE容器。使用Spring框架的JMS APIs使得开发消息驱动POJOs(message-driven POJOs)非常简单。这些Plain Old Java Objects(POJOs)可以作为消息驱动组件使用。这种开发风格在Java开发领域逐渐流行起来,因为它避免了使用完整Java EE容器的负载。通过Spring框架的开发将在第七章讨论。
不是所有的EJB容器都要求一个完全的Java EE容器----尝试使用OpenEJB
在写这本书的时候,几乎市场上所有的EJB容器都需要一个完全的Java EE容器来支持MDBs。只有Apache OpenEJB是例外。OpenEJB从EJB1.1,EJB2到EJB3都支持MDBs,它可以通过嵌入或者独立运行方式来支持MDBs。OpenEJB可以嵌入在Apache Geronimo,Apache Tomcat或者你自己的Java应用。
2.6总结
企业消息传送在商业上的影响是明显的。企业消息传送及相关的概念影响了很多而外的技术和观念。没有企业消息传送,开发者就不能在同步调用和解偶的应用间自由选择。SOA,CEP和其它高层次基于企业消息传送的观念也不会产生。更重要的是JMS规范也不会产生。
JMS规范对于Java世界的影响巨大,它使得消息传送变成Java的一部分,并且可以让所有的Java开发者使用。这对于允许Java连接商业上重要的应用非常必须,因为它为消息传送到使用提供一个标准的行为。这章的例子都非常简单和短,它们的目的是是你了解JMS。本书接下来的部分有更丰富的例子讨论,并且可以从网上下载。
现在我们对JMS及它提供什么有了一个基本的了解,下面将回顾这些例子。第三章提供的一个样例将会在全书使用。
第三章:ActiveMQ例子
本章内容
- 介绍本书每一个示例
- 使用Maven来编译和运行这些例子
- 怎样使用这些例子与ActiveMQ进行交互
ActiveMQ提供JMS规范要求的所有特性并且在这之上,提供了很多有用的特性。这些都在图3.1里描绘出来,并在本书剩下章节讨论。为了说明这些特性,我们提供了两个例子,这两个例子都是从真实商用领域得来的。相比前面的例子,这些例子更完整和简洁地展示ActiveMQ的特性。
其中一个例子是基于股票投资的,另一个例子是基于工作队列。这两个例子比ActiveMQ自带的例子更广泛地展现ActiveMQ。我们先介绍这些例子的使用场景,接着详细讨论如何使用它们。在你阅读这本书的任何时候,如果你需要回顾这些例子,你都可以回来阅读这一章。
股票投资例子演示了发布/订阅到消息传送领域。发布者向许多对该消息感兴趣的订阅者广播消息。消息发布到一个叫做主题的JMS目标,在线的订阅者客户端则接收消息。通过这种方式,代理器向每一个订阅者发送消息而不用订阅者去拉消息。每一个在线的订阅者接收到一条消息的副本。除非使用持久订阅,否则订阅者必须在线等待接收消息。在一个主题中,将使用发布/订阅模式将消息的副本发送给每一个订阅者。
工作队列例子演示里点对点消息传送领域。消息生产者发送消息到一个JMS队列,接收者则从这个队列接收消息。在点对点领域,消息的发送者和接收者没有时序性要求。队列会保存消息直到消费者准备好接收它们。如果消费者准备好,消息将发送给所有消费者,但是不会有两个或以上消费者接收到同一条消息。在点对点领域,队列里的消息通过循环方式发送到消费者。
不只是不同例子关注的消息传送领域不同,它们对应的使用场景也不同。另外,虽然接下来的例子看起来差不多,但是它们有一个很重要的不同就是应用于不同的消息传送领域。股票投资使用发布/订阅消息传送,而工作队列使用点对点的消息传送。这些例子的代码可以从Manning网站下载,URL:http://manning.com/snyder/activemq-in-action-examples-src.zip。
在这一章,首先我们会下载Maven并安装它,然后用它来编译和运行示例。之后,我们将回顾每一个例子并解释每一个例子的行为。在完成这些练习后,你将对这些例子有足够的了解可以在本书的其它部分认出它们,并且看看它们是如何被用来演示ActiveMQ特性。
3.1 下载Maven并编译例子
下面是下载和安装Maven的步骤:
- 从Apache Sofeware Foundation下载Maven,URL:http://maven.apache.org/。Maven提供tar和zip包,依据你操作系统选择不同的包。
- 在你的计算机上将压缩包解压到一个永久路径。
- 创建一个环境变量M2_HOME,并让它指向Maven目录
- 在Unix上,将$M2_HOME/bin目录添加到环境变量(在Windows上,将$M2_HOME/bin目录添加到%PATH%变量里)
- 通过运行如下命令确认Maven正确安装。
$ mvn -version Apache Maven 2.2.1 (r801777; 2009-08-06 13:16:01-0600) Java version: 1.5.0_19 Java home: /System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home Default locale: en_US, platform encoding: MacRoman OS name: "mac os x" version: "10.6.2" arch: "i386" Family: "unix"
如果Maven正确安装,你将看到与上面相似的输出。如果没看到相似输出,你必须先使它正确再进入下一步。你
可以在下面的地址获取更多关于安装Maven的说明.http://maven.apache.org/download.html#Installation
你必须有因特网连接
要使用这些例子,你必须有因特网连接。因为Maven必须为这些例子下载必须的依赖包。
如果你已经成功安装Maven,现在必须解压和编译这些例子了。在把包含源代码的例子解压后,你就可以编译了。现在移动到amq-in-action-example-src目录,然后运行下面的命令。为了将命令与输出区分开,下面的命令将用粗体字显示。
[amq-in-action-example-src] $ mvn clean install [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------- ----- [INFO] Building ActiveMQ in Action Examples [INFO] task-segment: [clean, install] [INFO] ------------------------------------------------------------------- ----- Downloading: http://localhost:8081/nexus/content/groups/public/org/apache/ maven/plugins/maven-clean-plugin/2.2/maven-clean-plugin-2.2.pom 3K downloaded (maven-clean-plugin-2.2.pom) ... [INFO] [install:install {execution: default-install}] [INFO] Installing /private/tmp/amq-in-action-example-src/target/ activemq-in-action-examples.jar to /Users/bsnyder/.m2/repository/org/ apache/activemq/book/activemq-in-action-examples/1.0-SNAPSHOT/
activemq-in-action-examples-1.0-SNAPSHOT.jar [INFO] Installing /private/tmp/amq-in-action-example-src/target/ activemq-in-action-examples-src.zip to /Users/bsnyder/.m2/repository/org/ apache/activemq/book/activemq-in-action-examples/1.0-SNAPSHOT/ activemq-in-action-examples-1.0-SNAPSHOT-src.zip [INFO] ------------------------------------------------------------------- ----- [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------- ----- [INFO] Total time: 57 seconds [INFO] Finished at: Fri Dec 04 22:35:57 MST 2009 [INFO] Final Memory: 24M/44M [INFO] ------------------------------------------------------------------- -----
由于输出信息太多,上面省略了一些。上面的信息说明了编译成功。当你看到BUILD SUCCESSFUL信息的时候,你就可以进入下面的学习了。如果你看到BUILD FAILURE,你必须找出出错原因并且解决后才能进入下一步。
3.2用例一:股票投资例子
就像之前提到的,我们第一个用例是使用股票投资的例子来展示发布/订阅的消息传送机制。这个例子使用一个Publisher类来发送一条股票价格消息到一个主题,同时,注册了Listener的Consumer类用异步方式从主题消费消息。这三个类体现了如何产生不停变换的股票消息并发送到被消费者订阅到主题上。
在这个例子中,股票价格被发布的任意数量的主题上。主题的数量是由命令行携带给发布者和消费者的参数数量决定的。每一个类会动态地向/从主题发送或接收消息。图3.2和3.3在一个高层次上展示了这些例子要达到的目的。
为了这个演示,两个主题将被使用。Publisher类使用一个单独的JMS MessageProducer以每次10条的方式去发送1000虚拟的股票消息,并随机地发送到由命令行参数所指明的各个主题上。发送完1000条消息,它将被关闭。Consumer类对每个主题都创建了一个MessageConsumer并且注册一个JMS MessageListener。因为这个例子是演示发布/订阅消息的,所以消费者必须在线消费消息(这个例子不使用持久订阅。)。下一步是真正地运行这个例子,这样你就能看到它们的动作了。
3.2.1运行股票投资例子
运行例子有以下步骤:
- 启动ActiveMQ
- 运行Consumer类
- 运行Publisher类
这些步骤看起来很简单。要注意的是,Consumer类必须比Publisher类先启动,这样它才能接收到所有的消息。这是因为这个例子是用来演示发布/订阅消息模式的,所以主题并不会为客户端保存消息(我们没有用持久订阅)。现在让我们开始这个例子吧。
首先,我们要打开一个终端或者命令行来运行ActiveMQ。这个只需一条命令,如下所示:
Listing 3.2 Start up ActiveMQ
[apache-activemq-5.4.1] $ ./bin/activemq console INFO: Using default configuration (you can configure options in one of these file: /etc/default/activemq /Users/bsnyder/.activemqrc) INFO: Invoke the following command to create a configuration file ./bin/activemq setup [/etc/default/activemq | /Users/bsnyder/.activemqrc] INFO: Using java '/System/Library/Frameworks/JavaVM.framework/Home/bin/java' INFO: Starting in foreground, this is just for debugging purposes (stop process by pressing CTRL+C) Java Runtime: Apple Inc. 1.6.0_20 /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home Heap sizes: current=258880k free=253105k max=258880k JVM args: -Xms256M -Xmx256M
-Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dcom.sun.management.jmxremote -Dactivemq.classpath=/Users/bsnyder/amq/apache-activemq-5.4.1/conf; -Dactivemq.home=/Users/bsnyder/amq/apache-activemq-5.4.1 -Dactivemq.base=/Users/bsnyder/amq/apache-activemq-5.4.1 ACTIVEMQ_HOME: /Users/bsnyder/amq/apache-activemq-5.4.1 ACTIVEMQ_BASE: /Users/bsnyder/amq/apache-activemq-5.4.1 Loading message broker from: xbean:activemq.xml ... INFO | Started SelectChannelConnector@0.0.0.0:8161
下一个任务是打开第二个终端(或命令行)来运行Consumer类。我们使用maven-exec-plugin来运行Consumer类。使用这个plugin时我们通过exec.args属性来传递一些系统参数。运行Consumer的例子如下:
Listing 3.3 Run the stock portfolio consumer
[amq-in-action-example-src] $ mvn exec:java - Dexec.mainClass=org.apache.activemq.book.ch3.portfolio.Consumer - Dexec.args="CSCO ORCL" [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'exec'. [INFO] org.apache.maven.plugins: checking for updates from central [INFO] org.codehaus.mojo: checking for updates from central [INFO] artifact org.codehaus.mojo:exec-maven-plugin: checking for updates from central [INFO] snapshot org.codehaus.mojo:exec-maven-plugin:1.1.2-SNAPSHOT: checking for updates from public-snapshots [INFO] snapshot org.codehaus.mojo:exec-maven-plugin:1.1.2-SNAPSHOT: checking for updates from central Downloading: http://localhost:8081/nexus/content/groups/public/org/codehaus/mojo/ exec-maven-plugin/1.1.2-SNAPSHOT/ exec-maven-plugin-1.1.2-20091120.114446-3.pom 4K downloaded (exec-maven-plugin-1.1.2-20091120.114446-3.pom) Downloading: http://localhost:8081/nexus/content/groups/public/org/codehaus/mojo/ mojo-parent/22/mojo-parent-22.pom 18K downloaded (mojo-parent-22.pom) Downloading: http://localhost:8081/nexus/content/groups/public-snapshots/org/codehaus/ mojo/exec-maven-plugin/1.1.2-SNAPSHOT/ exec-maven-plugin-1.1.2-20091120.114446-3.jar 36K downloaded (exec-maven-plugin-1.1.2-20091120.114446-3.jar) [INFO] ------------------------------------------------------------------- ----- [INFO] Building ActiveMQ in Action Examples [INFO] task-segment: [exec:java] [INFO] ------------------------------------------------------------------- ----- [INFO] Preparing exec:java [INFO] No goals needed for project - skipping
[WARNING] POM for 'woodstox:wstx-asl:pom:3.2.7:compile' is invalid. Its dependencies (if any) will NOT be available to the current build. Downloading: http://localhost:8081/nexus/content/groups/public/org/apache/commons/ commons-exec/1.0.1/commons-exec-1.0.1.pom 7K downloaded (commons-exec-1.0.1.pom) Downloading: http://localhost:8081/nexus/content/groups/public/org/apache/commons/ commons-exec/1.0.1/commons-exec-1.0.1.jar 48K downloaded (commons-exec-1.0.1.jar) [INFO] [exec:java {execution: default-cli}]
在列表3.3你可以看到Maven下载了一些运行例子必须的组件。当Consumer启动完毕,我们就可以启动Publisher并开始发送股票消息到两个有命令行参数指定的目标CSCO和ORCL。这两个名字是随机取的,你可以改为其它任意字符串。重要的是Consumer和Publisher必须使用相同的参数。
运行Consumer时构建错误
当你运行Consumer类时,如果你看到BUILD ERROR,那么你必须先编译源代码,然后再运行它。下面的命令可以用来编译源代码:$ mvn clean install 这个命令编译并打包源代码。之后你可以用原先的命令来运行Consumer类了。
注意,现在输出已经停了,看起来就像Consumer挂起来。这是正常的,因为它正在等待消息。当Publisher开始发送消息,Consumer就会开始消费它们。
在输出窗口为什么看到所有的组件都是从本地下载?
在3.1接如果Maven正确安装,那么它会下载运行例子所需要所有组件。在第一个输出窗口你可以看到它在下载组件。注意到所有的组件都是从本地下载而不是从远程Maven库下载。这是因为这些例子已经被配置为使用一个叫做Nexus的位于本地计算机的Maven库。Nexus提供很多好处,其中一个是它可以作为远程Maven库的代理缓存所有已下载组件。在Maven第一次下载组件后,它们都被保存到本地缓存里。如果多次编译,Nexus从本地缓存提供组件,这加快了编译速度。更多关于Nexus的消息请看:http://nexus.sonatype.org/
接下来我们打开第三个终端来运行Publisher类。注意在exec.args中必须使用与原来运行Consumer相同的参数,因为maven-exec-plugin也会用这些参数来运行Publisher。下面是运行Publisher的例子。
Listing 3.4 Running the stock portfolio publisher
[amq-in-action-example-src] $ mvn exec:java -Dexec.mainClass=org.apache.activemq.book.ch3.portfolio.Publisher -Dexec.args="CSCO ORCL" [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'exec'. [INFO] ------------------------------------------------------------------- ----- [INFO] Building ActiveMQ in Action Examples [INFO] task-segment: [exec:java] [INFO] ------------------------------------------------------------------- ----- [INFO] Preparing exec:java [INFO] No goals needed for project - skipping [WARNING] POM for 'woodstox:wstx-asl:pom:3.2.7:compile' is invalid. Its dependencies (if any) will NOT be available to the current build. [INFO] [exec:java {execution: default-cli}] Sending: {offer=62.6861410176471, price=62.62351750014696, up=true, stock=ORCL} on destination: topic://STOCKS.ORCL Sending: {offer=55.508573596887715, price=55.45312047641131, up=true, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=62.527946513790205, price=62.46548103275745, up=false, stock=ORCL} on destination: topic://STOCKS.ORCL Sending: {offer=55.78778713074073, price=55.73205507566507, up=true, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=55.593918646251986, price=55.53838026598601, up=false, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=55.83360390719586, price=55.777826081114746, up=true, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=55.99233608275527, price=55.93639968307221, up=true, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=62.006501598331475, price=61.94455704129019, up=false, stock=ORCL} on destination: topic://STOCKS.ORCL Sending: {offer=55.53698948617822, price=55.48150797820003, up=false, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=61.43866500377897, price=61.377287716062916, up=false, stock=ORCL} on destination: topic://STOCKS.ORCL Published '10' of '10' price messages Sending: {offer=55.466945358331216, price=55.41153382450671, up=false, stock=CSCO} on destination: topic://STOCKS.CSCO Sending: {offer=61.27694222131968, price=61.215726494824864, up=false, stock=ORCL} on destination: topic://STOCKS.ORCL ... Published '10' of '30' price messages ...
运行Publisher时,由于Maven在之前运行Consumer时已经下载了所有的依赖组件,所以无需再下载了。在输出窗口下半部分可以看到,股票价格消息每10条一组发送到了两个主题。由于输出太多,部分没有显示在上面,但是要知道Publisher将会一直运行直到发送完1000条消息。
运行完Publisher,现在切换到第二个终端,你可以看到Consumer正在从主题上消费消息。
... [INFO] [exec:java {execution: default-cli}] ORCL 62.62 62.69 up CSCO 55.45 55.51 up ORCL 62.47 62.53 down CSCO 55.73 55.79 up CSCO 55.94 55.99 up CSCO 55.41 55.47 down ORCL 61.22 61.28 down ORCL 61.42 61.48 up ...
输出的前缀是注册到ORCL和CSCO主题上的监听器打印出来的。这个输出说明了现在正在消费由Publisher发送到两个主题的消息。当Publisher发送我1000条消息后,它将被关闭。但是Consumer仍会继续运行并挂起等待更多的消息。你可以在第二个终端按CTRL-C来关闭Consumer。
现在你已经看到ActiveMQ怎样以发布/订阅模式运行,下一节将展示它如何以点对点消息传送方式运行。
3.3用例二:工作队列
第二个例子通过工作队列来演示点对点的消息传送。这个例子使用Producer类来向工作队列发送消息,而注册了Listener的Consumer类则用异步的方式从队列消费消息。这3个类展示了基本的JMS点对点消息传送是如何工作的。这些类和上一个例子很相似,区别就是用在不同的消息传送领域。
Producer类发送消息到JOBS.suspend和JOBS.delete队列,Consumer则消费它们。图3.3在一个高层次描述了这个过程。
Producer使用一个单独的MessageProducer来发送1000条工作消息,每次10条,随机地发送到两个队列。发完1000条消息后关闭。Consumer类对每一个队列使用一个MessageConsumer并注册一个JMS MessageListener来使用消息并输出相关信息。
3.3.1 运行工作队列例子
运行工作队列的步骤几乎和前一个例子的步骤一样:
- 启动ActiveMQ
- 运行Producer类
- 运行Consumer类
这些步骤都很简单,但有一个地方要注意。我们现在使用的是点对点消息传送,队列会保存消息直到它们被消费或者过期。所以即使Produce先于Consumer启动,Consumer也不会错失任何消息。
就像股票投资例子一样,第一个步骤是启动ActiveMQ。这个步骤就不再详述了,因为它和之前完全一样。
接上来,打开第二个终端或命令行窗口来运行Producer。
Listing 3.5 Running the job queue publisher
[amq-in-action-example-src] $ mvn exec:java -Dexec.mainClass=org.apache.activemq.book.ch3.jobs.Publisher [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'exec'. [INFO] ------------------------------------------------------------------- ----- [INFO] Building ActiveMQ in Action Examples [INFO] task-segment: [exec:java] [INFO] ------------------------------------------------------------------- ----- [INFO] Preparing exec:java [INFO] No goals needed for project - skipping [WARNING] POM for 'woodstox:wstx-asl:pom:3.2.7:compile' is invalid.
Its dependencies (if any) will NOT be available to the current build. [INFO] [exec:java {execution: default-cli}] Sending: id: 1000000 on queue: queue://JOBS.delete Sending: id: 1000001 on queue: queue://JOBS.delete Sending: id: 1000002 on queue: queue://JOBS.delete Sending: id: 1000003 on queue: queue://JOBS.delete Sending: id: 1000004 on queue: queue://JOBS.delete Sending: id: 1000005 on queue: queue://JOBS.delete Sending: id: 1000006 on queue: queue://JOBS.delete Sending: id: 1000007 on queue: queue://JOBS.delete Sending: id: 1000008 on queue: queue://JOBS.delete Sending: id: 1000009 on queue: queue://JOBS.delete Published '10' of '10' job messages Sending: id: 1000010 on queue: queue://JOBS.delete Sending: id: 1000011 on queue: queue://JOBS.suspend ... Published '10' of '30' job messages ...
注意运行Producer时并不需要传递任何参数。Publisher类包含了两个队列delete和suspend。这清楚地显示在输出内容上。Producer会发送1000条消息然后关闭。
接下来的任务是打开另一个终端或命令行窗口来运行Consumer,Consumer会从两个队列消费消息。这个命令如下:
Listing 3.6 Running the job queue consumer
[amq-in-action-example-src] $ mvn exec:java -Dexec.mainClass=org.apache.activemq.book.ch3.jobs.Consumer [INFO] Scanning for projects... [INFO] Searching repository for plugin with prefix: 'exec'. [INFO] ------------------------------------------------------------------- ----- [INFO] Building ActiveMQ in Action Examples [INFO] task-segment: [exec:java] [INFO] ------------------------------------------------------------------- ----- [INFO] Preparing exec:java [INFO] No goals needed for project - skipping [WARNING] POM for 'woodstox:wstx-asl:pom:3.2.7:compile' is invalid. Its dependencies (if any) will NOT be available to the current build. [INFO] [exec:java {execution: default-cli}] suspend id:1000003 suspend id:1000010 suspend id:1000012 suspend id:1000013 suspend id:1000015 suspend id:1000022 suspend id:1000025 suspend id:1000027 delete id:1000000 delete id:1000001
delete id:1000002 delete id:1000004 delete id:1000005 ...
一开始,Consumer会运行得很快,消费完队列里所有的消息。当它赶上Producer时,Consumer开始慢下来,然后和Producer同步,直到Producer发送完1000条消息。当所有的消息发送完后,Producer自动终止,而Consumer不会,你需要在第三个终端上按CTRL-C来停止它。
工作队列的例子结束了。现在你看到了ActiveMQ是如何以点对点消息传送方式工作的。
3.4总结
对本书例子的简单介绍的目的在于--快速和专注。工作和投资的例子在商业领域是很常见的,但这只是消息传送领域大量例子中的两个。虽然这两个例子只是在一个较高层次演示了两种消息传送领域,但它们还有其它作用。通过使用ActiveMQ提供的特性,这两个例子将在本书剩下部分被修改。所以你会经常看到这两个例子,不过稍有不同而已。
本书的第一部分(1~3章)先向你介绍了ActiveMQ,使你对ActiveMQ有一个高层次的理解。接下来是关注面向消息中间件和JMS规范。虽然这些主题不只是与ActiveMQ有关,但对于理解ActiveMQ很有帮助。你也看了一些例子,这些例子将在整本书中使用。第一部分的内容是掌握ActiveMQ的热身,第二部分你将学会对ActiveMQ的连接,持久化,安全等各个方面进行配置。