Spring boot整合RabbitM

194 阅读21分钟

前引

使用RabbitMQ首先要下载这不用说,即下载Erlang和RabbitMQ并配置,有不会的可以看博主上一篇保姆级教程

RabbitMQ4.1.0版本windows部署 文章链接

博主的项目使用两个模块,但博主的室友提示博主可以将两个模块简化为一个模块,具体实现思路博主写在后面(博主并未实现,因为觉得没有实际意义,除了简化(`・ω・´))

附上室友的主页链接,感兴趣的朋友可以去看看 落叶拾秋

创建项目

首先创建一个spring boot项目,引入相关依赖以及配置spring boot项目 创建项目博主就不一一演示了,csdn一搜一大堆,这里放出博主的maven配置以及spring boot配置用作参考,另外博主使用的是Java21spring boot3.4.5

pom.xml

不建议各位直接复制pom文件,由于Java和spring boot版本以及项目名模块名不同可能产生报错 ps:这都是血与泪的教训ಥ_ಥ

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>顶级域名.机构缩写</groupId>
    <artifactId>项目名</artifactId>
    <version>版本号</version>
    <name>模块名</name>
    <description>项目描述</description>
    <properties>
        <java.version>21</java.version>
    </properties>
	<dependencies>
			<!-- 添加Spring web支持 -->
	        <dependency>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-starter-web</artifactId>
	        </dependency>
	        <!-- 添加Spring AMQP Starter -->
	        <!-- RabbitMQ主要依赖,简化中间件开发 -->
	        <!-- AMQP即高级消息队列协议 -->
	        <dependency>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-starter-amqp</artifactId> 
	        </dependency>
	        <!-- Lombok用于简化setget方法以及有参无参构造 -->
	        <dependency>
	            <groupId>org.projectlombok</groupId>
	            <artifactId>lombok</artifactId>
	            <optional>true</optional>
	        </dependency>
	        <!-- 添加Spring测试依赖 -->
	        <dependency>
	            <groupId>org.springframework.boot</groupId>
	            <artifactId>spring-boot-starter-test</artifactId>
	            <scope>test</scope>
	        </dependency>
	        <!-- 添加jackson用于序列化Java对象 -->
	        <dependency>
	            <groupId>com.fasterxml.jackson.core</groupId>
	            <artifactId>jackson-databind</artifactId>
	        </dependency>
	
	    </dependencies>
		<!-- 插件部分 -->
	    <build>
	        <plugins>
	            <plugin>
	                <groupId>org.apache.maven.plugins</groupId>
	                <artifactId>maven-compiler-plugin</artifactId>
	                <configuration>
	                    <annotationProcessorPaths>
	                        <path>
	                            <groupId>org.projectlombok</groupId>
	                            <artifactId>lombok</artifactId>
	                        </path>
	                    </annotationProcessorPaths>
	                </configuration>
	            </plugin>
	            <plugin>
	                <groupId>org.springframework.boot</groupId>
	                <artifactId>spring-boot-maven-plugin</artifactId>
	                <configuration>
	                    <excludes>
	                        <exclude>
	                            <groupId>org.projectlombok</groupId>
	                            <artifactId>lombok</artifactId>
	                        </exclude>
	                    </excludes>
	                </configuration>
	            </plugin>
	        </plugins>
	    </build>

依赖的作用在文件中都用注释标注出来了,主要用到的就是Lombok,jackson-databind,spring-boot-starter-amqp三个依赖

application.yaml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtualHost: testhost
    username: #初始账号密码皆为‘guest’
    password: #初始账号密码皆为‘guest’
    publisher-confirm-type: correlated
    publisher-returns: true

配置项解读

常用配置

host:指定 RabbitMQ 服务器的主机名或 IP 地址,若RabbitMQ 部署在其他服务器,需改为对应 IP(如192.168.1.100)

port:RabbitMQ页面操作端端口设置

virtualHost:指定虚拟主机名,在页面操作端可以看到你设置的主机名

username、password:用户认证设置,可以使用初始账号guest也可以使用自己设置的用户,但需要确保用户拥有足够的权限

进阶技巧

publisher-confirm-type:启用发布者确认模式,确保消息成功到达交换器(Exchange),用于确认消息到达,有三种模式none、correlated、simple

模式作用
none不开启确认模式,对于不需要确认消息是否到达时使用
correlated异步确认模式,发送消息时附带唯一 ID,通过回调返回确认结果,适合消息不太重要但需确认时使用
simple同步确认模式,阻塞等待确认结果,适宜消息十分重要,必须传递成功后才能执行下一步时使用

publisher-returns:启用消息返回机制,当消息无法路由到队列时触发回调

它们都是对消息在从生产者发送到 RabbitMQ 服务器后,在服务器内部流转过程中不同阶段的情况进行配置,publisher-confirm-type在消息到达交换器时触发,publisher-returns在消息到达交换器且无法路由到队列时触发。

在实际使用中,为了全面保证消息的可靠发布,通常会同时开启publisher-confirm-type和publisher-returns,这样可以在消息发布的不同阶段都能进行有效的监控和处理,最大程度地减少消息丢失的可能性。但如果应用场景对消息路由到队列这一环节的可靠性要求不高,或者有其他方式来处理消息无法路由的情况,那么也可以仅开启publisher-confirm-type而不开启publisher-returns。

代码部分

RabbitMQ配置类

我们已经完成了springboot配置以及maven配置,但RabbitMQ配置还未进行,所以接下来我们进行RabbitMQ的配置 创建RabbitMQ配置类RabbitMQConfig

@Configuration
public class RabbitMQConfig {
    // 定义用于发送邮件通知的队列常量
    public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
    // 定义用于发送短信通知的队列常量
    public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
    // 定义用于通知的主题交换机常量
    public static final String EXCHANGE_TOPICS_INFORM="exchange_topics_inform";
    // 定义邮件通知的路由键模式,使用通配符#以匹配多个单词
    public static final String ROUTINGKEY_EMAIL="inform.#.email.#";
    // 定义短信通知的路由键模式,使用通配符#以匹配多个单词
    public static final String ROUTINGKEY_SMS="inform.#.sms.#";

    //声明交换机
    @Bean(EXCHANGE_TOPICS_INFORM)
    public Exchange EXCHANGE_TOPICS_INFORM(){
        //durable(true) 持久化,mq重启之后交换机还在
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
    }

    //声明QUEUE_INFORM_EMAIL队列
    @Bean(QUEUE_INFORM_EMAIL)
    public Queue QUEUE_INFORM_EMAIL(){
        return new Queue(QUEUE_INFORM_EMAIL);
    }
    //声明QUEUE_INFORM_SMS队列
    @Bean(QUEUE_INFORM_SMS)
    public Queue QUEUE_INFORM_SMS(){
        return new Queue(QUEUE_INFORM_SMS);
    }

    //ROUTINGKEY_EMAIL队列绑定交换机,指定routingKey
    @Bean
    public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
                                              @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
    }
    //ROUTINGKEY_SMS队列绑定交换机,指定routingKey
    @Bean
    public Binding BINDING_ROUTINGKEY_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
                                          @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
    }

    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

该配置类的作用是配置交换机以及队列,配置完后将队列绑定到交换机上并指定路由键,以及配置Jackson序列化

解读 RabbitMQ配置类代码

博主有话说

虽然代码中基本都带有配置,但是为了防止部分读者还是有部分不理解,博主接下来会对代码进行解释,对于代码的理解都是博主的个人理解,博主能力有限不保证全部正确,与读者的理解有冲突部分可以询问AI或查阅其他文献,如有错误请在评论留言,博主在此拜谢各位读者(*´∀`)~♥

ps:接下来的所有解读都是如此,如有如有错误请在评论留言,再次拜谢 (๑´ㅂ`๑)

常量定义部分

 // 定义用于发送邮件通知的队列常量
    public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
    // 定义用于发送短信通知的队列常量
    public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
    // 定义用于通知的主题交换机常量
    public static final String EXCHANGE_TOPICS_INFORM="exchange_topics_inform";
    // 定义邮件通知的路由键模式,使用通配符#以匹配多个单词
    public static final String ROUTINGKEY_EMAIL="inform.#.email.#";
    // 定义短信通知的路由键模式,使用通配符#以匹配多个单词
    public static final String ROUTINGKEY_SMS="inform.#.sms.#";

该部分代码用于定义消息队列名称以及交换机名称、路由键。在理解交换机、队列、路由键之前我们先看看RabbitMQ的架构图

在这里插入图片描述 在这里插入图片描述

  • Broker : 标识消息队列服务器实体rabbitmq-server

  • Virtual Host : 虚拟主机。标识一批交换机、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在链接时指定,RabbitMQ默认的vhost是 /。

  • Exchange: 交换器用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

  • Queue : 消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

  • Banding : 绑定,用于消息队列和交换机之间的关联。一个绑定就是基于路由键将交换机和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。

  • Channel : 信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟链接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

  • Connection : 网络连接,比如一个TCP连接。

不难看出我们可以有多个交换机以及队列,因此我们需要对每一个交换机和队列进行命名以便分清它们,这段代码就是给它们定义一个名字但是并不是给其命名,就像新生儿的父母给其起了一个名字但还没有上户口本算不上命名,而对于交换机和队列来说它们的命名时机在被创建的时候也就是接下来的代码声明部分

声明部分

//声明交换机
    @Bean(EXCHANGE_TOPICS_INFORM)
    public Exchange EXCHANGE_TOPICS_INFORM(){
        //durable(true) 持久化,mq重启之后交换机还在
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
    }

    //声明QUEUE_INFORM_EMAIL队列
    @Bean(QUEUE_INFORM_EMAIL)
    public Queue QUEUE_INFORM_EMAIL(){
        return new Queue(QUEUE_INFORM_EMAIL);
    }
    //声明QUEUE_INFORM_SMS队列
    @Bean(QUEUE_INFORM_SMS)
    public Queue QUEUE_INFORM_SMS(){
        return new Queue(QUEUE_INFORM_SMS);
    }

在这部分代码中才真正的对交换机和队列进行命名操作,而操作的代码即

@Bean(QUEUE_INFORM_EMAIL)
    public Queue QUEUE_INFORM_EMAIL(){
   	//返回一个Queue队列对象
        return new Queue(QUEUE_INFORM_EMAIL);
    }

这些方法在返回对象时通过构造方法将上面定义的名字传递,真正给 RabbitMQ 中的实体命名的,是传递给ExchangeBuilder和Queue构造函数的参数,而Bean注解中的字符用于标识该Bean的名字并非给交换机和队列命名。 这些方法的执行时机即对交换机和队列命名的时机是spring boot在将项目中的Bean实例化时执行。

绑定部分

//ROUTINGKEY_EMAIL队列绑定交换机,指定routingKey
    @Bean
    public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
                                              @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
    }
    //ROUTINGKEY_SMS队列绑定交换机,指定routingKey
    @Bean
    public Binding BINDING_ROUTINGKEY_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
                                          @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
    }

这部分代码将队列绑定在指定的交换机上并指定路由键,绑定用于指定队列接收哪个交换机的消息以及接收哪些类型消息,类似员工分配部门,交换机就是部门经理队列就是员工以后就听这位经理的。

而路由键类似经理分配任务,比如之前定义的 inform.#.email.# 就表示负责邮件信息的接收 但是需要注意的是一个队列可以绑定多个交换机,类似你是一个好牛马,公司又让你当保安又让你当清洁工(°ω°ฅ)*

而方法BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();用于声明交换机和队列进行绑定,bind方法指定要绑定的队列,to方法指定要绑定到的交换机,with方法设置路由键(匹配规则)noargs方法标识无额外参数(用于某些特殊绑定类型)

不同交换机类型的路由规则: 直连交换机(Direct):通过精确匹配路由键分发消息。 主题交换机(Topic):支持使用*(匹配一个单词)和 #(匹配零个或多个单词)的通配符。 例如:order.# 可匹配 order.create、order.paid 等。 扇形交换机(Fanout):忽略路由键,直接 >将消息广播到所有绑定队列。 头交换机(Headers):根据消息头(Headers)而非路由键匹配。

@Qualifier注解用于解决依赖注入时的歧义性问题,当存在多个同类型的候选 Bean 时,通过名称精确选择要注入的 Bean

序列化配置部分

@Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

用于配置 RabbitMQ 消息的 JSON 序列化与反序列化功能。在消息队列中,消息需要从 Java 对象转换为字节流(发送时),并在接收时从字节流还原为 Java 对象,序列化是将 Java 对象转换为字节流的过程,这样对象才能在网络中传输或存储到磁盘

而这样配置了还不行,仅仅是创建了一个这样的类,但是还没有告诉springboot在序列化和反序列化时使用这个配置而不是Java原生的序列化配置,所以我们还需要创建一个配置类RabbitTemplateConfig

@Configuration
public class RabbitTemplateConfig {
    private final RabbitTemplate rabbitTemplate;

    public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @PostConstruct
    public void init() {
        // 设置消息转换器
        rabbitTemplate.setMessageConverter(new RabbitMQConfig().jsonMessageConverter());
    }
}
代码解析

这段代码是一个 Spring 配置类,用于自定义 RabbitMQ 消息模板(RabbitTemplate)的行为。具体来说,它通过@PostConstruct注解在 Bean 初始化后设置 JSON 消息转换器,确保生产者和消费者能够自动序列化和反序列化 Java 对象。

这一部分用于通过构造方法注入一个RabbitTemplate对象

 private final RabbitTemplate rabbitTemplate;

    public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

RabbitTemplate是 Spring AMQP 提供的核心组件,用于发送和接收消息

@PostConstruct
public void init() {
    rabbitTemplate.setMessageConverter(new RabbitMQConfig().jsonMessageConverter());
}

@PostConstruct 注解表示该方法在 Bean 初始化后执行(依赖注入完成后) rabbitTemplate.setMessageConverter(new RabbitMQConfig().jsonMessageConverter()); 设置MessageConverter为 JSON 转换器,使 RabbitMQ 支持 Java 对象与 JSON 的自动转换

JSON 消息转换器的作用是当你发送Java对象时将对象序列化为json数据在网络上传递,而接收方将接收的json数据转化为对应Java对象

ps:为什么不直接写在一个配置类中?若RabbitMQConfig本身也依赖RabbitTemplate,会导致循环依赖而死锁(´⊙ω⊙`)

但AI给出了更好的配置方法

@Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                         MessageConverter jsonMessageConverter) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(jsonMessageConverter);
        return template;
    }

将上述代码直接粘贴到RabbitMQConfig配置类中可以达到同样的效果,因为原代码是在RabbitTemplate 创建后通过@PostConstruct在 Bean 初始化后执行的效果在方法中通过设置消息转换器来配置序列化使用Jackson

而AI给出的代码是在springboot实列化Bean时将创建的 MessageConverter jsonMessageConverter Bean对象注入rabbitTemplate方法中并通过自定义RabbitTemplate的创建来实现,比起原代码这样可以确保依赖正确注入,提高代码的可维护性和稳定性。

逻辑处理层(Service)

@Service
public class orderServiceImpl implements OrderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 使用 ConcurrentHashMap 并在类内部初始化
    private final ConcurrentHashMap<String, CompletableFuture<Boolean>> correlationMap = new ConcurrentHashMap<>();

    /**
     * 初始化 RabbitTemplate 的回调配置
     * 该方法在Bean创建完成后由Spring框架调用
     */
    @PostConstruct
    public void init() {
        // 设置确认回调,处理消息是否到达交换机的确认信息
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            // 获取消息ID
            String id = null;
            if (correlationData != null) {
                id = correlationData.getId();
            }else {
                System.out.println("未找到消息ID");
                return;
            }
            // 从correlationMap中移除对应的Future,并根据确认结果完成Future
            CompletableFuture<Boolean> future = correlationMap.remove(id);
            if (future != null) {
                if (ack) {
                    // 消息成功到达交换机,完成Future
                    future.complete(true);
                } else {
                    // 消息未到达交换机,使用异常完成Future
                    future.completeExceptionally(new RuntimeException("消息未到达交换机: " + cause));
                }
            }
        });

        // 设置返回回调,处理消息未被路由到队列的情况
        rabbitTemplate.setReturnsCallback(returned -> {
            // 打印未被路由的消息信息
            System.out.println("消息未被路由到队列,返回内容:" + returned.getMessage() +
                    ", 回复代码: " + returned.getReplyCode() +
                    ", 回复文本: " + returned.getReplyText() +
                    ", 交换机: " + returned.getExchange() +
                    ", 路由键: " + returned.getRoutingKey());
        });
    }

    /**
     * 发送订单消息
     *
     * @param order 订单对象,包含订单信息
     * @return 返回一个CompletableFuture,表示异步处理结果
     * @throws NotSerializableException 如果订单对象无法被序列化,则抛出此异常
     */
    @Override
    public CompletableFuture<Boolean> sentOrder(Order order) throws NotSerializableException {
        // 创建一个CompletableFuture来处理异步结果
        CompletableFuture<Boolean> resultFuture = new CompletableFuture<>();

        // 创建CorrelationData对象,用于关联消息和订单ID
        CorrelationData correlationData = new CorrelationData(order.getOrderId());

        // 存储 future 到 map 中,以便后续根据消息确认结果完成future
        correlationMap.put(order.getOrderId(), resultFuture);

        // 发送消息,包含消息确认和路由信息
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_TOPICS_INFORM,
                "inform." + order.getUser().getName() + ".sms." + order.getOrderId(),
                order,
                message -> {
                    // 设置消息的关联ID
                    message.getMessageProperties().setCorrelationId(order.getOrderId());
                    return message;
                },
                correlationData
        );
        // 添加超时处理逻辑
        resultFuture.orTimeout(5, TimeUnit.SECONDS)
                .exceptionally(ex -> {
                    // 超时后从map中移除,避免内存泄漏
                    correlationMap.remove(order.getOrderId());
                    // 记录日志,便于排查问题
                    //logger.warn("消息发送超时,订单ID: {}", order.getOrderId());
                    // 完成异常,调用方可以通过exceptionally捕获
                    throw new RuntimeException("消息发送超时", ex);
                });

        // 返回CompletableFuture对象,用于异步处理结果
        return resultFuture;
    }
}

