第29章 Spring框架对JMS的集成
本章内容
-
说说JMS的身世
-
使用JMS API进行应用开发的传统套路
-
Spring改进后的JMS实战格斗术
29.1 说说JMS的身世
在JMS横空出世之前,存在多种MOM(Message-Oriented Middleware,面向消息中间件)产品。在企业信息系统中使用这些MOM,通常不得不使用各个MOM产品特定的访问API,可能有C语言实现的,也可能有C++或者Java语言实现的。为了能够让Java平台上使用消息机制开发的应用程序有一个统一的访问不同MOM产品的方式,JMS规范(JSR 914)就此诞生了!
JMS是一套API接口规范。因为各种MOM产品的功能特性可能多种多样,JMS规范在规范本身的实现复杂度与功能支持的广度上做了一个权衡,使得开发复杂的企业级消息应用应保无忧。JMS规范有两个主要版本,即JMS 1.02和JMS 1.1。在JMS 1.02规范中定义了如下两种明确的消息域模型(Messaging Domain)。
-
点对点模式(Point-to-Point),简称PTP模式。消息发送者将消息发送到指定的消息队列(Queue),消息接收者也是从同一消息队列中顺序获取数据,并对获取的消息进行处理。这种模式类似于多线程环境下的“生产者-消费者”场景。
-
发布订阅模式(Publish/Subscribe),即Pub/Sub模式。消息发送者将消息发送到指定的消息主题(Topic),多个消息接收者可以从相应主题中获取某个相同消息的拷贝并进行处理。这种模式很接近生活中的报纸和杂志订阅的场景:不同的杂志社/报社就相当于消息发送者,他们发行的期刊就相当于不同的主题。如果我们订阅了其中某些主题(期刊报纸),那么,每个人可以获得同一主题(报纸期刊)的一份副本(消息),然后每个人怎么读这些消息那就因人(消息接收者)而异了。
JMS1 .02规范为这两种消息域模型分别定义了两套独立的消息访问API接口设计。但JMS 1.1之后更倾向于采用较为通用的消息访问API接口,也就是说,可以使用一套通用的API访问现有的两种消息域模型。如果是新开发的系统,应该尽量使用JMS 1.1之后提出的新的通用访问API,因为将来可能不赞成使用之前JMS 1.02规范中为每种消息域模型单独提供的API接口设计。
一个典型的JMS应用通常由以下几个部分组成。
-
JMS类型的客户端程序(JMS Client)。使用Java语言编写的用于发送或者接收消息的程序,这实际上也正是我们开发人员所要实现的东西。
-
非JMS类型的客户端程序(non-JMS Client)。使用相应消息系统或者产品提供的本地API,而不是JMS API来进行消息访问的程序。在JMS规范没有诞生之前,这种类型的客户端应该是比较常见的。
-
消息(Message)。作为消息应用,传递的当然是各种消息。JMS规定了5种消息类型,即StrearMessage、BytesMessage、MapMessage、TextMessage以及ObjectMessage。我们可以根据具体应用场景选择处理合适类型的消息。
-
JMS Provider。JMS规范指定了一系列的API接口,而加入JMS规范的各种MOM产品,需要根据规范提供特定于它们自身产品的JMS API接口实现,以及各种相关的服务(比如消息的存储转发等)。这些特定于MOM产品的JMS API接口实现以及相关功能实现就称之为JMS Provider。现有的实现有Apache ActiveMQ(
http://activemq.apache.org/
)、JBoss Messaging(http://www.jboss.org/jbossmessaging/
)以及IBM WebSphere MQ(http://www.306.ibm.com/software/integration/wmq/
)等。 -
受管理对象(Administered Object)。为了向不同JMS类型客户端程序屏蔽各种JMS Provider之间的差异性,系统管理员可以将某些特定类型的对象,通过JMS Provider提供的管理工具提前配置到系统中。这些提前配置到系统然后再交给JMS类型客户端使用的对象就叫做受管理对象。受管理对象有如下两种类型。
-
ConnectionFactory。客户端需要通过ConnectonFactory获取相应的Connection,并创建其他相关对象才能访问消息系统中的消息。
-
Destination。即消息发送或者接收的目的地。依消息域模型的不同,Destination可以划分为更加具体的消息队列(Queue)和消息主题(Topic)两种类型。
-
通常,受管理对象被绑定到相应的JNDI服务。这样,客户端只需要通过JNDI查找指定的接口类型并使用即可,根本不需要知道这些受管理对象所对应的具体实现到底是什么类型。临时替换其他类型的JMS Provider,只需要重新配置受管理对象并绑定到JNDI,而这些不会对客户端造成任何影响。所以,JNDI所提供的“隔离层”作用在这里可见一斑。
使用JMS可以帮助我们构建低耦合且极具灵活性的应用系统,从大的方面讲,我们可以使用JMS集成各种遗留系统,或者搭建现有系统与新开发的系统之间的通信桥梁,比如各种金融或者信贷系统。之前可能多使用MainFrame架构,而新的应用可能构建PC上,那通过JMS可以很好地集成这些不同类型的系统并使它们共同工作;从小的方面讲,JMS同样可以用于系统中各个组件之间的交互和通信。不过,因为引入了分布式概念,在这种场景下使用JMS需要确保其必要性。
应该说,JMS在JavaEE平台还是比较令人瞩目的,只不过,可能我们平常开发中很少用到,所以,大多数人对其都比较陌生。希望以上的简单介绍能够带你进入到JMS的世界中来。下面我们将剖析Spring对JMS的各种支持,首先了解JMS还是十分必要的。
提示:要了解JMS,最好的做法就是通读一遍JMS规范(JMS Specification)文档。短短140页的内容足以让我们了解JMS的来龙去脉。当然,这并非是理解余下内容的必要条件。不过,要是需要使用JMS开发系统的话,还是建议通读一遍JMS规范定义文档。
29.2 使用JMS API进行应用开发的传统套路
无论是JMS规范还是有关介绍JMS的大部分文章,它们所给出的代码示例大都是使用JMS原始API进行编写的。这当然有助于我们近距离地接触JMS API,但实际工作中要是同样按照这一套路走下来的话,或许我们就该反思一下了。因为实例的演示和实际的开发终究会有距离的。对于JMS的使用来说,情况也是如此。
对于初次使用JMS API开发应用的人来说,JMS API的使用看起来可能略显复杂。不过,稍加留意,我们就会发现其中是有规律可循的,不信?那不妨来看一下通常是如何实现消息发送功能的,见下方代码清单。
ConnectionFactory connectionFactory = lookupCFViaJNDI();
Destination dest = lookupDestViaJNDI();
Connection con = null;
Session session = null;
try {
con = connectionFactory.createConnection();
session = con.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer messageProducer = session.createProducer(dest);
TextMessage message = session.createTextMessage();
message.setText("Hi");
messageProducer.send(message);
messageProaucer.close();
session.close();
}
catch(JMSException e) {
e.printStackTrace(); // 不要这样做
}
finally {
if(con!=null) {
try {
con.close();
} catch (JMSException e) {
e.printStackTrace(); // 不要这样做
}
}
}
如果我们改用自然语言来描述这一过程,那么步骤如下所述。
(1)首先,从JNDI获取JMS的受管理对象,即ConnectionFactory和稍后要用到的一个或者多个Destination引用。
(2)使用获取的ConnectionFactory创建发送消息用的Connection。
(3)使用Connection创建发送消息用的Session。
(4)使用Session创建发送消息用的MessageProducer。
(5)使用Session创建将被发送的消息。
(6)调用MessageProducer发送创建完的消息到指定的Destination。
(7)最后做资源清理,包括关闭MessageProducer、Session以及Connection。
而且对于接收消息的功能实现来说,整个步骤其实也很相似。唯一不同的就是,现在要使用Session创建接收消息的MessageConsumer,并调用Connection的start()
方法开始传送消息以便接收并处理它。而像从JNDI获取相关受管理对象,创建Connection,创建Session,以及最后的资源清理这些步骤,几乎任何情况下都没有太大差别。说了这么多,我想你已经猜到Spring将对JMS API的使用做什么样的改进了。对,依然采用模板方法模式对JMS API的使用进行适度的封装,避免每次都重复编写同样的代码,以及可能存在的资源泄漏等容易出现的问题。
JMS从整体上来说是很成功的,只不过因为过于接近底层,从而导致在实际开发过程中使用过于烦琐,除此之外,其自身设计上还有一个问题,那就是对异常处理的设定不当。JMS规范虽然设计了一套以JMSException为首的异常类体系,但它们都归属于checked exception,对于消息的发送和接收过程中出现的异常,我们难道真的能够有效的处理吗?答案显然是否定的,网络出现问题,程序只能无可奈何,所以,JMSException最初更应该设计为unchecked exception,还好,现在Spring对这一问题也给予了适度的关注。
以上东西说的再多也都只是“温故”,而我们的实际目的却是“知新”,所以,还是让我们赶快看一下Spring都为JMS提供了哪些便利性支持吧!
29.3 Spring改进后的JMS实战格斗术
29.3.1 消息发送和同步接收
org.springframework.jms.core.JmsTemplate是Spring框架为JMS消息发送以及同步消息接收场景提供的核心支持。该类以模板方法模式对JMS API使用上的传统套路进行了封装,很好地避免了各种相关代码的重复和散落。
1. JmsTemplate亲密接触
JmsTemplate唯一必须依赖的就是一个JMS的ConnectionFactory。无论通过什么方式满足这一条件(通过JNDI获取也好,本地实例引用也好),只要这一条件得以满足,JmsTemplate即可马上进入工作状态,如下所示:
ConnectionFactory cf = ...;
JmsTemplate jmsTemplate = new JmsTemplate(cf);
...
这与JdbcTemplate需要一个DataSource是同样的道理。
JmsTemplate的核心模板方法定义的简单抽象如下方代码清单所示。
public Object execute(SessionCallback action, boolean startConnection) throws JmsException {
try {
// 获取相应的connection
// 创建相应的session
return action.doInJms(sessionToUse);
} catch(JMSException ex) {
// 转译并抛出相应异常....
}
finally {
// 清理相应资源
}
}
该方法通过sessionCallback回调接口为用户提供自定义逻辑介入点,而资源管理等一系列其他问题则由当前模板方法统一管理。startConnection参数告知当前模板方法,是否要调用Connection的start()
方法开始传送消息。对于发送消息的操作来说,该操作不是必须的。如果当前要处理的是消息接收,那startConnection参数应该给予true值。鉴于startConnection可以区分当前方法用于消息发送和接收操作的性质,JmsTemplate还提供了一个简化版的execute模板方法实现,如下所示:
public Object execute(SessionCallback action) throws JmsException {
return execute(action, false);
}
也就是说,默认情况下,如果调用该模板方法进行消息处理,是不调用Connection的start()
方法的。确切地讲,该简化版模板方法只能用在发送消息的时候。
JmsTemplate的execute()
方法加上sessionCallback回调接口可以给予我们最大的操作权限,但它们并不是最快捷的方式,除非我们确实要对消息发送的步骤做更多的定制。大部分情况下,直接使用JmsTemplate构建在核心模板方法上的其他模板方法要更划算一些。
JmsTemplate的模板方法大略上划分为三类,一类专门用于消息的发送,一类用于消息的同步接收,最后一类是使用QueueBrower检查消息队列的情况。
下面是这三类模板方法的简单概括。
(1)JmsTemplate的消息发送模板方法。用于发送消息的模板方法可以进一步划分为如下三类(或者说三组):
a)使用ProducerCallback的模板方法。ProducerCallback接口定义如下:
public interface ProducerCallback {
Object doInJms(Session session, MessageProducer producer) throws JMSException;
}
使用ProducerCallback回调接口的execute()
模板方法是由使用sessionCallback回调接口的execute()
模板方法发展而来。使用execute(SessionCallback)
,我们需要在SessionCallback中,自己创建发送消息用的MessageProducer实例并在合适的时机关闭它,如下方代码清单所示。
jmsTemplate.execute(new SessionCallback() {
public Object doInJms(Session session) throws JMSException {
ObjectMessagerequestMessage=session,createObjectMessage();
MessageProducerproducer=createProducer(session,destination);
try {
producer.send(requestMessage);
}
finally {
JmsUtils.closeMessageProducer(producer);
}
return null;
}
});
而使用execute(ProduerCallback)
方法则不需要自己来管理MessageProducer实例的创建和关闭。在ProducerCallback中,直接使用模板方法已经创建好的MessageProducer实例即可,如以下代码所示:
jmsTemplate.execute(new ProducerCallback() {
public Object doInJms(Session session, MessageProducer producer) throws JMSException {
Message message = session.createObjectMessage();
producer.send(destination, message);
return null;
}
});
b)使用MessageCreator的模板方法。通常,JmsTemplate中以send(..)
命名的模板方法可以接受MessageCreator作为参数。这里的MessageCreator将负责发送的消息的创建和相关处理,send(..)
方法最终只需要直接发送MessageCreator创建完的消息即可,如以下代码所示:
jmsTemplate.send(new MessageCreator(} {
public Message createMessage(Session session) throws JMSException {
ObjectMessage message = session.createObjectMessage();
// 或者 message = session.createXXXMessage();
return message;
}
});
send(..)
方法除了可以接受MessageCreator作为参数,还可以接受String型或者Destination类型参数,用于标志消息发送的Destination。如果没有为send(..)
方法指定消息发送的Destination相关信息,消息默认将被发送到为JmsTemplate指定的默认Destination。
c)使用MessageConverter的模板方法。以convertAndsend(..)
命名的多个方法都是使用MessageConverter的模板方法。MessageConverter在这里不是这些模板方法的方法参数,它是JmsTemplate使用的一个Helper类,负责具体消息类型与对象类型之间的相互转换。下面是这个接口的定义:
public interface MessageConverter {
Message toMessage(Object object, Session session) throws JMSException, MessageConversionException;
Object fromMessage(Message message) throws JMSException, MessageConversionException;
}
toMessage()
方法负责将相应的对象转换为JMS的Message以便发送,而fromMessage()
则负责将JMS的Message转型为应用程序所使用的对象类型,所以,convertAndSend(..)
这类模板方法可以接受任何类型的对象(String类型、Map类型以及Object类型等)。MessageConverter将负责把这些对象转换为合适的Message类型发送,比如:
jmsTemplate.convertAndSend("textmessage");
// 或者
jmsTemplate.convertAndSend(new AnyBean(..));
这显然要比使用MessageCreator自己创建JMS的Message实例简洁得多。
JmsTemplate默认使用的MessageConverter实现为org.springframework.jms.support.converter.SimpleMessageConverter,它可以满足String类型、byte数组、Map以及可序列化对象(Serializable)与JMS对应Message之间的相互转换。如果我们需要支持更多类型,可以实现自己的MessageConverter,然后通过“messageConverter"属性设置给JmsTemplate并启用它。
convertAndSend(..)
系列模板方法还有一个特性就是,我们可以同时指定一个MessagePostProcessor。通过它,我们可以在消息发送之前对消息做进一步的后处理,如下代码给出了这种情况下的代码示例:
Object yourMessage = ...;
jmsTemplate.convertAndSend(yourMessage, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("STOCK-MARKET", "NASDAQ");
return message;
}
});
我们在消息发送之前为消息设置了相应的Property,以便接收方可以根据该Property使用相应的MessageSelector选取/过滤要接收的消息。每组中的各种重载方法的详细情况,可以参考JmsTemplate的Javadoc,这里就不做一一罗列了。
(2)JmsTemplate的同步消息接收模板方法。用于同步消息接收的模板方法。其内部同样有不同的派别,好在不管派别如何,都是为了简化我们的开发过程。下面是各个派别的情况。
a)直接同步接收的模板方法。以receive(..)
命名的模板方法是最直接也是最简单的同步消息接收方法。如果要指定参数的话,receive(..)
方法只接受String类型或者Destination类型的参数作为消息接收的Destination。如果不指定任何参数,那么receive()
方法将直接从JmsTemplate的默认Destination中接收消息。
b)可以指定MessageSelector表达式的模板方法。JMS提供的MessageSelector机能,可以让消息接收方指定接收符合某种条件的消息。JmsTemplate中以receiveSelected(..
)命名的模板方法,允许我们传入selector表达式来启用MessageSelector机能,比如:
Message receivedMessage = jmsTemplate.receiveSelected("STOCK-MARKET='NASDAQ'");
// 处理接收到的信息....
有关MessageSelector可以使用的表达式语法,可以参考JMS规范文档(JMS Specification),这里不再赘述。
c)使用MessageConverter的模板方法。与消息发送模板方法相对的,用于同步消息接收的也有一组使用MessageConverter的模板方法。这些模板方法以receiveAndConvert(..)
命名。如果说receive()
和receiveSelected(..)
方法所返回的都是原始的JMSMessage类型的话,现在的receiveAndConvert(..)
返回的则是由Message转型后的对象类型,如下所示:
String textMessage = (String)jmsTemplate.receiveAndConvert();
Map mapMessage = (Map)jmsTemplate().receiveAndConvert();
Object objectMessage = jmsTemplate().receiveAndConvert();
...
至于MessageConverter,我想就不用多说了吧?
d)结合selector表达式和MessageConverter的模板方法。如果我们既想使用MessageConverter,又想同时使用MessageSelector机能,那么,以receiveSelectedAndConvert(..)
命名的这组模板方法就是我们想要的了。在使用MessageConverter的模板方法实现逻辑上添加selector表达式的指定,就是receiveSelectedAndConvert(..)
的奥秘,一切都是为了提供更多更便捷的服务。
JmsTemplate提供的所有同步消息接收模板方法都没有提供相应参数,用于指定消息接收的超时时间(Timeout)。默认情况下,它们都将无限期地等待,这将阻塞当前调用线程。在实际使用中,最好是根据场景,为消息的接收指定一个合适的超时时间,可以通过JmsTemplate的receiveTimeout
属性设定。
(3)检查消息队列情况的模板方法。JMS规范提供的QueueBrowser允许我们查看某一消息队列的情况。不过,“只能看不能碰”。JmsTemplate同样为这种情况下的API使用进行了封装,封装后的模板方法可以简单划分为如下两组。
a)不使用MessageSelector的模板方法。JmsTemplate中以browse命名的模板方法可以接受BrowserCallback作为回调接口,该接口定义如下:
public interface BrowserCallback {
Object doInJms(Session session, QueueBrowser browser) throws JMSException;
}
我们可以提供自定义的BrowserCallback实现,在其中使用模板方法公开给我们的QueueBrowser进行消息队列的检查。至于QueueBrowser如何创建和关闭,那就由browse(..)
模板方法操心就行了。
b)使用MessageSelector的模板方法。重载后的browseSelected(..)
这组模板方法较之browse(..)
那组模板方法多了一个功能,那就是可以同时指定一个selector表达式。至于差别,我想就不需要多提了吧?
无论是用于消息发送还是用于同步消息接收的模板方法,它们当中都会有一个不需要指定Destination的重载方法。这得益于JmsTemplate的defaultDestination属性所指定的默认Destination。如果系统中有多个消息被发送到同一个Destination,或者从同一个Destination接收,那么将该Destination设置为所使用的JmsTemplate的defaultDestination将是合适的做法。甚至,即使我们在发送消息或者接收消息的时候指定了要使用的Destination,但如果指定的Destination找不到的话,JmsTemplate也会使用defaultDestination指定的默认Destination作为“后备力量”。所以,通常情况下,通过defaultDestination为JmsTemplat设置一个默认的Destination还是有必要的。
为JmsTemplate指定默认的Destination并不意味着我们从此以后就只能向这个默认的Destination发送消息或者从它那里同步接收消息,我们依然可以在调用消息发送或者接收方法的时候指定其他的Destination。这时,指定Destination的方式可以有两种,一种是指定直接的Destination类型实例,另一种是以String形式给出。对于后者来说,JmsTemplate需要DestinationResolver的帮忙,把String类型的Destination名称转换为具体的Destination实例。DestinationResolver的定义如下所示:
public interface DestinationResolver {
Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) throws JMSException;
}
DestinationResolver的实现类只需要根据destinationName去获取对应的Destination实例即可。在Spring框架内部,DestinationResolver主要有如下三个可用的实现类。
-
org.springframework.jma.gupport.destination.DynamicDestinationResolver。这是JmsTemplate默认使用的DestinationResolver实现,它将根据destinationName创建动态的Destination实例。
-
org.springframework.jms.support.destination.JndiDestinationResolver。对于事先配置到JNDI的Destinations,我们需要使用JndiDestinationResolver。这个时候,就可以通过设置JmsTemplate的destinationResolver属性,以某一JndiDestinationResolver替换默认的DynamicDestinationResolver。
-
org.springframework.jms.support.destination.BeanFactoryDestinationResolver。如果可以将Destination以bean定义的形式添加的Spring的IoC容器的话,BeanFactoryDestinationResolver将可以保证根据destinationName去容器中获取相同beanName的Destination实例。
DynamicDestinationResolver和JndiDestinationResolver是比较常用的DestinationResolver实现类,这可以因JMS Provider的不同以及应用的场景需求而发生变化。不过,我想,现有的DestinationResolver实现类已经足够用了。如果还有什么特殊需求的话,实现DestinationResolver也不难,只有一个接口方法要实现而已。
JmsTemplate最引入注目的,当然就是它提供的各式各样服务周到的模板方法。不过,这些模板方法指定的参数都比较有限,无法做更细粒度的功能定制。这不是说JmsTemplate设计上有问题,实际上,那些在消息处理期间比较通用的功能或者说行为,都是通过JmsTemplate定义的相应属性来进行控制的,如receiveTimeout、defaultDestination等属性,我们已经认识过了。表29-1中是其他几个可以对JmsTemplate行为进行定制的属性。
JmsTemplate看起来可能比较庞大,但实际上,其实现原理并不复杂,尤其是,Spring框架内使用模板方法模式加上相应回调接口的API封装方式,我们已经再熟悉不过了。所以,如果你要了解有关JmsTemplate的更多秘密,不妨自己再发掘一下。
注意:JmsTemplate主要面向的是JMS 1.1规范。如果你不得不依然使用JMS 1.02规范进行开发,那么,使用org.springframework.jms.core.JmsTemplate102即可。Spring为JMS提供的各种装备,对应JMS 1.02规范的,都较之JMS 1.1的相关类名后缀102,比如,除了JmsTemplate102,还有SimpleMessageConverter102等。