Spring核心技术与最佳实践- 9.3 集成Java消息服务
9.3 集成Java消息服务
Java消息服务,即JMS(Java Message Service),是JavaEE标准中为企业应用程序提供消息传递服务的API标准,JMS使得异步发送和接受事件通知的应用程序变得容易设计和实现。
在Spring框架中,同样为JMS提供了非常好的封装,使JMS使用起来更加方便。
9.3.1 Java消息服务概述
大多数时候,应用程序内容的各个对象间都是以同步的方式进行方法调用的。当一个方法被调用的时,当前线程的控制权就转移到了这个方法中,知道方法执行完毕,线程的控制权才返回给调用者。
如果要执行一个非常耗时的方法,会使当前线程的执行变得缓慢,从而引起用户长时间等待。为了避免同步调用来执行一个耗时的任务,可以在一个新的线程中执行这个耗时的任务,原来的线程则不必等待,就可以立刻继续执行下去。
相当于多线程模型,使用消息机制能更好的实现异步调用,而不必处理复杂的线程管理问题。JMS为Java应用程序提供了完整的一部消息服务机制。
读者可能会问,Spring的ApplicationContext容器本身也支持事件发布,类似消息传递,能否替代JMS呢?答案是否定的。因为Spring的ApplicationContext容器不支持异步事件处理,同步会使应用程序性能大打折扣。此外,JMS并不要求通信双方一定要在同一台机器上,可以将发送消息和处理消息的应用程序部署在不同的机器上。JMS还提供了持久化服务,这是Spring的ApplicationContext容器无法做到的。
直接使用JMS API比较复杂,而Spring对JMS做了非常方便的封装,读者需要注意,在Spring1.x版本中,仅对发送JMS消息进行了封装,而接受JMS消息的封装在Spring2.0版本中才被添加进来。
9.3.2 JMS编程模型
JMS支持两种消息发送模式:点对点模式和发布-订阅模式。
在点对点模式下,消息的发送是一对一的,消息在两个实体间进行单项传递。点对点模式使用Queue来传递消息:
在发布-订阅模式下,消息的发送是一对多的。消息的发送者只需要发送消息,而凡是订阅了该消息的实体都可以接收到消息。
最初的JMS1.0 规范对这两种模式定义的接口是不同的,这意味着开发人员在编写JMS代码时,必须清楚地知道使用那种模式来发送消息。辛运的是,在JMS1.1 标准中,这两种模式的接口完全统一了,开发人员不必关心究竟使用哪种模式来发送消息,而使用何种消息模式被定义在了服务器的相关配置中,也就是说,使用点对点模式还是发布订阅模式只需要修改服务器的设置即可。JMS1.1 中统一的API使得JMS代码更简单,更易于维护,并且代码复用性也更好。
以下表中显示了点对点模型和发布-订阅模型的主要接口,以及在JMS1.1中同一的接口。
点对点模型 | 发布-订阅模型 | JMS1.1 的同一接口 |
QueueConnectionFactory | TopicConnectionFactory | ConnectionFactory |
QueueConection | TopicConnection | Connection |
Queue | Topic | Destination |
QueueSession | TopicSession | Session |
QueueSender | TopicPublisher | MessageProducer |
QueueReceiver | TopicSubscnber | MessageConsumer |
使用JMS1.1 就无需关心两种模式的不同的接口,而只需使用同一的接口。
9.3.3 使用JMSAPI
为了在命令程序中使用JMS测试,需要一个JMS服务器的实现。SUN提供了一个免费的mom4j的JMS实现,它完整地支持JMS1.1标准,可以从http://mom4j.sourceforge.net下载,不过,在使用mom4j之前,还需要手动下载JMS接口库和mom4j依赖的几个第三方库,然后用Ant编译源代码。
在Eclipse中建立如下的JMS工程。
Mom4jUtil负责初始化并启动JMS服务器,同时绑定ConnectionFactory和Queue到JNDI上,然后,在Sender类中,就可以发送JMS消息了。
public class Sender extends Thread {
public void run() { try { //初始化上下文,获取一个JNDI连接 Context ctx = new InitialContext(); //查找一个JMS连接工厂 QueueConnectionFactory"); //创建一个JMS目的地队列(Queue) "jms/queue"); for(;;) { Connection connection = null; try { //通过JMS连接工厂创建连接 connection = factory.createConnection(); //创建JMS会话 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //创建消息生产者 MessageProducer "Hello, it is " +new Date(); System.out.println(" Send: " //创建一个文本消息 Message message = session.createTextMessage(text); //发送消息 producer.close(); session.close(); }finally { if(connection!=null){ connection.close(); } } Thread.sleep(1000 + (long)(Math.random() * 5000)); } }catch(Exception e) { e.printStackTrace(); } } } |
Sender类以随机的间隔发送TextMessage消息。为了能够发送JMS消息,需要从JNDI获得ConnectionFactory和Destination对象。ConnectionFactory将用于创建到JMS服务器的链接,Destination则指定了消息的目的地,即消息将发向何处。
接受JMS消息和发送类似,也需要通过ConnectionFactory创建到JMS服务器的链接,Destination指定从何处接受消息。此外,接受者必须实现MessageListener接口以便服务器回调。Receiver类将接收到的TextMessage打印出来。
public class Receiver extends Threadimplements MessageListener {
public void run() { try { Context ctx = new InitialContext(); "QueueConnectionFactory"); "jms/queue"); Connection connection = null;
try { connection = factory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //创建消息接受者 MessageConsumer consumer = session.createConsumer(destination); consumer.setMessageListener(this); //启动JMS连接,允许传从消息 Thread.sleep(20000); }finally { if(connection!=null){ connection.close(); } } }catch(Exception e) { e.printStackTrace(); } }
/** * 接受来自MessageProducer的消息 */ public void onMessage(Message message) { if(messageinstanceof TextMessage) { TextMessage text = (TextMessage) message; try { System.out.println("Receive: " } catch(JMSException e) { e.printStackTrace(); } } } } |
在main方法中启动JMS服务器,然后分别启动发送和接受线程。
public class Main {
public static void main(String[] args)throws Exception { Mom4jUtil.startJmsServer(); //启动 jms服务
new Sender().start();//发送消息 new Receiver().start();//接受消息
Thread.sleep(30000); //主程序等待30秒退出 System.exit(0); } }
|
可以看到如下输出:
Send: Hello, it is Sun Apr 22 16:10:03 CST 2012 Receive: Hello, it is Sun Apr 22 16:10:03 CST 2012 Send: Hello, it is Sun Apr 22 16:10:07 CST 2012 Receive: Hello, it is Sun Apr 22 16:10:07 CST 2012 Send: Hello, it is Sun Apr 22 16:10:12 CST 2012 Receive: Hello, it is Sun Apr 22 16:10:12 CST 2012 |
从上面的例子可以看到,直接使用JMS API发送和接受消息都是比较复杂的,并且没有很好的封装成组建。幸运的是,Spring对JMS提供了非常简单的封装。通过使用Spring提供的JmsTemplate模板,操作JMS就易如反掌了。下面,我们看看在Spring中配置JMS组组件的方法。
9.3.4 Spring如何封装JMS
为了演示在Spring环境下操作JMS,我们建立了一个JMS_Spring工程。
Spring提供了JmsTemplate模板来简化JMS操作。利用JmsTemplate,Sender只需被注入JmsTemplate,发送的消息通过MessageCreator以回调的方式创建,这个发送过程非常简单。
public class Sender {
private JmsTemplatejmsTemplate;
public void setJmsTemplate(JmsTemplate jmsTemplate) { this.jmsTemplate }
public void send(final String text) { System.out.println(" Send: " jmsTemplate.send(new MessageCreator() { public Message createMessage(Session session)throws JMSException { return session.createTextMessage(text); } }); } } |
其所需要的JMS资源全部定义在XML配置文件中。
<beanid="jmsConnectionFactory"class="org.springframework.jndi.JndiObjectFactoryBean"> <propertyname="jndiName"value="QueueConnectionFactory"/> </bean> <beanid="jmsQueue"class="org.springframework.jndi.JndiObjectFactoryBean"> <propertyname="jndiName"value="jms/queue"/> </bean> <beanid="jmsTemplate"class="org.springframework.jms.core.JmsTemplate"> <propertyname="connectionFactory"ref="jmsConnectionFactory"/> <propertyname="defaultDestination"ref="jmsQueue"/> </bean> <beanid="sender"class="com.zsw.spring.jms.Sender"> <propertyname="jmsTemplate"ref="jmsTemplate"/> </bean> |
对于接受消息的Receiver类,则仅实现了MessageListener接口,甚至没有注入任何JMS资源。
public class Receiver implements MessageListener {
@Override public void onMessage(Message message) { if(messageinstanceof TextMessage) { TextMessage text = (TextMessage) message; try { System.out.println("Receive: " }catch(JMSException e) { e.printStackTrace(); } } } } |
Spring提供了MessageListener容器来包裹MessageListener。通常情况下,使用DefaultMessageListenerContainer就足够了,只需要在XML配置文件中定义。
<beanid="receiver"class="com.zsw.spring.jms.Receiver"/> <beanid="listenerContainer"class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <propertyname="connectionFactory"ref="jmsConnectionFactory"/> <propertyname="destination"ref="jmsQueue"/> <propertyname="messageListener"ref="receiver"/> </bean> |
编写测试程序如下:
public class Main { public static void main(String[] args)throws Exception { Mom4jUtil.startJmsServer(); ApplicationContext context = new ClassPathXmlApplicationContext("config.xml"); sender = (Sender) context.getBean("sender"); for(int i=0; i<10; i++) { sender.send("Hello, it is " +new Date()); Thread.sleep(1000); } System.exit(0); } } |
运行程序,输出结构与直接使用JMS的工程类似,但是发送和接受JMS消息的编码被大大简化了。需要注意的一点是,必须保证MessageListener的实现类时线程安全的,因为onMessage()方法可能被多个线程同时调用。
在EJB2.0 中使用消息驱动Bean(Message – Driven Bean )也能够大大简化JMS的编程,但是消息驱动Bean也是EJB的一种,必须运行在EJB容器中。相别之下,Spring提供了以POJO形式的包裹Bean,能非常方便的处理JMS消息。
对于仅支持JMS1.02 版本规范的服务器,Spring提供了一个兼容的JmsTemplate102(表示JMS1.02版本)模板类,JmsTemplate102需要知道究竟使用Queue还是Topic 方式,通过设置属性pubSubDomain=true指定了Topic方式,如果不设定,则默认为Queue方式。
9.3.5 自动转换消息
大多时候,除了简单的TextMessage外,需要发送消息都应当被封装在Java类中,例如,一个电子邮件消息应该通过一个MailMessage对象来表示。通常,ObjectMessage可以自动实现Java对象的序列化,不过,很多时候仍需要将消息以BytesMessage等其他形式发送,为此,Spring提供了一个MessageConverter接口来方便地实现Java类和JMS消息的转化。
public interface MessageConverter{ Object fromMessage(Message message); Message toMessage(Object object,Session session); } |
同时,Spring内置的SimpleMessageConverter已经能够满足大多数消息的转化,它支持String和TextMessage、byte[]和ByteMessage,以及Map和MapMessage之间的转化。要在发送消息时自动将Java对象转化为消息,可以调用JmsTemplate的convertAndSend()。JmsTemplate默认使用SimpleMessageConverter作为默认的MessageConverter,要编写一个自定义的MessageConverter也是极其容易的,这里不再给出示例代码。
9.3.6 同步接受消息
虽说绝多数,JMS消息都是异步传输的,但是某些时候也确实需要同步接受一条消息,JmsTemplate提供了多个重载的receive()方法,可以同步地接受消息。需要注意的是,使用receive()方法要格外小心,在接受到消息之前,当前线程将被阻塞。
9.3.7 使用JMS发送E-mail
在Web应用程序中,常常需要给用户发送邮件通知,例如,注册成功的欢迎邮件、订单确认邮件,由于发送邮件是非常耗时的任务,对于需要实时发送的邮件,以同步的方式在一个HTTP请求中完成将影响用户浏览,在新的线程中发送则需要自己管理线程池。这时,利用JMS提供的一部编程模型,借助Spring框架可以在一个简单的POJO中非常方便地处理邮件发送的任务。
9.3.8 在服务器中发送消息
上面的例子中,我们使用的是独立的第三方JMS实现库,对于调试JMS程序来说是比较方便的。但是,一旦产品部署在服务器上,就应当使用服务器提供的JMS服务。常见的JavaEE服务器提供了JMS的实现,少数高端JavaEE服务器还支持JMS集群。只需要对服务器做一定的配置,就可以直接在应用程序中使用JMS服务了。
在这里,我们以Resin服务器为例,演示如何在服务器环境中使用JMS。首先,在Eclipse中新建JMS_Servlet工程。(这这里要说下,关于Eclipse与Resin集成,可以参考这篇文章:javascript:void(0))
在Resin3.1服务器中配置JMS相当简便,可以参考Resin根目录下的配置文件/conf/resin.conf.我们在工程根目录下创建一个新的resin.conf,然后编写一个简单的配置,只需加入下面的内容即可:
<!-- Resin 3.1 configuration file --> <resin xmlns="http://caucho.com/ns/resin" xmlns:resin="http://caucho.com/ns/resin/core">
<log name="" level="info" path="stdout:"/>
<cluster id=""> <!-- web应用发布目录 --> <root-directory>D:\Development\spring2.5.6\JMS_Servlet\WebContent</root-directory> <server id=""> <http port="8080"/> </server> <resource jndi-name="jms/factory" type="com.caucho.jms.ConnectionFactoryImpl"/> <resource jndi-name="jms/queue" type="com.caucho.jms.memory.MemoryQueue"/> <!-- resin安装目录下,为app-default.xml的绝对路径。此处不能使用相对路径。 --> <resin:import path="D:/Java/OpenSourceJavaSourceCode/resin-3.x/resin-3.1.12/conf/app-default.xml"/>
<host id="" root-directory="."> <!-- web应用发布目录 --> <web-app id="/" root-directory="D:\Development\spring2.5.6\JMS_Servlet\WebContent"/> </host> </cluster> </resin>
|
然后,编写SendMessageController以便结接受用户输入,并将用户输入的内容作为TextMessage发送出去,为了简化程序,这里没有使用MVC架构,而是直接输出到response对象。
public class SendMessageController implements Controller {
private JmsTemplatejmsTemplate;
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)throws Exception { final String text = request.getParameter("text"); "text/html;charset=UTF-8"); "UTF-8"); PrintWriter writer = response.getWriter(); if(text!=null && !text.equals("")) { jmsTemplate.send(new MessageCreator() { public Message createMessage(Session session)throws JMSException { return session.createTextMessage(text); } }); "发送成功!<a href='index.html'>返回</a>"); writer.flush(); }else { "发送失败!<a href='index.html'>返回</a>"); writer.flush(); } return null; }
public void setJmsTemplate(JmsTemplate jmsTemplate) { this.jmsTemplate } } |
编写简单的index.html,接受用户输入并将表单发送给SendMessageController处理。
<html> <head> <metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/> <title>发送JMS</title></head> <body> <formname="form1"method="post"action="sendMessage.jms"> 输入消息: <inputname="text"type="text"id="text"maxlength="255"> <inputtype="submit"name="Submit"value="发送"> </form> </body> </html> |
Receiver类和前面说的JMS_Spring工程中的完全相同,这里不再重复。最后一步便是在Xml配置文件中将他们全部装配出来。注意:由于是WEB应用程序,XML配置文件被放在/web/WEB-INF目录下,命名为dispatcher-servlet.xml
<beanid="jmsConnectionFactory"class="org.springframework.jndi.JndiObjectFactoryBean"> <propertyname="jndiName"value="java:comp/env/jms/factory"/> </bean>
<beanid="jmsQueue"class="org.springframework.jndi.JndiObjectFactoryBean"> <propertyname="jndiName"value="java:comp/env/jms/queue"/> </bean>
<beanid="jmsTemplate"class="org.springframework.jms.core.JmsTemplate"> <propertyname="connectionFactory"ref="jmsConnectionFactory"/> <propertyname="defaultDestination"ref="jmsQueue"/> </bean>
<beanid="receiver"class="com.zsw.springmvc.jms.Receiver"/>
<beanid="listenerContainer"class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <propertyname="connectionFactory"ref="jmsConnectionFactory"/> <propertyname="destination"ref="jmsQueue"/> <propertyname="messageListener"ref="receiver"/> </bean>
<beanname="/sendMessage.jms"class="com.zsw.springmvc.jms.SendMessageController"> <propertyname="jmsTemplate"ref="jmsTemplate"/> </bean> |
最后,在/web/WEB-INF/目录下配置好web.xml
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>*.jms</url-pattern> </servlet-mapping> |
运行start_resin.bat,启动Resin服务器,然后打开浏览器,输入地址http://localhost:8080/,如图9-11所示。
单击“发送”按钮,就将表单发送给SendMessageController处理,若发送成功,将显示发送成功的页面。
在Resin的控制台可以看到Receiver对象已经打印出了接受的消息:
[2012/04/23 03:15:07.250] WebApp[http://localhost:8080] Initializing Spring FrameworkServlet 'dispatcher' Receive: Hello World! |
如果这是一个在线聊天的应用,Receiver就可以将消息转发给其他用户。
对于其他JavaEE服务器,可以参考其他的说明文档,只要正确设置了JNDI名称,应用程序的代码不用更改就可以直接部署在相应的服务器上。