解读Service代码

对于基本使用部分如注入部分代码不进行解读,还请原谅ρ(・ω・、)

消息发送
/**
     * 发送订单消息
     *
     * @param order 订单对象,包含订单信息
     * @return 返回一个CompletableFuture,表示异步处理结果
     * @throws NotSerializableException 如果订单对象无法被序列化,则抛出此异常
     */
    @Override
    public CompletableFuture<Boolean> sentOrder(Order order) throws NotSerializableException {
        // 创建一个CompletableFuture来处理异步结果
        CompletableFuture<Boolean> resultFuture = new CompletableFuture<>();

        // 创建CorrelationData对象,用于关联消息和订单ID
        CorrelationData correlationData = new CorrelationData(order.getOrderId());

        // 存储 future 到 map 中,以便后续根据消息确认结果完成future
        correlationMap.put(order.getOrderId(), resultFuture);

        // 发送消息,包含消息确认和路由信息
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_TOPICS_INFORM,
                "inform." + order.getUser().getName() + ".sms." + order.getOrderId(),
                order,
                message -> {
                    // 设置消息的关联ID
                    message.getMessageProperties().setCorrelationId(order.getOrderId());
                    return message;
                },
                correlationData
        );
        // 添加超时处理逻辑
        resultFuture.orTimeout(5, TimeUnit.SECONDS)
                .exceptionally(ex -> {
                    // 超时后从map中移除,避免内存泄漏
                    correlationMap.remove(order.getOrderId());
                    // 记录日志,便于排查问题
                    //logger.warn("消息发送超时,订单ID: {}", order.getOrderId());
                    // 完成异常,调用方可以通过exceptionally捕获
                    throw new RuntimeException("消息发送超时", ex);
                });

        // 返回CompletableFuture对象,用于异步处理结果
        return resultFuture;
    }
}

