作为一个原生支持Java的消息中间件ActiveMQ,哪里都会插一手的Spring也提供了一套继承JMS连接ActiveMQ的定制方案。Spring基于JMS原生接口规范和原生实现,自行封装实现了一套能够良好集成到Spring框架中并有效管理的解决方案。本文暂时只讨论基本Spring中集成JMS中间件的方法,有关springboot的中activemq的集成,我打算放到springboot必知必会系列的后续文章中再写。
spring为JMS重新封装了几个接口,用于在调用时替换原接口:
(1)ConnectionFactory用于管理连接的连接工厂。
注意它并不是JMS规范中的ConnnectionFactory
(2)JMSTemplate用于接收和调用消息的模板类。
用于接收和发送消息的模板。JMS规范里用的是MessageProducer和MessageConsumer。
(3)MessageListener消息监听器
ConnectionFactory
它是Spring为我们提供的一个连接池。
为什么需要一个连接池呢?
那是因为JMSTemplate每次发送消息都创建一个连接,会话和producer。这是一个非常耗费性能的环节,特别是在操作大量数据的情况下。所以说spring为我们提供了一个连接池。
在spring中,spring提供了两个连接池:SingleConnectionFactory和CachingConnectionFactory。
SingleConnectionFactory对同一个JMS服务器的请求只会返回同一个Connection。
CachingConnectionFactory继承了SingleConnectionFactory,拥有SingleConnectionFactory的所有功能。同时,还新增了缓存功能,可以缓存Session会话、producer、consumer。
JMSTemplate
JMSTemplate是spring提供的,只需要向spring容器注册这个类就可以使用JMSTemplate来方便地操作JMS。
JMSTemplate是线程安全的,可以在整个应用中使用它。但这并不代表我们只能使用一个JMSTemplate。
MessageListener
消息监听器MessageListener,我们只需要实现它的一个onMessage方法,该方法会接收一个Message参数。我们可以在onMessage方法中实现我们对消息的处理。
我们导入依赖:
我们首先需要指定spring上下文的基础jar。同时引入一个active core,这里包含了基础JMS规范接口;然后我们引入spring activemq,这里包含我们刚才说到的spring的jms接口以及实现类(spring的ConnectionFactory和JMSTemplate)等。
这里注意active core中自己已经包含了spring context,由于我们项目中已经引入了spring context,为了以免冲突,这里需要将activemq core的坐标依赖移除内置spring context。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.happybks.jms</groupId>
<artifactId>jms-spring</artifactId>
<version>1.0-SNAPSHOT</version>
<name>jms-spring</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.17.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jms -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>4.3.17.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.17.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-core -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.7.0</version>
<exclusions>
<exclusion>
<artifactId>spring-context</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
spring JMS队列模式 生产者
我们先声明一个服务接口:
package com.happybks.jms.producer;
public interface ProducerService {
void sendMessage(final String message);
}
然后写明其实现类:
注意这里的注解如果bean容器中某个bean的类型唯一,则可以用@Autowired。
如果bean如期中存在两个(或者以后可能配置多个)相同类型(或者存在继承关系)的,则无法通过类型来指明是哪一个bean,因为会冲突异常,所以需要用@Resource(name=?),通过bean的id来指定。如果非要想用@Autowired又想指明id,可以在@Autowired上配合使用 @Qualifier("???")
package com.happybks.jms.producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator ;
import javax.annotation.Resource;
import javax.jms.*;
public class ProducerServiceImpl implements ProducerService {
@Autowired
JmsTemplate jmsTemplate;
//之所以使用@Resouce,是因为项目中可能会有多个Destination对象,这里我们需要按照bean的id来进行注入,而不是通过类型来@Autowired
@Resource(name = "queueDestination")
Destination destination;
@Override
public void sendMessage(final String message) {
final MessageCreator messageCreator = new MessageCreator() {
//实现MessageCreator接口的createMessage方法,要求实现对消息的创建。创建消息需要一个Session会话。
//创建一个消息
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage(message);
return textMessage;
}
};
//使用jmsTemplate发送消息
jmsTemplate.send(destination, messageCreator);
System.out.println("发送消息:" + message);
}
}
相应地,我们创建一个producer.xml配置spring容器中的bean。
我这里IDE使用IDEA,可以选择新建时用spring Config
但是这个生成的xml的beans的标签头里的声明有些问题,需要将以下两个删除,不然配置context命名空间的种种配置时不会自动提示。
spring配置文件producer.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<!-- 这个是ActiveMQ为我们提供的ConnectionFactory -->
<bean id="producerTargetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://127.0.0.1:61616"/>
</bean>
<!-- 这个是spring jms为我们提供的连接池 -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="producerTargetConnectionFactory"/>
</bean>
<!-- 做一个队列模式的消息,需要一个消息目的地 -->
<!-- 一个队列目的地,队列目的地是点对点的,即P2P。所以队列模式又叫P2P模式 -->
<!-- 这里传入一个构造参数,这个参数是队列目的地的起的名字-->
<bean id="queueDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="queue"/>
</bean>
<!-- 配置JmsTemplate,用于发送消息-->
<!-- 注意:这里的参数是spring为我们提供的连接池-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<bean class="com.happybks.jms.producer.ProducerServiceImpl"/>
</beans>
最后我们编写一个生产者服务的调用程序类,将我们配置好的生产者服务调用一下,向消息中间件发送100个消息:
package com.happybks.jms.producer;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AppProducer {
public static void main(String[] args) {
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("producer.xml");
ProducerService service = context.getBean(ProducerService.class);
for (int i = 0; i < 100; i++) {
service.sendMessage(i+". hello! happyBKs!");
}
//让spring容器将所有的资源(包括连接)都清理掉,不然连接会一直存在。
context.close();
}
}
运行程序,发送消息
查看管理界面http://127.0.0.1:8161
注意这个是activemq管理页面的端口,8161。刚才代码里配置的服务端口是tcp协议,61616端口。
spring JMS队列模式 消费者
同样我们需要给消费者创建一个程序,构建一个单独的spring容器配置文件consumer.xml。但是我们发现其实很多bean的构建,比如链接工厂、destination目的地等都是通用的,所以我们将生产者和消费者通用的部分抽出来,单独弄一个common.xml然后在consumer.xml和producer.xml中引用资源即可。
common.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<!-- 这个是ActiveMQ为我们提供的ConnectionFactory -->
<bean id="producerTargetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://127.0.0.1:61616"/>
</bean>
<!-- 这个是spring jms为我们提供的连接池 -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="producerTargetConnectionFactory"/>
</bean>
<!-- 做一个队列模式的消息,需要一个消息目的地 -->
<!-- 一个队列目的地,队列目的地是点对点的,即P2P。所以队列模式又叫P2P模式 -->
<!-- 这里传入一个构造参数,这个参数是队列目的地的起的名字-->
<bean id="queueDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="queue"/>
</bean>
</beans>
producer.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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="common.xml"/>
<!-- 配置JmsTemplate,用于发送消息-->
<!-- 注意:这里的参数是spring为我们提供的连接池-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<bean class="com.happybks.jms.producer.ProducerServiceImpl"/>
</beans>
consumer.xml
这里我们新加入consumer.xml作为消费者客户端程序的spring容器,这里需要另外创建两个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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--导入公共配置-->
<import resource="common.xml"/>
<!--配置消息监听器-->
<bean id="consumerMessageListener" class="com.happybks.jms.consumer.ConsumerMessageListener"/>
<!--配置消息容器-->
<!--创建一个JMS消息监听的容器,这个监听容器完全是由spring为我们提供的。-->
<!--这个容器的作用是管理容器中的连接,让其去自动连接我们的连接工厂;指定我们的消息目的地和我们的消息监听者。-->
<!--所以需要配置一个连接工厂,一个消息监听器-->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<!--指明需要监听的消息地址-->
<property name="destination" ref="queueDestination"/>
<!--配置消息监听器-->
<property name="messageListener" ref="consumerMessageListener"/>
</bean>
</beans>
消息监听器需要我们自己继承MessageListener 接口自己实现。
其中覆盖onMessage方法对消息进行接收后的处理。
package com.happybks.jms.consumer;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
public class ConsumerMessageListener implements MessageListener {
//onMessage方法被调用意味着 MessageListener已经接受到消息了。
@Override
public void onMessage(Message message) {
TextMessage textMessage=(TextMessage)message;
try {
System.out.println("消费者接收到了消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
同样,我们编写一个客户端启动类:
package com.happybks.jms.consumer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AppConsumer {
public static void main(String[] args) {
final ApplicationContext applicationContext = new ClassPathXmlApplicationContext("comsumer.xml");
// 由于消息的接收是一个异步的过程,如果这里把application关闭,会连同连接等一起关闭,可能会导致消息接收不全,所以这里我们就不close()了
}
}
运行程序:
查看管理界面http://127.0.0.1:8161
队列模式的多客户端接收实验:
我们像上一文章那样启动两个消费者,等待接收JMS消息中间件中的消息。
我们然后启动一个生产者,发送100个消息:
发现两个消费者平分了消息。
主题模式 JMS 发布者和订阅者
主题模式下的发布者和订阅者的代码实现与之前队列模式的基本相同,这里我们只说需要改动的三个地方:
(1)在spring容器配置文件common.xml中新配置一个desitnation的bean,类型是Destination的主题子类:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<!-- 这个是ActiveMQ为我们提供的ConnectionFactory -->
<bean id="producerTargetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://127.0.0.1:61616"/>
</bean>
<!-- 这个是spring jms为我们提供的连接池 -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="producerTargetConnectionFactory"/>
</bean>
<!-- 做一个队列模式的消息,需要一个消息目的地 -->
<!-- 一个队列目的地,队列目的地是点对点的,即P2P。所以队列模式又叫P2P模式 -->
<!-- 这里传入一个构造参数,这个参数是队列目的地的起的名字-->
<bean id="queueDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="queue"/>
</bean>
<!--一个主题目的地,发布订阅模式-->
<bean id="topicDestination" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="topic"/>
</bean>
</beans>
(2)更改发布者(生产者)服务实现类中desination的注入引用的注解,将@Resource的name换为我们刚刚配置的新Destination主题目的地bean——topicDestination。
package com.happybks.jms.producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator ;
import javax.annotation.Resource;
import javax.jms.*;
public class ProducerServiceImpl implements ProducerService {
@Autowired
JmsTemplate jmsTemplate;
//之所以使用@Resouce,是因为项目中可能会有多个Destination对象,这里我们需要按照bean的id来进行注入,而不是通过类型来@Autowired
//@Resource(name = "queueDestination")
@Resource(name = "topicDestination")
Destination destination;
@Override
public void sendMessage(final String message) {
final MessageCreator messageCreator = new MessageCreator() {
//实现MessageCreator接口的createMessage方法,要求实现对消息的创建。创建消息需要一个Session会话。
//创建一个消息
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage(message);
return textMessage;
}
};
//使用jmsTemplate发送消息
jmsTemplate.send(destination, messageCreator);
System.out.println("发送消息:" + message);
}
}
(3)修改订阅者(消费者)的spring容器配置文件comsumer.xml中的消息容器jmsContainer的属性destination为topicDestination
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--导入公共配置-->
<import resource="common.xml"/>
<!--配置消息监听器-->
<bean id="consumerMessageListener" class="com.happybks.jms.consumer.ConsumerMessageListener"/>
<!--配置消息容器-->
<!--创建一个JMS消息监听的容器,这个监听容器完全是由spring为我们提供的。-->
<!--这个容器的作用是管理容器中的连接,让其去自动连接我们的连接工厂;指定我们的消息目的地和我们的消息监听者。-->
<!--所以需要配置一个连接工厂,一个消息监听器-->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<!--指明需要监听的消息地址-->
<!--<property name="destination" ref="queueDestination"/>-->
<property name="destination" ref="topicDestination"/>
<!--配置消息监听器-->
<property name="messageListener" ref="consumerMessageListener"/>
</bean>
</beans>
完成以上三处修改,我们就成功将刚才的队列模式(或者说P2P模式)的代码改为了主题模式的情景。
运行主题模式程序:
首先我们启动两个订阅者客户端,注意,一定是先启动订阅者后发布者,因为在主题模式下,订阅者只能接收其订阅之后时间内JMS中间件上发布者发布的消息,订阅之前的无法接收。
之后我们启动发布者程序,发送100个消息:
瞬间,两个订阅者程序也收到了消息,而且都是完整的、一模一样的消息:
附: