springboot整合activemq queue topic消息类型 及消息的手动签收、持久化 — 1
前言
项目由于业务需求需要使用消息中间件,需要支持queue和topic消息类型,持久化消息并在消费后手动签收
安装
本案例activemq安装在docker中
#查找镜像
docker search activemq
#拉取镜像
docker pull webcenter/activemq
#运行容器
docker run -d --name activemq -p 61616:61616 -p 8161:8161 webcenter/activemq
访问宿主机ip:8161/admin可以查看到管理页面
用户名\密码:admin\admin
ActiveMQ
activemq是一个JMSProvider,即实现了JMS规范的消息中间件,因为本身用java开发,所以与spring天然契合。
业务需要处理的消息需要持久化,activemq支持自带的kahadb持久化,也支持自定义持久层比如mysql,我们不追求对消息的高度可视,因此追求性能决定使用内置kahadb
activemq的消息传输分为两种模式:
- p2p,点对点,即一个消息生产者对应一个消费者
- 发布/订阅模式,即允许多个消费者订阅同一个生产者的消息,当发布消息时订阅者不在线,还可以支持消息的持久化
JMS规范
引用https://www.jianshu.com/p/ecdc6eab554c
该文对JMS术语的解释可以让我们更清晰地了解整合时相关的一些配置
Provider/MessageProvider:生产者
Consumer/MessageConsumer:消费者
PTP:Point To Point,点对点通信消息模型
Pub/Sub:Publish/Subscribe,发布订阅消息模型
Queue:队列,目标类型之一,和PTP结合
Topic:主题,目标类型之一,和Pub/Sub结合
ConnectionFactory:连接工厂,JMS用它创建连接
Connnection:JMS Client到JMS Provider的连接
Destination:消息目的地,由Session创建
Session:会话,由Connection创建,实质上就是发送、接受消息的一个线程,因此生产者、消费者都是Session创建的
工程结构
activemq-provider:消息生产者
activemq-consumer:消息消费者
mq相关依赖
<!--ActiveMq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!--消息队列连接池-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.15.0</version>
</dependency>
配置
前言中提到不同的多种需求,比如对消息的点对点收发,对主题消息持久化订阅,对消息读取后的手动签收等等,因此默认的配置无法满足我们的需求,需要对配置进行进一步了解与配置
Destination
activemq-provider中我们针对点对点和主题订阅注册两个默认的Destination Bean,如下
@Configuration
@EnableJms
public class ActiveMQConfig {
/**
* 默认queue
* @return
*/
@Bean
public Queue defaultQueue() {
return new ActiveMQQueue("default") ;
}
/**
* 默认topic
* @return
*/
@Bean
public Topic defaultTopic() {
return new ActiveMQTopic("default") ;
}
}
发送消息controller
@RestController
@RequestMapping(value = "/activemq")
@Api(tags = "activemq整合")
public class SendController {
@Autowired
private SendService sendService;
@RequestMapping(value = "/sendQueueText", method = RequestMethod.GET)
@ApiOperation(value = "消息发送")
@ApiImplicitParams({
@ApiImplicitParam(name = "message", value = "消息", required = true)
})
public ResultDTO sendQueueText(@RequestParam(name = "message") String message)
{
return sendService.sendQueueText(message);
}
}
发送消息service实现
@Service
@Slf4j
public class SendServiceImpl implements SendService {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
@Qualifier(value = "defaultQueue")
private Queue defaultQueue;
@Autowired
@Qualifier(value = "defaultTopic")
private Topic defaultTopic;
@Override
public ResultDTO sendQueueText(String message) {
try {
jmsMessagingTemplate.convertAndSend(defaultQueue, message);
} catch (Exception e) {
log.error(e.getMessage(), e);
return new ResultDTO(CodeMap.STATE_CODE_THROWABLE);
}
return new ResultDTO(CodeMap.STATE_CODE_SUCCESS);
}
@Override
public ResultDTO sendTopicText(String message) {
try {
jmsMessagingTemplate.convertAndSend(defaultTopic, message);
} catch (Exception e) {
log.error(e.getMessage(), e);
return new ResultDTO(CodeMap.STATE_CODE_THROWABLE);
}
return new ResultDTO(CodeMap.STATE_CODE_SUCCESS);
}
}
代码中的相关dto不再列出,后续给出源码地址
JmsListenerContainerFactory
同样的,activemq-consumer中我们为不同的消息模式各定义一个默认的JmsListenerContainerFactory
/**
* topic模式的ListenerContainer
* @param activeMQConnectionFactory
* @return
*/
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerTopic(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setPubSubDomain(true);
bean.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
/**
* Queue模式的ListenerContainer
* @param activeMQConnectionFactory
* @return
*/
@Bean
public JmsListenerContainerFactory<?> jmsListenerContainerQueue(ConnectionFactory activeMQConnectionFactory) {
DefaultJmsListenerContainerFactory bean = new DefaultJmsListenerContainerFactory();
bean.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
bean.setConnectionFactory(activeMQConnectionFactory);
return bean;
}
示例中我们定义的都是DefaultJmsListenerContainerFactory,可以看到JmsListenerContainerFactory还有一个实现类SimpleJmsListenerContainerFactory
它提供了线程池处理消息的方法,需要的可以具体去了解
其次,注册这两个bean时我们指定消息需要消费者手动签收,后续测试是否生效
bean.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE)
消费者service实现类
@Service
@Slf4j
public class ReciveServiceImpl implements ReciveService {
@Override
@JmsListener(destination = "default", containerFactory = "jmsListenerContainerQueue")
public void reciveQueueText(ActiveMQMessage message, Session session) {
try {
System.out.println("收到消息" + message);
message.acknowledge();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
测试queue点对点模式
测试消息能否点对点收发
-
启动activemq-provider
-
启动activemq-consumer
-
访问provider swagger页面发送一条消息
-
consumer成功接收消息
测试点对点是否持久化
-
断开consumer
-
provider发送一条消息
-
观察管理界面,发现消费者下线时消息并没有被消费
-
此时让消费者上线,启动consumer,发现消费者上线后就收到了消息
-
管理页面也显示消息被消费
测试手动签收是否生效
当前可以明确消息确实被签收了,但不确定是自动还是手动,我们注释掉message.acknowledge();
手动签收的代码,测试消息是否确实是手动签收的
注释后启动consumer,再由生产者发送一条消息
很不幸,消息还是被签收了,说明目前并没有实现消息的手动签收,先记录该问题。接下来测试一下topic订阅消息模式
测试订阅模式
测试消息能否被多消费者订阅
- 启动provider
- 启动consumer1
- 启动consumer2
- 由生产者发送一条消息
- 观察管理页面发现我们的一条消息确实被两个消费者接收
测试topic消息是否持久化
-
我们让其中一个消费者下线
-
发送一条消息,发现消息只被在线的消费者消费
-
如果此时让下线的消费者再上线,是否还能收到刚才这条消息呢
-
很遗憾,上线后的消费者并没有收到消息,说明目前的配置也不满足topic类型消息的持久化
总结
至此,我们算是完成了第一步,无论点对点还是订阅模式的消费都可以在正常情况(指双方都在线)的情况下被消费。
但是我们提到,需求远不止这么简单,首先我们要支持消息手动签收,而不是由客户端自动签收。其次,订阅的消息必须持久化,不能因为消费者的掉线(网络等原因)而永久错过这条消息。下一篇我们将继续解决这些问题!