CompletableFuture 的作用是对消息确认结果进行异步处理和管理,CorrelationData 用于标识消息用于后续获取消息中的数据,这里使用订单ID作为唯一标识。

代码逻辑

由于CorrelationData用于标识消息的ID为订单ID所以将订单ID与创建的用于存储resultFuture 放入创建的map对象中存储以便在使用时通过订单ID拿出resultFuture 并对resultFuture进行操作

随后使用封装好的方法convertAndSend发送消息,该方法的四个参数分别为目标交换机名称,路由键,要发送的消息内容(自动序列化),用于异步确认机制,修改消息属性(如添加头部信息),唯一标识信息. 代码中的message是一个消息后处理器用于在消息发送前,将业务订单 ID 设置为消息的关联 ID,以便在消息处理的各个环节(如消费者端、监控系统)追踪该订单的处理状态且可以通过MessageProperties.getCorrelationId()快速获取订单id 随后设置超时处理逻辑,将map中的resultfuture删除避免内存泄漏

RabbitTemplate 的回调配置
方法头部分

使用@PostConstruct注解令init初始化方法在Bean初始化以后执行,而确认回调和返回回调需要依赖完全初始化后的组件,所以需要该注解

@PostConstruct
    public void init()
确认回调部分

通过rabbitTemplate.setConfirmCallback方法设置确认回调

 // 设置确认回调,处理消息是否到达交换机的确认信息
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            // 获取消息ID
            String id = null;
            if (correlationData != null) {
                id = correlationData.getId();
            }else {
                System.out.println("未找到消息ID");
                return;
            }
            // 从correlationMap中移除对应的Future,并根据确认结果完成Future
            CompletableFuture<Boolean> future = correlationMap.remove(id);
            if (future != null) {
                if (ack) {
                    // 消息成功到达交换机,完成Future
                    future.complete(true);
                } else {
                    // 消息未到达交换机,使用异常完成Future
                    future.completeExceptionally(new RuntimeException("消息未到达交换机: " + cause));
                }
            }
        });
