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来传递消息:

 

Spring最佳实践- 9.3 集成Java消息服务_服务器

 

 

在发布-订阅模式下,消息的发送是一对多的。消息的发送者只需要发送消息,而凡是订阅了该消息的实体都可以接收到消息。

 

Spring最佳实践- 9.3 集成Java消息服务_jms_02

 

 

 

 

 

最初的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名称,应用程序的代码不用更改就可以直接部署在相应的服务器上。