前言
内容主要参考自《Spring源码深度解析》一书,算是读书笔记或是原书的补充。进入正文后可能会引来各种不适,毕竟阅读源码是件极其痛苦的事情。
本文主要涉及书中第十三章的部分,依照书中内容以及个人理解对Spring源码进行了注释,详见Github仓库:https://github.com/MrSorrow/spring-framework
Java消息服务 (Java Message Service,JMS) 应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间或分布式系统中发送消息进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数 MOM 提供商都对 JMS 提供支持。
Java消息服务的规范包括两种消息模式,点对点和发布者/订阅者。许多提供商支持这一通用框架。因此,程序员可以在他们的分布式软件中实现面向消息的操作,这些操作将具有不同面向消息中间件产品的可移植性。
Java消息服务支持同步和异步的消息处理,在某些场景下,异步消息是必要的,而且比同步消息操作更加便利。
本文以 Java 消息服务的开源实现产品 ActiveMQ 为例来进行Spring整合消息服务功能的实现分析。
I. 单独使用ActiveMQ
安装ActiveMQ
这里我是在Windows平台上安装 ActiveMQ 的。需要等上官网,下载 apache-activemq-5.15.7-bin.zip 。下载完毕后解压至本地磁盘,运行 apache-activemq-5.15.7\bin\win64\activemq.bat 批处理文件,ActiveMQ 就能够顺利启动了。
访问 http://localhost:8161/admin,登录默认账号密码都为 admin。
JMS独立使用
尽管大多数的Java消息服务的使用都会跟Spring相结合,但是,我们还是非常有必要了解消息的独立使用方法,这对于我们了解消息的实现原理以及后续的与Spring整合实现分析都非常重要。消息服务的使用除了要开启消息服务器外,还需要构建消息的发送端与接收端,发送端主要用来将包含业务逻辑的消息发送至消息服务器,而消息接收端则用于将服务器中的消息提取并进行相应的处理。
① 发送端
发送端主要用于发送消息到消息服务器,以下为发送消息测试,尝试发送 10 条消息到消息服务器,消息的内容为“测试发送消息”。
public class Sender {
public static void main(String[] args) throws JMSException, InterruptedException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("my-queue");
MessageProducer producer = session.createProducer(destination);
for (int i = 0; i < 10; i++) {
TextMessage message = session.createTextMessage("测试发送消息");
Thread.sleep(1000);
producer.send(message);
}
session.commit();
session.close();
connection.close();
}
}
② 接收端
接收端主要用于连接消息服务器并接收服务器上的消息。
public class Receiver {
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("my-queue");
MessageConsumer consumer = session.createConsumer(destination);
int i = 0;
while (i < 10) {
i++;
TextMessage message = (TextMessage) consumer.receive();
session.commit();
System.out.println("接收到消息内容为:" + message.getText());
}
session.close();
connection.close();
}
}
③ 测试结果
先运行发送端向消息队列中发送 10 条消息,然后运行接收端,即可打印出发送端向 my-queue 发送的 10 条消息内容。接收端总共消费了10次消息,消息队列中 my-queue 中的消息应该全部被消费完毕。
分析
从发送端与接收端的代码可以看出,整个消息的发送与接收过程非常简单,但是其中却参杂着大量的冗余代码,比如 Connection 的创建与关闭,Session 的创建与关闭等。
对于发送的流程,主要包括:
- 初始化连接工厂;
- 利用连接工厂创建一个连接;
- 使用连接建立会话 Session;
- 通过会话创建一个管理对象 Destination ,包括队列 (Queue) 或主题 (Topic);
- 使用会话 Session 和管理对象 Destination 创建消息生产者 MessageSender;
- 使用消息生产者 MessageSender 发送消息。
对于接收的流程,主要包括:
- 1-4 步与发送相同;
- 使用会话 Session 和管理对象 Destination 创建消息消费者 MessageConsumer;
- 使用消息消费者 MessageConsumer 接收消息。
很容易让我们联想到数据库JDBC的实现,在使用消息队列时都需要一系列冗余的但又必不可少的套路代码,而其中真正用于数据操作/发送消息的代码其实很简单。前 1-3 步都可以Spring帮助我们完成,包含个性化信息的步骤交给用户进行设置。
所以Spring对于 JMS 消息队列同样利用模板设计模式加上回调的方式提供了一个模板类 JmsTemplate
,能让我们非常快捷方便地利用Spring进行消息的收发。
II. Spring整合ActiveMQ
和Spring封装JDBC一样,Spring也提供了一个模板类 JmsTemplate
来帮助我们使用 JMS。
添加依赖
主要在环境中添加上 spring-jms 和 activemq 两个依赖即可。
plugins {
id 'java'
}
group 'org.springframework'
version '5.1.0.BUILD-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile(project(":spring-beans"))
compile(project(":spring-context"))
compile(project(":spring-aop"))
compile(project(":spring-jdbc"))
compile(project(":spring-jms"))
compile group: 'org.springframework', name: 'spring-aspects', version: '5.0.7.RELEASE'
compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.5.0'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.18'
compile group: 'org.mybatis', name: 'mybatis', version: '3.4.6'
compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.2'
compile group: 'org.apache.activemq', name: 'activemq-pool', version: '5.15.7'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
sourceSets.main.resources.srcDirs = ["src/main/java","src/main/resources"]
配置文件
Spring整合消息服务的使用也从配置文件配置开始。在 Spring 的核心配置文件中首先要注册 JmsTemplate
类型的 bean。当然,ActiveMQConnectionFactory
用于连接消息服务器,是消息服务的基础,也要注册 ActiveMQQueue
则用于指定消息的目的地。
<?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.xsd">
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>tcp://127.0.0.1:61616</value>
</property>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory">
<ref bean="connectionFactory" />
</property>
</bean>
<bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg index="0">
<value>spring_and_activemq</value>
</constructor-arg>
</bean>
</beans>
发送端
有了以上的配置,Spring就可以根据配置信息简化我们的工作量。Spring中使用发送消息到消息服务器,省去了冗余的 Connection
以及 Session
等的创建与销毁过程,简化了工作量。
public class SpringSender {
@Test
public void sendMessage() {
ApplicationContext context = new ClassPathXmlApplicationContext("activeMQ-Test.xml");
JmsTemplate jmsTemplate = (JmsTemplate) context.getBean("jmsTemplate");
Destination destination = (Destination) context.getBean("destination");
jmsTemplate.send(destination, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("发送一个消息");
}
});
}
}
接收端
同样,在Spring中接收消息也非常方便,Spring中连接服务器接收消息的示例如下。
public class SpringReceiver {
@Test
public void receiveMessage() {
ApplicationContext context = new ClassPathXmlApplicationContext("activeMQ-Test.xml");
JmsTemplate jmsTemplate = (JmsTemplate) context.getBean("jmsTemplate");
Destination destination = (Destination) context.getBean("destination");
TextMessage textMessage = (TextMessage) jmsTemplate.receive(destination);
System.out.println("接收到消息:" + textMessage);
}
}
测试结果
同样,先运行发送端程序,然后运行接收端代码,测试结果如下。
监听器
使用 jmsTemplate.receive(destination)
方法只能接收一次消息,如果未接收到消息,则会一直等待,当然用户可以通过设置 timeout 属性来控制等待时间,但是一旦接收到消息本次接收任务就会结束,虽然用户可以通过 while(true) 的方式来实现循环监听消息服务器上的消息,还有一种更好的解决办法:创建消息监听器。消息监听器的使用方式如下。
① 创建消息监听器
用于监听消息,一旦有新消息Spring会将消息引导至消息监听器以方便用户进行相应的逻辑处理。实现监听器需要实现 MessageListener
接口,重写 onMessage()
方法。
public class MyMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
TextMessage msg = (TextMessage) message;
try {
System.out.println("接收消息: " + msg.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
② 修改配置文件
注入自定义的监听器 bean,添加一个监听器容器。
<?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.xsd">
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>tcp://127.0.0.1:61616</value>
</property>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory">
<ref bean="connectionFactory" />
</property>
</bean>
<bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg index="0">
<value>spring_and_activemq</value>
</constructor-arg>
</bean>
<bean id="myMessageListener" class="guo.ping.activemq.MyMessageListener" />
<bean id="javaConsumer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="destination" />
<property name="messageListener" ref="myMessageListener" />
</bean>
</beans>
③ 测试结果
通过以上的修改便可以进行消息监听的功能了,一旦有消息传入至消息服务器,则会被消息监听器监听到,并由Spring将消息内容引导至消息监听器的处理函数中等待用户的进一步逻辑处理。
将发送代码改为循环发送10条消息,可以看到结果如下,只截取了部分,可以看到监听器一直在接收消息。
消息接收可以使用消息监听器的方式替代模版方法,但是在发送消息的时候是无法替代的。接下来,我们主要研究 JmsTemplate
中的发送接收消息方法。
III. 源码分析
查看 JmsTemplate
的类型层级结构图,发现其实现了 InitializingBean
接口。
实现 InitializingBean
接口的方法是在 JmsAccessor
抽象类中,实现内容如下。
/**
* 实现InitializingBean接口
*/
@Override
public void afterPropertiesSet() {
// 对ConnectionFactory判空
if (getConnectionFactory() == null) {
throw new IllegalArgumentException("Property 'connectionFactory' is required");
}
}
/**
* Return the ConnectionFactory that this accessor uses for obtaining
* JMS {@link Connection Connections}.
*/
@Nullable
public ConnectionFactory getConnectionFactory() {
return this.connectionFactory;
}
方法中只是一个验证连接工厂存在与否的功能,并没有其他逻辑实现。所以,创建 JmsTemplate
没有什么特殊的 bean 后处理等操作,我们可以直接进行 JmsTemplate
模板类中方法的分析。
JmsTemplate
我们先以发送方法为例,使用模板类的 send()
方法示例如下。
jmsTemplate.send(destination, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("发送一个消息");
}
});
可以看到方法中传入了两个参数,一个是 ActiveMQQueue
类型的 bean——destination,另一个是 MessageCreator
接口的实现类实例。实现接口的 createMessage()
方法我们可以看出,主要目的是根据 session 创建用户自定义的消息内容。
继续查看 send()
方法中内容。
/**
* 发送消息
* @param destination the destination to send this message to
* @param messageCreator callback to create a message
* @throws JmsException
*/
@Override
public void send(final Destination destination, final MessageCreator messageCreator) throws JmsException {
execute(session -> {
doSend(session, destination, messageCreator);
return null;
}, false);
}
看到 send()
方法中又调用了 execute()
方法,我们不得不回想起 JdbcTemplate
实现风格,极为相似,两者都是提取一个公共的方法作为最底层、最通用的功能实现,然后又通过回调函数的不同来区分个性化的功能。我们首先查看通用代码的抽取实现。
① 通用代码抽取
通过 send()
方法可以看出,需要传入的参数包含会话 SessionCallback
回调函数接口实现以及一个布尔值表示是否开启向服务器推送连接信息,只有接收信息时需要,发送不需要。由于Spring5对于函数式接口,都采用了 lambda 表达式写法,所以看起来有点不够清晰,其实本质上就是实现 SessionCallback
接口的 doInJms()
方法。
execute(new SessionCallback<Object>() {
public Object doInJms(Session session) throws JMSException {
doSend(session, destination, messageCreator);
return null;
}
}, false);
也就是说,doInJms()
方法中最后真正做的是 doSend(session, destination, messageCreator)
方法,就是实际发送消息的操作,这其实仅与发送有关,我们完全可以把 doInJms()
方法的实际内容替换成接收消息方法,所以Spring利用回调接口,先进行通用代码的抽取。
我们回过头来研究 execute()
方法,没错,execute()
方法就是通用代码部分。根据之前分析 JdbcTemplate
的经验,我们推断,在 execute()
中一定是封装了 Connection
以及 Session
的创建操作等套路代码。
/**
* Execute the action specified by the given action object within a
* JMS Session. Generalized version of {@code execute(SessionCallback)},
* allowing the JMS Connection