1. 背景
通常,在Web应用中,客户端发起HTTP请求,访问服务接口,服务端进行处理,并反馈消息,通信即告结束。
但是,当服务端处理过程较长时,情况就会变得复杂。例如,客户端会考虑添加超时机制,即超过指定时间还没有得到服务端反馈时,将会强制终止连接,并认为远程服务不可用。
消息队列(Message Queue)通常作为中间件,通过异步的机制,来解决上述这些实时性要求不高、且耗时较长的接口通信问题。
其优势是,大大缩短客户端的通信等待时间,且能够将请求稳妥地交付给服务端,保证请求的正常执行。
在用法上,通常会独立地创建一个消息队列服务,所有的生产者(客户端和Web应用)都可以往队列中发送消息,同时所有的消费者(Web应用)都可以侦听并读取消息,并进行后续处理。
在这种模式下,消息队列是一个公共的消息中转站,多个应用之间可以实现数据的互通。这种方式下,各个应用与消息队列服务之间是通过TCP协议建立长连接,而并非HTTP。
相对于公共的消息队列服务,在有些场合,也需要采用异步机制,但无需在多个应用之间互通。例如:导出数据记录并保存为excel、执行图片识别任务等。在这些场合下,是否可以考虑不部署单独的消息队列服务,而是嵌入到当前应用中。
2. 解决方案
官方参考:http://activemq.apache.org/how-do-i-embed-a-broker-inside-a-connection.html。
2.1 Spring MVC框架下的解决方案
(1)代理服务的定义
源代码:WEB-INF/mq-broker.xml,该文件在web.xml中被引用。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:amq="http://activemq.apache.org/schema/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.11.0.xsd">
<!-- Create an embedded ActiveMQ Broker -->
<!-- http://activemq.apache.org/how-do-i-embed-a-broker-inside-a-connection.html -->
<amq:broker brokerName="embedded" dataDirectory="./data" useJmx="false" persistent="true">
<amq:transportConnectors>
<amq:transportConnector name="openwire" uri="tcp://localhost:12345" />
</amq:transportConnectors>
<amq:plugins>
<amq:simpleAuthenticationPlugin>
<amq:users>
<amq:authenticationUser username="admin" password="***" groups="admins,publishers,consumers"/>
<amq:authenticationUser username="publisher" password="***" groups="publishers,consumers"/>
<amq:authenticationUser username="consumer" password="***" groups="consumers"/>
<amq:authenticationUser username="guest" password="***" groups="guests"/>
</amq:users>
</amq:simpleAuthenticationPlugin>
</amq:plugins>
</amq:broker>
<import resource="mq.xml"/>
</beans>
其中需要注意的是,
① 消息队列代理服务由框架来创建,无需代码。
② 在配置中指定不同的账户和群组,而不是采用系统默认的admin/admin。
③ 该消息队列支持持久化。
(2)客户端的定义
源代码:WEB-INF/mq.xml,该文件在mq-broker.xml中被引用,用来建立与消息队列服务的连接。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<bean id="connectionFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/myJMS/ConnectionFactory"></property>
</bean>
<!-- P2P模式 -->
<bean id="messageQueue" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/myJMS/MessageQueue"></property>
</bean>
<!-- ### 当代码结构改变时需修改此处类路径 ###-->
<bean id="messageDispatcher" class="com.foo.app1.integration.MessageDispatcher">
</bean>
<bean id="receiveMessageListener" class="com.foo.app1.integration.ReceiveMessageListener">
<property name="messageDispatcher" ref="messageDispatcher"></property>
</bean>
<bean id="listenerContainerQueue" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"></property>
<property name="destination" ref="messageQueue"></property>
<property name="messageListener" ref="receiveMessageListener"></property>
</bean>
<!-- P2P模式 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
<property name="defaultDestination" ref="messageQueue"></property>
</bean>
</beans>
其中,
① 使用JNDI定义的消息队列连接工厂(connectionFactory)和消息队列(messageQueue)。
② 定义了一个消息侦听器ReceiveMessageListener,
③ 定义的消息发送模板jmsTemplate。
①中的JNDI资源定义如下:
源代码:conf/server.xml,其中的连接信息和账户信息与创建消息队列代理服务(mq-brokder.xml)的一致。
<Context>
……
<Resource name="myJMS/ConnectionFactory"
auth="Container"
type="org.apache.activemq.ActiveMQConnectionFactory"
description="JMS Connection Factory"
factory="org.apache.activemq.jndi.JNDIReferenceFactory"
brokerURL="tcp://localhost:12345"
brokerName="embedded"
userName="consumer"
password="***"/>
<Resource name="myJMS/MessageQueue"
auth="Container"
type="org.apache.activemq.command.ActiveMQQueue"
description="Foo Queue"
factory="org.apache.activemq.jndi.JNDIReferenceFactory"
physicalName="Foo.Queue"/>
</Context>
(3)消息的发送
消息发送的思路是调用消息发送模板jmsTemplate的发送方法(“send”)即可。
源代码:MessageSenderService.java
this.jmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage(msg.toString(TAG0));
return textMessage;
}
});
(4)消息的接收
消息接收的思路是通过消息侦听器ReceiveMessageListener来捕获。
源代码:ReceiveMessageListener.java
public class ReceiveMessageListener implements MessageListener {
……
@Override
public void onMessage(Message message) {
final String TAG0 = Module_Tag+"::onMessage()";
if (message instanceof TextMessage) { //文本消息
final TextMessage msg = (TextMessage) message;
String text = null;
try {
text = msg.getText();
} catch(JMSException e) {
this.logger.E(TAG0, e.getMessage() );
}
……
}
}
……
};
(5)相关依赖包
源代码:pom.xml
<!-- 消息队列组件 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.11.4</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-jaas</artifactId>
<version>5.11.4</version>
</dependency>
Spring框架的版本为:4.3.25.RELEASE。
3 结语
嵌入式的消息队列服务适用于接口异步操作的场合,例如:数据记录导出、图像生成、调用第三方的工具服务等。
嵌入模式的优势在于,既不考虑网络通信(哪怕是LAN),又能使用消息队列的功能特性。
出于稳定安全考虑,消息队列建议支持持久化(哪怕是Web应用重启),且采用一定安全机制(ActiveMQ支持的是JAAS)。
相关文档:
(1)有关ActiveMQ jar文件版本导致Spring MVC框架代码冲突的问题,可参见:【开发笔记】Spring MVC框架升级错误:找不到ReflectionUtils.doWithLocalFields方法。
在Spring框架中使用嵌入的消息队列代理服务(二)