最近项目进度比较赶,可怜的老王一直在加班,好几天没更新,不能松懈。借着周末这个机会一起学习下MQ的相关知识。
本文主要内容为Springboot整合RabbitMQ的基本配置、测试实例。不深度涉及RabbitMQ的理论概念等知识,我也还在学习中。
更新日志:
2019-12-07 新建
2019-12-11 增加生产者和消费者确认模式
RabbitMQ介绍
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
RabbitMQ部署
我选择的是使用docker部署rabbitmq,如果有小伙伴还没有部署,可以点击我前面的文章,先部署MQ,再开始实例操作。
SpringBoot整合RabbitMQ实例
GitHub地址
https://github.com/oldwang666666/springboot-rabbitmq-demo
工程目录结构
新建maven工程,输入信息后,点击Finish
pom文件内容
<?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.springboot.demo</groupId>
<artifactId>springboot-rabbitmq-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>consumer-service</module>
<module>provider-service</module>
</modules>
</project>
新建消息生产者provider-service工程
1、New Module,新建provider-service ,rabbitmq生产(提供)者
2、建好provider-service 后点击pom.xml文件,添加rabbitmq依赖,依赖只有一个,我就不贴出工程的所以依赖啦
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
3、配置application.properties,填上你的rabbitmq server ip和port,程序启动端口设置为8083
server.port=8083
#rabbitmq
spring.rabbitmq.host=your rabbimq server ip
spring.rabbitmq.port=your rabbimq server port
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#开启 confirm 确认机制
spring.rabbitmq.publisher-confirms=true
#开启 return 确认机制
spring.rabbitmq.publisher-returns=true
4、ProviderServiceApplication 可以使用@EnableRabbit开启注解模式。
5、新建一个消息发送控制类,我建了SendMessageController方便测试,小伙伴可以根据自己需要自己创建一个
package com.springboot.demo.provider.controller;
import com.springboot.demo.provider.service.SendMessageService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* rabbitmq生产控制层
* @Description
* @Author longzhang.wang
* @Since 1.0
* @Date 2019/12/3
*/
@RequestMapping("/rabbitmq")
@RestController
public class SendMessageController {
//使用RabbitTemplate,来发送消息
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
private SendMessageService sendMessageService;
/**
* @MethodName Default 默认交换机 demo
* @Description
* @Author longzhang.wang
* @Version V1.0.0
* @Since 2019/12/11
*/
@RequestMapping("/defaultSendMessage")
public String defaultSendMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "Default 默认交换机 消息发送";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
System.out.println("发送消息:" + map.toString());
rabbitTemplate.convertAndSend("DefaultQueue", map.toString());
return "发送成功";
}
/**
* @MethodName Direct 直连交换机 demo
* @Description
* @Author longzhang.wang
* @Version V1.0.0
* @Since 2019/12/11
*/
@RequestMapping("/directSendMessage")
public String directSendMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "Direct 直连交换机 消息发送";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
System.out.println("发送消息:" + map.toString());
//(交换机, routingKey, 消息内容)
rabbitTemplate.convertAndSend("DirectExchange","direct.key", map.toString());
return "发送成功";
}
/**
* @MethodName Fanout 伞形交换机 demo
* @Description
* @Author longzhang.wang
* @Version V1.0.0
* @Since 2019/12/11
*/
@RequestMapping("/fanoutSendMessage")
public String fanoutSendMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "Fanout 伞形交换机 消息发送";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
System.out.println("发送消息:" + map.toString());
//(交换机, routingKey, 消息内容)
rabbitTemplate.convertAndSend("FanoutExchange","none", map.toString());
return "发送成功";
}
/**
* @MethodName 消息确认模式 demo
* @Description
* @Author longzhang.wang
* @Version V1.0.0
* @Since 2019/12/11
*/
@RequestMapping("/sendConfirmMessage")
public String SendConfirmMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "SendConfirmMessage 消息发送";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String,Object> map=new HashMap<>();
//业务主键
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
sendMessageService.sendString(map.toString());
return "发送成功";
}
}
6、新建一个消息发送Service
package com.springboot.demo.provider.service;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* 消息发送service
* @Description
* @Author longzhang.wang
* @Since 1.0
* @Date 2019/12/11
*/
@Service
public class SendMessageService implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* @MethodName 直连交换机消费者
* @Description
* @Author longzhang.wang
* @param message 消息内容
* @Return
* @Version V1.0.0
* @Since 2019/12/11
*/
public void sendString(String message){
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
System.out.println("**********************************************************");
System.out.println("发送消息:" + message + "callbackSender UUID: " + correlationData.getId());
rabbitTemplate.convertAndSend("DirectExchange","direct.key", message, correlationData);
System.out.println("----------------------------------------------------------");
}
/**
* @MethodName 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
* @Description
* @Author longzhang.wang
* @param correlationData 唯一标示符
* @param ack 结果
* @param cause 错误信息,正常时返回null
* @Return
* @Version V1.0.0
* @Since 2019/12/11
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack) {
System.out.println("confirm方法: " + correlationData.getId() + ", 发送正常,ack:" + ack + ",cause:" + cause);
} else {
System.out.println("confirm方法: " + correlationData.getId() + ", 发送失败,ack:" + ack + ",cause:" + cause);
}
}
/**
* @MethodName 通过实现 ReturnCallback 接口,启动消息失败返回,比如路由没有到达到队列时触发回调
* @Description
* @Author longzhang.wang
* @param message 消息主体 message
* @param replyCode 回应编码 replyCode
* @param replyText 描述
* @param exchange 消息使用的交换器 exchange
* @param routingKey 消息使用的路由键 routing
* @Return
* @Version V1.0.0
* @Since 2019/12/11
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息主体 message : " + message);
System.out.println("回应编码 replyCode : " + replyCode);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交换器 exchange : " + exchange);
System.out.println("消息使用的路由键 routing : " + routingKey);
}
}
rabbitmq生产者创建完毕,我们来创建rabbitmq消费者
新建消息消费者consumer-service工程
1、New Module,新建consumer-service ,rabbitmq消费者,步骤和生产者差不多
2、同样,在pom.xml添加rabbimq依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
3、配置application.properties,填上你的rabbitmq server ip和port,我们启动端口错开,设置为8084
server.port=8084
#rabbitmq
spring.rabbitmq.host=your rabbimq server ip
spring.rabbitmq.port=your rabbimq server port
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
4、ConsumerServiceApplication.java 可以使用@EnableRabbit开启注解模式。
5、配置RabbitConfig.java,这里我只写了普通的 Default(默认)、Direct(直连)和Fanout(伞形)交换机demo,所以没有太多配置,只初始了DefaultQueue给默认交换机demo使用。
package com.springboot.demo.consumer.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
/**
* 队列:TestQueue ,true 是否持久
* @return
*/
@Bean
public Queue TestDirectQueue() {
return new Queue("DefaultQueue",true);
}
}
6、新建 Default(默认)、Direct(直连)和Fanout(伞形)交换机demo
@RabbitListener
- 使用 @RabbitListener 注解标记方法,当监听到队列中有消息时则会进行接收并处理。
- 使用 @RabbitListener标注的类(或方法所在类)必须可被Bean实例化,也就是要么在@Bean方法中手动创建实例,或者用@Component标注。
Default(默认)交换机demo:RabbitDefaultListenerService.java,我们也可以把@RabbitListener(queues = "DefaultQueue")写在getMessage方法上,当有多个方法的时候会去选择你配置的方法进行调用。
package com.springboot.demo.consumer.receiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 默认交换机消费者
* @Description
* @Author longzhang.wang
* @Since 1.0
* @Date 2019/12/3
*/
@Component
@RabbitListener(queues = "DefaultQueue")
public class RabbitDefaultListenerService {
//这里只是demo所以直接抛出异常,实际业务要根据自己的业务进行异常捕获,使用basicAck、basicNack、basicReject等方法对消息进行处理,还有幂等处理
@RabbitHandler
public void getMessage(@Payload String message, Channel channel, @Headers Map<String, Object> headers) throws Exception {
long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
System.out.println("幂等处理 & 业务处理 getMessage消费者收到消息 : " + message);
//消息确认,deliveryTag:该消息的index,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(deliveryTag, false);
}
}
Direct(直连)交换机demo:RabbitDirectListenerService.java 其中还有附带了参考了其他文章标注的 rabbitmq channel部分方法,想要深入学习channel方法的同学可以网上搜索相关文章学习。
package com.springboot.demo.consumer.receiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.Map;
/*
主要方法介绍,参考文章:https://www.cnblogs.com/piaolingzxh/p/5448927.html rabbitmq channel参数详解
------------------------------------------------------
1、channel.basicPublish
exchange:交换器
routingKey:路由键,#匹配0个或多个单词,*匹配一个单词,在topic exchange做消息转发用
mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉
immediate:true:如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
BasicProperties:需要注意的是BasicProperties.deliveryMode,0:不持久化 1:持久化 这里指的是消息的持久化,配合channel(durable=true),queue(durable)可以实现,即使服务器宕机,消息仍然保留
body:要发送的消息body数组
PS:简单来说:mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException;
------------------------------------------------------
2、channel.basicAck();
deliveryTag:该消息的index
multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
void basicAck(long deliveryTag, boolean multiple) throws IOException;
------------------------------------------------------
3、channel.basicNack();
deliveryTag:该消息的index
multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
requeue:被拒绝的是否重新入队列
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
------------------------------------------------------
4、channel.basicReject()
deliveryTag:该消息的index
requeue:被拒绝的是否重新入队列
PS:channel.basicNack 与 channel.basicReject 的区别在于basicNack可以拒绝多条消息,而basicReject一次只能拒绝一条消息
void basicReject(long deliveryTag, boolean requeue) throws IOException;
*/
/**
* 直连交换机消费者
* @Description
* @Author longzhang.wang
* @Since 1.0
* @Date 2019/12/3
*/
@Component
@RabbitListener(bindings = @QueueBinding(value = @Queue("DirectQueue")
, exchange = @Exchange(value = "DirectExchange", type = ExchangeTypes.DIRECT)
, key = "direct.key"))
public class RabbitDirectListenerService {
@RabbitHandler
public void getMessage(@Payload String message, Channel channel, @Headers Map<String, Object> headers) throws Exception {
long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
try {
//业务逻辑处理......
System.out.println("幂等处理 & 业务处理 getMessage消费者String收到deliveryTag : " + deliveryTag + ", 收到消息: " + message);
//消息确认,deliveryTag:该消息的index,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//deliveryTag:该消息的index,multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息,requeue:被拒绝的是否重新入队列(false:不进入,true:进入)
channel.basicNack(deliveryTag , false,true);
throw new RuntimeException(e);
}
}
@RabbitHandler
public void getMessage(@Payload byte[] message, Channel channel, @Headers Map<String, Object> headers) throws Exception {
long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
//做拒绝查看demo,没有死信队列,所以拒绝后会一直重发,方便查看rabbimq消息 数字大家可以自己修改
if(deliveryTag > 4) {
System.out.println("deliveryTag:" + deliveryTag + ",---------消息拒绝 : " + (new String(message)));
//消息拒绝deliveryTag:该消息的index,requeue:被拒绝的是否重新入队列(false:不进入,true:进入)
channel.basicReject(deliveryTag, true);
// channel.basicNack(deliveryTag , false,true);
} else {
System.out.println("幂等处理 & 业务处理 getMessage消费者byte收到deliveryTag : " + deliveryTag + "消费者byte收到消息 : " + (new String(message)));
channel.basicAck(deliveryTag, false);
}
}
}
Fanout(伞形)交换机demo:RabbitFanoutListenerService.java
package com.springboot.demo.consumer.receiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.annotation.RabbitListeners;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 伞形交换机消费者
* @Description
* @Author longzhang.wang
* @Since 1.0
* @Date 2019/12/3
*/
@Component
@RabbitListeners({
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("FanoutQueue-01"),
exchange = @Exchange(value = "FanoutExchange", type = ExchangeTypes.FANOUT),
key = "key.one")),
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("FanoutQueue-02"),
exchange = @Exchange(value = "FanoutExchange", type = ExchangeTypes.FANOUT),
key = "key.two")),
@RabbitListener(
bindings = @QueueBinding(
value = @Queue("FanoutQueue-03"),
exchange = @Exchange(value = "FanoutExchange", type = ExchangeTypes.FANOUT),
key = "key.three")),
})
public class RabbitFanoutListenerService {
//这里只是demo所以直接抛出异常,实际业务要根据自己的业务进行异常捕获,使用basicAck、basicNack、basicReject等方法对消息进行处理,还有幂等处理
@RabbitHandler
public void getMessage(@Payload String msg, Channel channel, @Headers Map<String, Object> headers) throws Exception {
long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
//headers.get(AmqpHeaders.CONSUMER_QUEUE) 打印的是QueueBinding中Queue的值
System.out.println("幂等处理 & 业务处理 接收" + headers.get(AmqpHeaders.CONSUMER_QUEUE) + "消费者的消息:" + msg);
//消息确认,deliveryTag:该消息的index,multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(deliveryTag, false);
}
}
以上,rabbimq消费者配置完成,咱们进入运行环节。
运行环节
1、启动生产者和消费者,查看8083和8084服务是否启动。
2、测试Default(默认)交换机demo http://localhost:8083/rabbitmq/defaultSendMessage ,结果为下图,则发送完成。
生产者后台打印
消费者后台打印
3、测试Direct(直连)交换机demo http://localhost:8083/rabbitmq/directSendMessage
生产者后台打印
消费者后台打印
4、Fanout(伞形)交换机demo http://localhost:8083/rabbitmq/fanoutSendMessage
生产者后台打印
消费者后台打印
5、确认模式demo http://localhost:8083/rabbitmq/sendConfirmMessage
生产者后台打印
消费者后台打印
以上,Springboot整合RabbitMQ的一个简单demo就完成了,当然RabbitMQ远不止这些,这些只是最初级的demo,后面掌握了rabbimq再补充其他用法。
参考文章:https://www.cnblogs.com/piaolingzxh/p/5448927.html - rabbitmq channel参数详解