SpringBoot整合RabbitMQ实例 消息确认模式 附GitHub代码 demo下载直接运行

最近项目进度比较赶,可怜的老王一直在加班,好几天没更新,不能松懈。借着周末这个机会一起学习下MQ的相关知识。

本文主要内容为Springboot整合RabbitMQ的基本配置、测试实例。不深度涉及RabbitMQ的理论概念等知识,我也还在学习中。

更新日志:
2019-12-07 新建
2019-12-11 增加生产者和消费者确认模式

RabbitMQ介绍

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

 

RabbitMQ部署

我选择的是使用docker部署rabbitmq,如果有小伙伴还没有部署,可以点击我前面的文章,先部署MQ,再开始实例操作。

阅读  Docker安装RabbitMQ教程  点击此处

 

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

  1. 使用 @RabbitListener 注解标记方法,当监听到队列中有消息时则会进行接收并处理。
  2. 使用 @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参数详解

发布了22 篇原创文章 · 获赞 4 · 访问量 3021

猜你喜欢

转载自blog.csdn.net/weixin_43841693/article/details/103437925