异步处理通信是面向服务架构(SOA)的重要部分,因为企业中的许多系统通信,尤其是跟外部系统通信本来就是异步的。Java消息服务(JMS)就是用来编写异步消息J2EE应用的API。使用JMS API的传统消息实现涉及到象这样的一些步骤:查找对列连接工厂、队列资源以及在实际发送和接受消息前,创建JMS会话(JMS session)。
Spring framework简化了用JEE组件(包括JMS)开发JMS应用的工作。它提供了一个模板机制来隐藏典型的JMS实现细节,所以开发者可以专注于消息处理任务而不用担心怎样创建、访问和释放JMS资源。
本文用一个运行在JBoss MQ server上的简单Web应用概述了Spring JMS API和怎样使用它异步处理(发送和接受)消息。我将对比JMS实现的传统方法和Spring JMS实现方法,以显示使用Spring JMS来处理消息是多么的简单和灵活。
异步消息和SOA
现实世界中,大多数Web请求是同步处理的。例如,当用户登陆一个站点,他或她输入用户名和口令以及服务器识别登陆凭证。如果身份验证成功,程序让用户进入站点。这里,登陆请求从客户端被接受后,立即被处理。信用卡授权也是一个同步处理的例子;仅当服务器核实了发送进来的信用卡号是有效并且该客户的账号有足够的信用额度后,才允许客户继续进行下一步动作。让我们来考察一下订单处理系统中的支付结算步骤。一旦系统核实了那个用户的信用卡信息是正确的,而且帐户上有足够的资金,那么不需要等到支付细节和转帐最终完成。支付结算用异步方式处理,如此客户便可以继续进行结帐处理。
与典型的同步请求相比,异步处理用于需要长时间来处理的请求。异步处理的另外一个例子是住房贷款处理应用中,处理提交到AUS(Automated Underwriting System)的贷款请求。贷款人提交贷款申请后,抵押公司发送请求到AUS取得信用历史信息。因为该请求要取得综合详细的信用报告如贷款人当前和过去的信用帐户,最近的支付以及其它金融详细信息,所以从请求到获得响应常常需要很长时间。对客户端程序来说开一个到服务器的连接并且长时间等待响应是没有意义的。于是就有了异步通信;也就是,一旦请求被提交,它就被放入队列里面并且客户断开服务器连接。然后,AUS服务从特定队列摘取请求,处理它,把结果消息放入另外一个消息队列。最后客户程序从消息队列摘取响应结果继续处理信用历史结果信息。
JMS
如果用过JMS的话,会发现它类似写JDBC或JCA代码。它有创建或检索JMS资源的样板代码,每当你需要编写一个新类来发送或接受消息时,都得重复编写那个样本代码。下面列出了传统JMS实现涉及的步骤:
1、创建JNDI初始上下文context;
2、从JNDI上下文获得队列连接工厂;
3、从队列连接工厂取得队列Queue;
4、创建一个Session对象;
5、创建一个发送或接受对象;
6、利用第5部创建的发送或接受对象发送或接受消息;
7、处理完消息后,关闭所有JMS资源。
如你所见,只有第6步是处理消息的步骤。其他步骤都只是管理JMS资源,与实际业务需求无关,但开发者不得不编写和维护那些附加步骤代码。
Spring JMS
Spring框架提供一个模板机制来隐藏Java API细节。JEE开发者可用JDBCTemplate和JNDITemplate类来分别访问后端数据库和JEE资源(数据源,连接池)。JMS没有异常。Spring提供了JMSTemplate类,所以开发者不必为JMS实现编写样本代码。当开发JMS应用时,Spring提供了一下一些优势:
1、提供了一个JMS的抽象API,简化了JMS的使用。如:访问目的地(队列或主体)和出版消息到特定目的地。
2、JEE开发者不必关心JMS不同版本之间的差异(如JMS 1.0.2 同 JMS 1.1);
3、开发者不必特定地处理JMS异常,因为Spring为JMS代码抛出的任何JMS异常提供了一个unchecked异常。
一旦你在JMS应用中开始使用Spring,你将会欣赏到异步消息处理的简易性。Spring JMS框架提供了各种java类使JMS开发变得简单。
表1:Spring JMS类
随后的部分,我将详细解释表1中的类(如JmsTemplate, DestinationResolver,和 MessageConverter)。
#p#
JMSTemplate
JmsTemplate提供了几个helper方法来执行基本操作。开始使用JmsTemplate前,有必要知道JMS提供者支持哪种JMS规范。JBoss AS 4.0.2 和 WebLogic 8.1服务器支持JMS1.0.2规范。WebLogic 服务器 9.0包含JMS1.1支持。JMS1.1统一了PTP和Pub/Sub编程接口。有了这个改变,开发者可以创建一个事务会话,然后在同一个JMS事务中,从Queue(PTP)接受消息和发送一个消息到Topic(Pub/Sub)。JMS1.1向后兼容JMS1.0,因此基于JMS1.0编写的代码仍然能跟JMS1.1工作。
JmsTemplate提供各种方法来接收和发送消息。表2是方法列表。
表2:JMS模板方法
使用JNDI上下文存储和检索目的地。当配置Spring应用上下文时,我们用JndiObjectFactoryBean获得JMS目的地引用。DestinationResolver用来解析目的地名称到一个JMS目的地,当应用有许多目的地时,那是很有帮助的。DynamicDestinationResolver(缺省DestinationResolver实现)用于解析动态目的地。
MessageConverter接口定义了java对象和JMS消息之间转换的契约。使用转换器,应用代码可以专注于业务对象,不用操心它是如何代表JMS消息的。SimpleMessageConverter(和SimpleMessageConverter102)是缺省MessageConverter实现。它们用于将String、字节数组((byte[])、Map、Serializable对象分别转换成JMS TextMessage、JMS BytesMessage,JMS MapMessage,JMS ObjectMessage。你可以编写MessageConverter接口的定制实现并结合XML绑定框架如JAXB, Castor, Commons Digester, XMLBeans, 或 XStream来转换XML文档到TextMessage。
样本应用
我将用一个样本贷款应用处理系统(叫LoanProc)来说明怎样在JMS应用中使用Spring。作为贷款处理的一部分,LoanProc发送贷款详细资料(loan ID, borrower name, borrower's SSN, loan expiration date, and loan amount)从AUS系统请求信贷历史。为让例子简单一点,我们将基于两个参数:信用评分和贷款数量来获得信贷历史详细资料。让我们假定处理信用检查请求的业务规则如下:
1、如果贷款数量等于或小于$500,000,那么贷款人必须至少有一个“good”信用(例如,贷款人的信用评分在680到699之间);
2、如果贷款数量超过$500,000,那么贷款人必须至少要有一个“very good”信用,这意味他/她的信用评分超过700。
贷款应用Use Case
贷款请求处理Use Case由下列步骤组成:
1、用户在贷款申请web页面输入贷款详细资料并提交贷款申请;
2、然后程序发送贷款详细资料到AUS系统取得信用历史详细资料。用发送请求到名叫CreditRequestSendQueue的消息队列来完成。
3、AUS系统从队列摘取贷款详细资料并用贷款参数来从数据库检索信用历史信息;
4、然后AUS系统用找到的贷款人信用历史信息创建一个新的消息并发送到名叫CreditRequestReceiveQueue的消息队列;
5、最后LoanProc从接收消息队列摘取响应消息并处理贷款申请,决定申请是被核准还是拒绝。
应用中,在同样的JBoss MQ server中配置了两个消息队列。Use Case用序列图1表示如下:
图1:贷款处理应用的序列图
技术
表3列出了例子应用中用到的一些技术和开源框架
表3:JMS应用中用到的框架
#p#
使用Hermes 的JMS资源设置
为异步处理消息,首先,我们需要消息队列来发送和接收消息。我们在JBoss中使用xml配置文件创建消息队列并且用JMS控制台来浏览队列详细资料。清单1显示了XML配置文件JMS配置片断。(这个片断可以被添加到%JBOSS_HOME%\server\all\deploy-hasingleton\jms目录下的jbossmq-destinations-service.xml文件中。)
清单1:JBoss MQ服务器中JMS队列配置
<!-- Credit Request Send Queue -->
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=CreditRequestSendQueue">
<depends optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager
</depends></mbean>
<!-- Credit Request Receive Queue -->
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=CreditRequestReceiveQueue">
<depends optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager </depends>
</mbean>
现在,让我们看看怎么使用一个叫做Hermes的JMS工具浏览消息队列。Hermes是一个Java Swing应用,它能创建、管理和监控JMS提供者中的JMS destination。这样的JMS提供者有JBossMQ, WebSphereMQ, ActiveMQ 和 Arjuna。从website下载Hermes并解压zip文件到一个本地目录(如c:\dev\tools\hermes)。一旦安装,双击hermes.bat (在bin目录)启动程序。
为在Hermes中配置JBossMQ服务器,参考Hermes站点上的demo。它一步一步形象说明了JBoss MQ的配置。
当配置一个新的JNDI初始上下文时,输入下面的配置信息。
providerURL = jnp://localhost:1099
initialContextFactory = org.jnp.interfaces.NamingContextFactory
urlPkgPrefixes = org.jnp.interfaces:org.jboss.naming
securityCredentials = admin
securityPrincipal = admin
当创建一个新destinations时,输入queue/CreditRequestSendQueue和queue/CreditRequestReceiveQueue。图2显示了JMS控制台主屏幕,它显示了为样本JMS应用创建的新消息。
图2:Hermes中所有destinations的截屏
图3是消息发送者发送一些消息到CreditRequestSendQueue后,Hermes JMS控制台显示的消息队列详细资料。你能看到这里队列中有5个消息并且控制台显示了消息的详细信息如message ID、message destination、time stamp、和实际的消息。
图3:Hermes中队列详细资料截屏
使用Spring JMS,异步消息变得简单
用在样本应用中的这些消息队列名和其它JMS以及JNDI参数如下表4所示。
表4:Spring JMS配置参数
#p#
Spring配置
已经有了运行样本应用所需的JMS destinations,现在该是进入用XML Spring配置文件(叫做spring-jms.xml)装配JMS组件的细节的时候了。用IOC设计模式中的setter依赖注入原理装入这些组件。让我们仔细看看组件,为每一个JMS组件显示了一个XML配置片断。
JNDI上下文是获取JMS资源的入口,所以我们首先配置一个JNDI模板。清单2显示了一名为jndiTemplate的Spring bean,它具有取得JNDI初始上下文必须的常用参数。
清单2:JNDI上下文模板
<bean id="jndiTemplate" class="org.springframework.jndi.JndiTemplate">
<property name="environment">
<props>
<prop key="java.naming.factory.initial">
org.jnp.interfaces.NamingContextFactory
</prop>
<prop key="java.naming.provider.url">
localhost
</prop>
<prop key="java.naming.factory.url.pkgs">
org.jnp.interfaces:org.jboss.naming
</prop>
</props>
</property>
</bean>
下一步,我们配置队列连接工厂。清单3显示了队列连接工厂。
清单3:JMS对了连接工厂配置
<bean id="jmsQueueConnectionFactory"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiTemplate">
<ref bean="jndiTemplate"/>
</property>
<property name="jndiName">
<value>UIL2ConnectionFactory</value>
</property>
</bean>
我们定义了两个JMS destinations来发送和接收消息。清单4和清单5显示了这些细节。
清单4:发送队列配置
<bean id="sendDestination"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiTemplate">
<ref bean="jndiTemplate"/>
</property>
<property name="jndiName">
<value>queue/CreditRequestSendQueue</value>
</property>
</bean>
清单5:接收队列配置
<bean id="receiveDestination"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiTemplate">
<ref bean="jndiTemplate"/>
</property>
<property name="jndiName">
<value>queue/CreditReqeustReceiveQueue</value>
</property>
</bean>
然后,我们配置JmsTemplate组件。我们在样本应用中使用JmsTemplate102。使用defaultDestination属性来指定JMS destination。
清单6:JMS template配置
<bean id="jmsTemplate"
class="org.springframework.jms.core.JmsTemplate102">
<property name="connectionFactory">
<ref bean="jmsQueueConnectionFactory"/>
</property>
<property name="defaultDestination">
<ref bean="destination"/>
</property>
<property name="receiveTimeout">
<value>30000</value>
</property>
</bean>
最后,我们配置发送和接收者组件。清单7和清单8显示了Sender 和Receiver对象配置。
清单7:JMS Sender配置
Listing 7. JMS Sender configuration
<bean id="jmsSender" class="springexample.client.JMSSender">
<property name="jmsTemplate">
<ref bean="jmsTemplate"/>
</property>
</bean>
清单8:JMS Receiver配置
<bean id="jmsReceiver" class="springexample.client.JMSReceiver">
<property name="jmsTemplate">
<ref bean="jmsTemplate"/>
</property>
</bean>
#p#
测试和监控
我借了一个叫做LoanApplicationControllerTest的测试类来测试LoanProc应用。我们使用这个类来设置贷款参数和调用那个信用请求服务类。
让我们看看使用传统JMS而不用Spring JMS API的消息发送者实现。清单9显示了MessageSenderJMS这个类的sendMessage方法,这个类具备使用JMS API处理消息的所有必须步骤。
清单9:传统JMS实现
public void sendMessage() {
queueName = "queue/CreditRequestSendQueue";
System.out.println("Queue name is " + queueName);
/*
* Create JNDI Initial Context
*/
try {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial",
"org.jnp.interfaces.NamingContextFactory");
env.put("java.naming.provider.url","localhost");
env.put("java.naming.factory.url.pkgs",
"org.jnp.interfaces:org.jboss.naming");
jndiContext = new InitialContext(env);
} catch (NamingException e) {
System.out.println("Could not create JNDI API " +
"context: " + e.toString()); }
/*
* Get queue connection factory and queue objects from JNDI context.
*/
try {
queueConnectionFactory = (QueueConnectionFactory)
jndiContext.lookup("UIL2ConnectionFactory");
queue = (Queue) jndiContext.lookup(queueName);
} catch(NamingException e) {
System.out.println("JNDI API lookup failed: " +
e.toString());
}
/*
* Create connection, session, sender objects.
* Send the message.
* Cleanup JMS connction.
*/
try {
queueConnection =
queueConnectionFactory.createQueueConnection();
queueSession = queueConnection.createQueueSession(false,
Session.AUTO_ACKNOWLEDGE);
queueSender = queueSession.createSender(queue);
message = queueSession.createTextMessage();
message.setText("This is a sample JMS message.");
System.out.println("Sending message: " + message.getText());
queueSender.send(message);
} catch (JMSException e) {
System.out.println("Exception occurred: " + e.toString());
} finally {
if (queueConnection != null){
try {
queueConnection.close();
} catch (JMSException e) {}
}
}
}
现在,让我们看看用Spring实现的消息发送者。清单10显示了MessageSenderSpringJMS类中send方法代码。
清单10:用Spring API的JMS实现
public void send() {
try {
ClassPathXmlApplicationContext appContext =
new ClassPathXmlApplicationContext(new String[]
{ "spring-jms.xml"});
System.out.println("Classpath loaded");
JMSSender jmsSender = (JMSSender)appContext.getBean("jmsSender");
jmsSender.sendMesage();
System.out.println("Message sent using Spring JMS.");
} catch(Exception e) {
e.printStackTrace(); }
}
如你所见,所有与管理JMS资源相关的资源步骤都由Spring容器用配置文件处理。我们仅需要获取JMSSender引用并且调用它上面的sendMessage即可。
结论
本文中,我们看到Spring框架如何简化了使用JMS API的异步消息应用开发的工作。Spring移走了所有JMS消息处理必须的样板代码,如获取连接工厂,从Java代码创建队列和会话对象并在运行时用配置文件装配它们。由于这个强大的IOC原理,我们可以不必修改Java代码便可动态交换JMS资源对象。
因为异步消息是构成SOA框架整体所需的一部分,Spring非常适合放入SOA工具集。同样,JMS管理工具如Hermes使得创建,管理和控制JMS资源变得简单,尤其对系统管理员。