回调方法解析

rabbitTemplate.setConfirmCallback方法是参数类型为函数式接口的实现,该接口中的三个参数correlationData,ack, cause 分别表示生产者发送消息时附带的自定义元数据、消息是否成功到达 Exchange(true= 成功,false= 失败)、当ack为false时,包含失败的具体原因(如 "Exchange not found")

其中correlationData用于通过ID标识该条信息的归属,ID在发送信息前创建correlationData时通过构造参数传递并与发送的信息一同发送以保证异步消息的跟踪

代码逻辑

这端代码的整体逻辑为当消息到达服务器实体后Broker 处理完消息并确认 Exchange 状态后触发确认回调,并返回correlationData用于找到异步消息以及是否到达交换机。

然后通过correlationData确认消息是否存在(因为所有消息应该都有correlationData标识消息)

若消息存在则获取标识ID,前面说过标识ID以及和map中存储信息的键都是订单ID,所以可以用标识ID直接获取resultFuture进行操作完成future,而remove方法会返回被删除的数据也就是之前存储的resultFuture避免了使用get方法获取后还要删除数据所以直接使用remove方法

最后对其进行再次验证是否有future存储结果,若有则根据ack通过complete手动设置future的结果 ps:还可以自动设置哦(◜◔。◔◝)

返回回调部分
        // 设置返回回调,处理消息未被路由到队列的情况
        rabbitTemplate.setReturnsCallback(returned -> {
            // 打印未被路由的消息信息
            System.out.println("消息未被路由到队列,返回内容:" + returned.getMessage() +
                    ", 回复代码: " + returned.getReplyCode() +
                    ", 回复文本: " + returned.getReplyText() +
                    ", 交换机: " + returned.getExchange() +
                    ", 路由键: " + returned.getRoutingKey());
        });
    }
