19.2. 使用Spring JMS
19.2.1. JmsTemplate
JmsTemplate
类有两个实现方式。JmsTemplate
类使用JMS 1.1的API,而子类JmsTemplate102
使用了JMS 1.0.2的API。
使用JmsTemplate
的代码只需要实现规范中定义的回调接口。 MessageCreator
回调接口通过JmsTemplate中调用代码提供的Session来创建一条消息。然而,为了允许更复杂的JMS API应用,回调接口SessionCallback
为用户提供JMS session,并且回调接口ProducerCallback
将Session和MessageProducer对显露给用户。
JMS API有两种发送方法,一种采用发送模式、优先级和存活时间作为服务质量(QOS)参数,另一种使用无需QOS参数的缺省值方法。由于在JmsTemplate
中有许多种发送方法,QOS参数通过bean的属性方式进行设置,从而避免在多种发送方法中重复。同样,使用setReceiveTimeout
属性值来设置同步接收调用的超时值。
某些JMS供应者允许通过ConnectionFactory的配置来设置缺省的QOS值。这样在调用MessageProducer
的发送方法send(Destination destination, Message message)
时会使用那些不同的QOS缺省值,而不是JMS规范中定义的值。所以,为了提供对QOS值的一致管理,JmsTemplate
必须通过设置布尔值属性isExplicitQosEnabled为true,使它能够使用自己的QOS值。
19.2.2. 连接工厂
JmsTemplate
需要一个对ConnectionFactory
的引用。ConnectionFactory
是JMS规范的一部分,并且是使用JMS的入口。客户端应用通常用它作工厂配合JMS提供者去创建连接,并封装许多和供应商相关的配置参数,例如SSL的配置选项。
当在EJB里使用JMS时,供应商会提供JMS接口的实现,这样们可以参与声明式事务管理并提供连接池和会话池。为了使用这个JMS实现,Java EE容器通常要求你在EJB或servlet部署描述符中声明一个JMS连接工厂做为 resource-ref。为确保可以在EJB内使用JmsTemplate
的这些特性,客户应用应当确保它引用了被管理的ConnectionFactory实现。
Spring提供了一个ConnectionFactory
接口的实现,SingleConnectionFactory
,它将在所有的createConnection
调用中返回一个相同的Connection
,并忽略所有对close
的调用。这在测试和独立环境中相当有用,因为多个JmsTemplate
调用可以使用同一个连接以跨越多个事务。SingleConnectionFactory
通常引用一个来自JNDI的标准ConnectionFactory
。
19.2.3. (消息)目的地管理
和连接工厂一样,目的地是可以在JNDI中存储和获取的JMS管理的对象。配置一个Spring应用上下文时,可以使用JNDI工厂类JndiObjectFactoryBean
把对你对象的引用依赖注入到JMS目的地中。然而,如果在应用中有大量的目的地,或者JMS供应商提供了特有的高级目的地管理特性,这个策略常常显得很麻烦。创建动态目的地或支持目的地的命名空间层次就是这种高级目的地管理的例子。JmsTemplate
将目的地名称到JMS目的地对象的解析委派给DestinationResolver
接口的一个实现。JndiDestinationResolver
是JmsTemplate
使用的默认实现,并且提供动态目的地解析。同时JndiDestinationResolver
作为JNDI中的目的地服务定位器,还可选择回退去使用DynamicDestinationResolver
中的行为。
经常见到一个JMS应用中使用的目的地在运行时才知道,因此,当部署一个应用时,它不能用可管理的方式创建。这是经常发生的,因为在互相作用的系统组件间有些共享应用逻辑会在运行的时候按照共同的命名规范创建消息目的地。虽然动态创建目的地不是JMS规范的一部分,但是大多数供应商已经提供了这个功能。用户为动态创建的目的地定义和临时目的地不同的名字,并且通常不被注册到JNDI中。不同供应商创建动态消息目的地所使用的API差异很大,因为和目的地相关的属性是供应商特有的。然而,有时由供应商会作出一个简单的实现选择-忽略JMS规范中的警告,使用TopicSession
的方法createTopic(String topicName)
或者QueueSession
的方法createQueue(String queueName)
来创建一个带默认值属性的新目的地。依赖于供应商的实现,DynamicDestinationResolver
也可能创建一个物理上的目的地,而不只是一个解析。
布尔属性pubSubDomain用来配置JmsTemplate
使用什么样的JMS域。这个属性的默认值是false,使用点到点的域,也就是队列。在1.0.2的实现中,这个属性值用来决定JmsTemplate
将消息发送到一个Queue
还是一个Topic
。这个标志在1.1的实现中对发送操作没有影响。然而,在这两个JMS版本中,这个属性决定了通过接口DestinationResolver
的实现来决定如何解析动态消息目的地。
你还可以通过属性defaultDestination配置一个带有默认目的地的JmsTemplate
。不指明目的地的发送和接受操作将使用该默认目的地。
19.2.4. 消息侦听容器
在EJB世界里,JMS消息最常用的功能之一是用于实现消息驱动bean(MDBs)。Spring提供了一个方法来创建消息驱动的POJO(MDPs),并且不会把用户绑定在某个EJB容器上。(关于Spring的MDP支持的细节请参考标题为第 19.4.2 节 “异步接收 - 消息驱动的POJOs”的节)
通常用AbstractMessageListenerContainer
的一个子类从JMS消息队列接收消息并驱动被注射进来的MDP。AbstractMessageListenerContainer
负责消息接收的多线程处理并分发到各MDP中。一个消息侦听容器是MDP和消息提供者之间的一个中介,用来处理消息接收的注册,事务管理的参与,资源获取和释放,异常转换等等。这使得应用开发人员可以专注于开发和接收消息(可能的响应)相关的(复杂)业务逻辑,把和JMS基础框架有关的样板化的部分委托给框架处理。
Spring提供了三种AbstractMessageListenerContainer
的子类,每种各有其特点。
19.2.4.1. SimpleMessageListenerContainer
这个消息侦听容器是三种中最简单的。它在启动时创建固定数量的JMS session并在容器的整个生命周期中使用它们。这个类不能动态的适应运行时的要求或参与消息接收的事务处理。然而它对JMS提供者的要求也最低。它只需要简单的JMS API。
19.2.4.2. DefaultMessageListenerContainer
这个消息侦听器使用的最多。和SimpleMessageListenerContainer
一样,这个子类不能动态适应运行时侯的要求。然而,它可以参与事务管理。每个收到的消息都注册到一个XA事务中(如果配置过),这样就可以利用XA事务语义的优势了。这个类在对JMS提供者的低要求和提供包括事务参于等的强大功能上取得了很好的平衡。
19.2.4.3. ServerSessionMessageListenerContainer
这个子类是三者中最强大的。它利用JMS ServerSessionPool SPI允许对JMS session进行动态管理。它也支持事务。使用这种消息侦听器可以获得强大的运行时调优功能,但是对使用到的JMS提供者有很高的要求(ServerSessionPool SPI)。如果不需要运行时的性能调整,请使用DefaultMessageListenerContainer
或SimpleMessageListenerContainer
。
19.2.5. 事务管理
Spring提供了一个JmsTransactionManager
为单个JMSConnectionFactory
管理事务。这将允许JMS应用利用第 9 章 事务管理中描述的Spring的事务管理功能。JmsTransactionManager
从指定的ConnectionFactory
绑定了一个Connection/Session对到线程上。然而,在Java EE环境中,SingleConnectionFactory
将把连接和session放到缓冲池中,所以绑定到线程的实例将依赖越缓冲池的行为。在标准环境下,使用Spring的SingleConnectionFactory
将使得和每个事务相关的JMS连接有自己的session。JmsTemplate
也可以和JtaTransactionManager
以及具有XA能力的JMS ConnectionFactory
一起使用来提供分布式交易。
当使用JMS API从一个连接中创建session时,在受管理的和非受管理的事务环境下重用代码会可能会让人迷惑。这是因为JMS API只有一个工厂方法来创建session并且它需要用于事务和模式确认的值。在受管理的环境下,由事务结构环境负责设置这些值,这样在供应商包装的JMS连接中可以忽略这些值。当在一个非管理性的环境中使用JmsTemplate
时,你可以通过使用属性SessionTransacted
和SessionAcknowledgeMode
来指定这些值。当配合 JmsTemplate
中使用PlatformTransactionManager
时,模板将一直被赋予一个事务性JMS的 Session
。
19.3. 发送一条消息
JmsTemplate
包含许多方便的方法来发送消息。有些发送方法可以使用 javax.jms.Destination
对象指定目的地,也可以使用字符串在JNDI中查找目的地。没有目的地参数的发送方法使用默认的目的地。这里有个例子使用1.0.2版的JMS实现发送消息到一个队列。
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;
import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.JmsTemplate102;
public class JmsQueueSender {
private JmsTemplate jmsTemplate;
private Queue queue;
public void setConnectionFactory(ConnectionFactory cf) {
this.jmsTemplate = new JmsTemplate102(cf, false);
}
public void setQueue(Queue queue) {
this.queue = queue;
}
public void simpleSend() {
this.jmsTemplate.send(this.queue, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("hello queue world");
}
});
}
}
这个例子使用MessageCreator
回调接口从提供的Session
对象中创建一个文本消息,并且通过一个ConnectionFactory
的引用和指定消息域的布尔值来创建JmsTemplate
。提供了一个无参数的构造方法和connectionFactory / queuebean属性并可用于创建实例(使用一个BeanFactory或者普通Java 代码code)。或者考虑从Spring的基类JmsGatewaySupport
,它对JMS配置具有内置的bean属性,继承一个类。
当在应用上下文中配置JMS 1.0.2时,重要的是记得设定布尔属性pubSubDomain的值以指明你是要发送到队列还是主题。
方法send(String destinationName, MessageCreator creator)
让你利用目的地的字符串名字发送消息。如果这个名字在JNDI中注册,你应当将模板中的destinationResolver属性设置为JndiDestinationResolver
的一个实例。
如果你创建JmsTemplate
并指定一个默认的目的地,send(MessageCreator c)
发送消息到这个目的地。
19.3.1. 使用消息转换器
为便于发送领域模型对象,JmsTemplate
有多种以一个Java对象为参数并做为消息数据内容的发送方法。JmsTemplate
里可重载的方法convertAndSend
和receiveAndConvert
将转换的过程委托给接口MessageConverter
的一个实例。这个接口定义了一个简单的合约用来在Java对象和JMS消息间进行转换。缺省的实现SimpleMessageConverter
支持String
和TextMessage
,byte[]
和BytesMesssage
,以及java.util.Map
和MapMessage
之间的转换。使用转换器,可以使你和你的应用关注于通过JMS接收和发送的业务对象而不用操心它是具体如何表达成JMS消息的。
目前的沙箱模型包括一个MapMessageConverter
,它使用反射转换JavaBean和MapMessage
。其他流行可选的实现方式包括使用已存在的XML编组的包,例如JAXB,Castor, XMLBeans, 或XStream的转换器来创建一个表示对象的TextMessage
。
为方便那些不能以通用方式封装在转换类里的消息属性,消息头和消息体的设置,通过MessagePostProcessor
接口你可以在消息被转换后并且在发送前访问该消息。下例展示了如何在java.util.Map
已经转换成一个消息后更改消息头和属性。
public void sendWithConversion() {
Map m = new HashMap();
m.put("Name", "Mark");
m.put("Age", new Integer(47));
jmsTemplate.convertAndSend("testQueue", m, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setIntProperty("AccountID", 1234);
message.setJMSCorrelationID("123-00001");
return message;
}
});
}
This results in a message of the form:
这将产生一个如下的消息格式:
MapMessage={
Header={
... standard headers ...
CorrelationID={123-00001}
}
Properties={
AccountID={Integer:1234}
}
Fields={
Name={String:Mark}
Age={Integer:47}
}
}
19.3.2. SessionCallback
和ProducerCallback
虽然send操作适用于许多常见的使用场景,但是有时你需要在一个JMS Session
或者MessageProducer
上执行多个操作。接口SessionCallback
和ProducerCallback
分别提供了JMS Session
和Session
/ MessageProducer
对。JmsTemplate
上的execute()
方法执行这些回调方法。
19.4. 接收消息
19.4.1. 同步接收
虽然JMS一般都和异步处理相关,但它也可以同步的方式使用消息。可重载的receive(..)
方法提供了这种功能。在同步接收中,接收线程被阻塞直至获得一个消息,有可能出现线程被无限阻塞的危险情况。属性receiveTimeout指定了接收器可等待消息的延时时间。
19.4.2. 异步接收 - 消息驱动的POJOs
类似于EJB世界里流行的消息驱动bean(MDB),消息驱动POJO(MDP)作为JMS消息的接收器。MDP的一个约束(但也请看下面的有关javax.jms.MessageListener
类的讨论)是它必须实现javax.jms.MessageListener
接口。另外当你的POJO将以多线程的方式接收消息时必须确保你的代码是线程-安全的。
以下是MDP的一个简单实现:
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
public class ExampleListener implements MessageListener {
public void onMessage(Message message) {
if (message instanceof TextMessage) {
try {
System.out.println(((TextMessage) message).getText());
} catch (JMSException ex) {
throw new RuntimeException(ex);
}
} else {
throw new IllegalArgumentException("Message must be of type TextMessage");
}
}
}
一旦你实现了MessageListener
后就可以创建一个消息侦听容器。
请看下面例子是如何定义和配置一个随Sping发行的消息侦听容器的(这个例子用DefaultMessageListenerContainer
)
<!-- this is the Message Driven POJO (MDP) -->
<bean id="messageListener" class="jmsexample.ExampleListener" />
<!-- and this is the attendant message listener container -->
<bean id="listenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5"/>
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="messageListener" />
关于各个消息侦听容器实现的特色请参阅相关的Spring Javadoc文档。
19.4.3. SessionAwareMessageListener
接口
SessionAwareMessageListener
接口是一个Spring专门用来提供类似于JMS MessageListener
的接口,也提供了从接收Message
来访问JMS Session
的消息处理方法。
package org.springframework.jms.listener;
public interface SessionAwareMessageListener {
void onMessage(Message message, Session session) throws JMSException;
}
如果你希望你的MDP可以响应所有接收到的消息(使用onMessage(Message, Session)
方法提供的Session
)那么你可以选择让你的MDP实现这个接口(优先于标准的JMS MessageListener
接口)。所有随Spring发行的支持MDP的消息侦听容器都支持MessageListener
或SessionAwareMessageListener
接口的实现。要注意的是实现了SessionAwareMessageListener
接口的类通过接口和Spring有了耦合。是否选择使用它完全取决于开发者或架构师。
请注意SessionAwareMessageListener
接口的'onMessage(..)'
方法会抛出JMSException
异常。和标准JMS MessageListener
接口相反,当使用SessionAwareMessageListener
接口时,客户端代码负责处理任何抛出的异常。
19.4.4. MessageListenerAdapter
MessageListenerAdapter
类是Spring的异步支持消息类中的不变类(final class):简而言之,它允许你几乎将任意一个类做为MDP显露出来(当然有某些限制)。
注意
如果你使用JMS 1.0.2 API,你将使用和MessageListenerAdapter
一样功能的类MessageListenerAdapter102
。
考虑如下接口定义。注意虽然这个接口既不是从MessageListener
也不是从SessionAwareMessageListener
继承来得,但通过MessageListenerAdapter
类依然可以当作一个MDP来使用。同时也请注意各种消息处理方法是如何根据他们可以接收并处理消息的内容来进行强类型匹配的。
public interface MessageDelegate {
void handleMessage(String message);
void handleMessage(Map message);
void handleMessage(byte[] message);
void handleMessage(Serializable message);
}
public class DefaultMessageDelegate implements MessageDelegate {
// implementation elided for clarity...
}
特别请注意,上面的MessageDelegate
接口(上文中DefaultMessageDelegate
类)的实现完全不依赖于JMS。它是一个真正的POJO,我们可以通过如下配置把它设置成MDP。
<!-- this is the Message Driven POJO (MDP) --><bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="jmsexample.DefaultMessageDelegate"/>
</constructor-arg>
</bean><!-- and this is the attendant message listener container... -->
<bean id="listenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5"/>
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="messageListener" />
下面是另外一个只能处理接收JMSTextMessage
消息的MDP示例。注意消息处理方法是如何实际调用'receive'
(在MessageListenerAdapter
中默认的消息处理方法的名字是'handleMessage'
)的,但是它是可配置的(你下面就将看到)。注意'receive(..)'
方法是如何使用强制类型来只接收和处理JMS TextMessage
消息的。
public interface TextMessageDelegate {
void receive(TextMessage message);
}
public class DefaultTextMessageDelegate implements TextMessageDelegate {
// implementation elided for clarity...
}
辅助的MessageListenerAdapter
类配置文件类似如下:
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="jmsexample.DefaultTextMessageDelegate"/>
</constructor-arg>
<property name="defaultListenerMethod" value="receive"/>
<!-- we don't
<property name="messageConverter">
<null/>
</property>
</bean>
请注意,如果上面的'messageListener'
收到一个不是TextMessage
类型的JMS Message
,将会产生一个IllegalStateException
异常(随之产生的其他异常只被捕获而不处理)。
MessageListenerAdapter
还有一个功能就是如果处理方法返回一个非空值,它将自动返回一个响应消息
。
请看下面的接口及其实现:
public interface ResponsiveTextMessageDelegate {
// notice the return type...
String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
// implementation elided for clarity...
}
如果上面的DefaultResponsiveTextMessageDelegate
和MessageListenerAdapter
联合使用,那么任意从执行'receive(..)'
方法返回的非空值都将(缺省情况下)转换成一个TextMessage
。这个返回的TextMessage
将被发送到原来的Message
中JMS Reply-To属性定义的目的地
(如果存在),或者是MessageListenerAdapter
设置(如果配置了)的缺省目的地
;如果没有定义目的地
,那么将产生一个InvalidDestinationException
异常(此异常将不会只被捕获而不处理,它将沿着调用堆栈上传)。
19.4.5. 事务中的多方参与
参与到事务中只需要一点微小的改动。你需要创建一个事务管理器,并且注册到一个可以参与事务的子类中(DefaultMessageListenerContainer
或ServerSessionMessageListenerContainer
)。
为了创建事务管理器,你需要创建一个JmsTransactionManager
的实例并提供给它一个支持XA事务功能的连接工厂。
<bean id="transactionManager" class="org.springframework.jms.connection.JmsTransactionManager">
<property name="connectionFactory" ref="connectionFactory" />
</bean>
然后你只需要把它加入到我们先前的容器配置中。容器会处理其他的事情。
<bean id="listenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5" />
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="messageListener" />
<property name="transactionManager" ref="transactionManager" />
</bean>