代码逻辑

文中对于返回回调的处理过于简单,不过多赘述 返回回调用于消息到达交换机但为被路由到队列时触发,所以直接在setReturnsCallback方法中的函数式接口中编写异常逻辑,代码中将信息输出到控制台但实际应用中应该输出到日志问价中

关于returned

returned中有以下数据

Message message; // 被退回的原始消息 int replyCode; // 错误码(如404表示队列不存在) String replyText; // 错误描述(如"NO_ROUTE") String exchange; // 原始发送的Exchange名称 String routingKey; // 原始使用的路由键

控制器层(Controller)

@RestController
public class orderController {
    private final orderServiceImpl orderService;

    @Autowired
    public orderController(orderServiceImpl orderService) {
        this.orderService = orderService;
    }

    @RequestMapping("/testSession")
    public String testSession() {
        System.out.print("网址正确");
        return "success";
    }

    @PostMapping("/createOrder")
    public String createOrder(@RequestBody Order order) {
        System.out.println(order);
        try {
            orderService.sentOrder(order)
                    .thenAccept(result -> {
                        if (result) {
                            // 异步成功处理
                            System.out.println("消息成功发送");
                        } else {
                            // 可选失败处理
                            System.out.println("消息发送失败");
                        }
                    })
                    .exceptionally(ex -> {
                        // 异常处理
                        System.err.println("发送消息时发生错误:" + ex.getMessage());
                        return null;
                    });
            return "订单已提交,请查看日志确认结果";
        } catch (Exception e) {
            e.printStackTrace();
            return "fail";
        }
    }
}

代码解析

对于依赖注入以及测试连接部分不做解析( ˘・з・) 该部分主要解析Future 在创建订单方法中接收前端传递的订单数据并调用service层的sentOrder方法将订单发送至RabbitMQ,sentOrder 方法返回的是一个Future对象,通过链式调用thenAccept方法来判断处理结果并返回对应处理消息给前端 ps:thenAccept并不返回结果,方法中的逻辑需要自己编写

代码中使用的CompletableFuture是Java 8 引入的一个强大工具,用于处理异步编程,它扩展了 Future 接口,提供了更丰富的功能和更灵活的 API。与传统的 Future 相比,CompletableFuture 支持链式调用、组合多个异步操作、处理异常,并且可以在操作完成时自动触发后续动作,无需手动阻塞等待结果。

在代码中博主使用的CompletableFuture是布尔类型,手动设置时需布尔类型,若设置为其他类型则在设置结果时也需要该类型而thenAccept方法中的result 也是该类型 总之CompletableFuture是一个用于存储异步消息处理结果的类,在异步消息中替代Boolean类型,因为RabbitMQ是异步返回确认结果,而Boolean是同步获取,可能导致返回的结果不是RabbitMQ的确认结果而是一开始设置的Boolean值

消费者端

重新建一个spring boot模块或者项目 由于消费者端的springboot配置和maven配置相同所以就不做展示了(・ε・)

RabbitMQ配置类

在新项目中创建一个配置类RabbitmqConfig 配置代码与上诉生产者端代码几乎相同,但需要添加运行反序列化类列表配置

    //允许列表类反序列化配置
    @Bean
    public List<String> allowedClasses(){
        return Arrays.asList(
                "edu.hnuit.demo5rabbitmq.pojo.Order",
                "edu.hnuit.demo5rabbitmq.pojo.User"
        );
    }

对于从网络接收的数据RabbitMQ为安全做了限制,若不是白名单中的类不会进行反序列且会报错,所以需要添加允许反序列化配置

对于反序列的类必须与生产者端的实体类属性完全一致,我的建议是直接复制粘贴(´≖◞౪◟≖)

监听类

@Component
public class ReceiveHandler {
    //监听email队列
    @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
    public void receive_email(Object msg, Message message, Channel channel){
        System.out.println("QUEUE_INFORM_EMAIL msg"+msg);
    }
    //监听sms队列
    @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_SMS})
    public void receive_sms(Order order, Message message, Channel channel){
        System.out.println("QUEUE_INFORM_SMS msg"+order);
    }
}
代码解析

使用@RabbitListener注解通过队列名称监听指定队列,注解接收的参数为队列名称用于指定监听对象 方法中的三个参数分别表示传递的消息,完整的消息(包含消息头、消息属性、消息体也就是msg),通道 其中完整的消息用于需要消息头消息属性的场景,通道则用于手动确认消息、拒绝消息等高级操作

ps:实在没啥好说的,关于@RabbitListener以及Channel 通道的复杂使用建议去看专门分析的文章,这里不过多阐述(¯﹃¯)

运行截图

Apifox 在这里插入图片描述

生产端 在这里插入图片描述

消费端 在这里插入图片描述