RabbitMQ SpringBoot消息队列与应用间通信

RabbitMQ 实战:Spring Boot 消息队列与应用间通信

上一篇已经整理过 RabbitMQ 的基础概念,所以这篇不再重复讲什么是生产者、消费者、队列、交换机,而是直接从实战角度,把 Spring Boot 版本的消息队列代码和不同项目之间通过 RabbitMQ 通信的写法梳理一遍。

本文主要包含两部分:

  1. Spring Boot 整合 RabbitMQ,实现 work、fanout、direct、topic 几种常用模式。
  2. 拆成两个 Spring Boot 项目,模拟订单系统下单后通知物流系统发货。

示例默认使用本地 RabbitMQ:

yaml 复制代码
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /

如果使用远程 RabbitMQ 服务器,把上面的连接信息替换成自己的地址、端口、用户名、密码和 virtual-host 即可。

一、Spring Boot 整合 RabbitMQ

Spring Boot 中操作 RabbitMQ 主要依赖 spring-boot-starter-amqp。它内部封装了 Spring AMQP,常用的发送工具是 RabbitTemplate,常用的消费方式是 @RabbitListener

1. 引入依赖

创建 Spring Boot 项目时,勾选 Spring Web 和 Spring for RabbitMQ,也可以手动在 pom.xml 中添加依赖:

xml 复制代码
<dependencies>
    <!-- Web 接口,方便通过浏览器或 Postman 触发发送消息 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AMQP,整合 RabbitMQ -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2. 添加 RabbitMQ 配置

application.yml

yaml 复制代码
server:
  port: 8080

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /

也可以写成地址形式:

yaml 复制代码
spring:
  rabbitmq:
    addresses: amqp://guest:guest@127.0.0.1:5672/

3. 定义常量

先把所有队列名、交换机名集中放到一个常量类中,后面的配置类、生产者、消费者都直接引用它,避免字符串写错。

java 复制代码
package com.example.rabbitmq.constant;

public class MqConstants {

    private MqConstants() {
    }

    // work 模式
    public static final String WORK_QUEUE = "work_queue";

    // fanout 发布订阅模式
    public static final String FANOUT_QUEUE1 = "fanout_queue1";
    public static final String FANOUT_QUEUE2 = "fanout_queue2";
    public static final String FANOUT_EXCHANGE = "fanout_exchange";

    // direct 路由模式
    public static final String DIRECT_QUEUE1 = "direct_queue1";
    public static final String DIRECT_QUEUE2 = "direct_queue2";
    public static final String DIRECT_EXCHANGE = "direct_exchange";

    // topic 通配符模式
    public static final String TOPIC_QUEUE1 = "topic_queue1";
    public static final String TOPIC_QUEUE2 = "topic_queue2";
    public static final String TOPIC_EXCHANGE = "topic_exchange";
}

二、Work Queues 工作队列模式

工作队列模式中,一个生产者往同一个队列发送多条消息,多个消费者监听同一个队列。多个消费者之间是竞争关系,同一条消息只会被其中一个消费者拿到。

1. 声明队列

java 复制代码
package com.example.rabbitmq.config;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WorkQueueConfig {

    @Bean
    public Queue workQueue() {
        return QueueBuilder.durable(MqConstants.WORK_QUEUE).build();
    }
}

durable 表示队列持久化,RabbitMQ 重启后队列本身还会存在。注意,队列持久化不等于消息一定不丢,消息本身也要以持久化方式发送才更稳妥。

2. 编写生产者接口

java 复制代码
package com.example.rabbitmq.controller;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/producer")
public class ProducerController {

    private final RabbitTemplate rabbitTemplate;

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

    @GetMapping("/work")
    public String work(@RequestParam(defaultValue = "10") Integer count) {
        for (int i = 1; i <= count; i++) {
            String message = "hello spring amqp: work message " + i;

            // 使用默认交换机发送消息,routingKey 写队列名
            rabbitTemplate.convertAndSend("", MqConstants.WORK_QUEUE, message);
        }
        return "work 消息发送成功,发送数量:" + count;
    }
}

默认交换机是 RabbitMQ 内置交换机,发送时第一个参数写空字符串,第二个参数 routingKey 写队列名,消息就会被投递到对应队列。

3. 编写消费者

java 复制代码
package com.example.rabbitmq.listener;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class WorkQueueListener {

    @RabbitListener(queues = MqConstants.WORK_QUEUE)
    public void listener1(Message message) {
        System.out.println("listener1 [" + MqConstants.WORK_QUEUE + "] 收到消息:"
                + new String(message.getBody()));
    }

    @RabbitListener(queues = MqConstants.WORK_QUEUE)
    public void listener2(Message message) {
        System.out.println("listener2 [" + MqConstants.WORK_QUEUE + "] 收到消息:"
                + new String(message.getBody()));
    }
}

访问:

text 复制代码
http://127.0.0.1:8080/producer/work?count=10

可以看到两组监听方法交替消费消息。重点是:同一个队列中的一条消息,不会同时被两个监听器消费。

三、Publish/Subscribe 发布订阅模式

发布订阅模式使用 FanoutExchange,也就是广播交换机。生产者把消息发送给交换机,交换机会把消息转发给所有绑定到它的队列。

它适合一条消息需要被多个业务同时处理的场景,比如订单创建后,同时通知短信服务、积分服务、优惠券服务。

1. 声明队列、交换机和绑定关系

java 复制代码
package com.example.rabbitmq.config;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {

    @Bean
    public Queue fanoutQueue1() {
        return QueueBuilder.durable(MqConstants.FANOUT_QUEUE1).build();
    }

    @Bean
    public Queue fanoutQueue2() {
        return QueueBuilder.durable(MqConstants.FANOUT_QUEUE2).build();
    }

    @Bean
    public FanoutExchange fanoutExchange() {
        return ExchangeBuilder
                .fanoutExchange(MqConstants.FANOUT_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Binding fanoutBinding1(@Qualifier("fanoutQueue1") Queue queue,
                                  @Qualifier("fanoutExchange") FanoutExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange);
    }

    @Bean
    public Binding fanoutBinding2(@Qualifier("fanoutQueue2") Queue queue,
                                  @Qualifier("fanoutExchange") FanoutExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange);
    }
}

2. 发送 fanout 消息

在前面的 ProducerController 中继续添加接口:

java 复制代码
@GetMapping("/fanout")
public String fanout() {
    rabbitTemplate.convertAndSend(
            MqConstants.FANOUT_EXCHANGE,
            "",
            "hello spring boot: fanout"
    );
    return "fanout 消息发送成功";
}

fanout 交换机不关心 routingKey,所以第二个参数可以写空字符串。

3. 编写消费者

java 复制代码
package com.example.rabbitmq.listener;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class FanoutListener {

    @RabbitListener(queues = MqConstants.FANOUT_QUEUE1)
    public void listenQueue1(String message) {
        System.out.println("[" + MqConstants.FANOUT_QUEUE1 + "] 收到消息:" + message);
    }

    @RabbitListener(queues = MqConstants.FANOUT_QUEUE2)
    public void listenQueue2(String message) {
        System.out.println("[" + MqConstants.FANOUT_QUEUE2 + "] 收到消息:" + message);
    }
}

访问:

text 复制代码
http://127.0.0.1:8080/producer/fanout

两个队列都会收到同一条消息:

text 复制代码
[fanout_queue1] 收到消息:hello spring boot: fanout
[fanout_queue2] 收到消息:hello spring boot: fanout

四、Routing 路由模式

路由模式使用 DirectExchange。它会根据消息发送时携带的 routingKey,找到绑定关系中 routingKey 完全匹配的队列。

这里用一个简单的颜色路由来演示:

text 复制代码
direct_queue1 绑定 orange
direct_queue2 绑定 black、green

发送 orange,只有队列 1 收到;发送 blackgreen,只有队列 2 收到。

1. 声明队列、交换机和绑定关系

java 复制代码
package com.example.rabbitmq.config;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {

    @Bean
    public Queue directQueue1() {
        return QueueBuilder.durable(MqConstants.DIRECT_QUEUE1).build();
    }

    @Bean
    public Queue directQueue2() {
        return QueueBuilder.durable(MqConstants.DIRECT_QUEUE2).build();
    }

    @Bean
    public DirectExchange directExchange() {
        return ExchangeBuilder
                .directExchange(MqConstants.DIRECT_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Binding directBindingOrange(@Qualifier("directQueue1") Queue queue,
                                       @Qualifier("directExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("orange");
    }

    @Bean
    public Binding directBindingBlack(@Qualifier("directQueue2") Queue queue,
                                      @Qualifier("directExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("black");
    }

    @Bean
    public Binding directBindingGreen(@Qualifier("directQueue2") Queue queue,
                                      @Qualifier("directExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("green");
    }
}

2. 发送 direct 消息

java 复制代码
@GetMapping("/direct")
public String direct(@RequestParam String routingKey) {
    rabbitTemplate.convertAndSend(
            MqConstants.DIRECT_EXCHANGE,
            routingKey,
            "hello spring boot: direct " + routingKey
    );
    return "direct 消息发送成功,routingKey:" + routingKey;
}

3. 编写消费者

java 复制代码
package com.example.rabbitmq.listener;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class DirectListener {

    @RabbitListener(queues = MqConstants.DIRECT_QUEUE1)
    public void listenQueue1(String message) {
        System.out.println("[" + MqConstants.DIRECT_QUEUE1 + "] 收到消息:" + message);
    }

    @RabbitListener(queues = MqConstants.DIRECT_QUEUE2)
    public void listenQueue2(String message) {
        System.out.println("[" + MqConstants.DIRECT_QUEUE2 + "] 收到消息:" + message);
    }
}

测试接口:

text 复制代码
http://127.0.0.1:8080/producer/direct?routingKey=orange
http://127.0.0.1:8080/producer/direct?routingKey=black
http://127.0.0.1:8080/producer/direct?routingKey=green

预期结果:

text 复制代码
[direct_queue1] 收到消息:hello spring boot: direct orange
[direct_queue2] 收到消息:hello spring boot: direct black
[direct_queue2] 收到消息:hello spring boot: direct green

如果发送一个没有绑定过的 routingKey,比如 blue,消息不会进入这两个队列。

五、Topics 通配符模式

通配符模式使用 TopicExchange。它和 direct 模式很像,区别是 direct 要求 routingKey 完全相等,而 topic 支持通配符。

常用通配符:

text 复制代码
*  表示匹配一个单词
#  表示匹配零个或多个单词

这里设计的绑定关系是:

text 复制代码
topic_queue1 绑定 *.error
topic_queue2 绑定 #.info
topic_queue2 绑定 *.error

也就是说:

text 复制代码
order.error       可以匹配 *.error,所以两个队列都能收到
pay.error         可以匹配 *.error,所以两个队列都能收到
order.pay.info    可以匹配 #.info,所以 topic_queue2 能收到

1. 声明队列、交换机和绑定关系

java 复制代码
package com.example.rabbitmq.config;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TopicConfig {

    @Bean
    public Queue topicQueue1() {
        return QueueBuilder.durable(MqConstants.TOPIC_QUEUE1).build();
    }

    @Bean
    public Queue topicQueue2() {
        return QueueBuilder.durable(MqConstants.TOPIC_QUEUE2).build();
    }

    @Bean
    public TopicExchange topicExchange() {
        return ExchangeBuilder
                .topicExchange(MqConstants.TOPIC_EXCHANGE)
                .durable(true)
                .build();
    }

    @Bean
    public Binding topicBindingError1(@Qualifier("topicQueue1") Queue queue,
                                      @Qualifier("topicExchange") TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("*.error");
    }

    @Bean
    public Binding topicBindingInfo(@Qualifier("topicQueue2") Queue queue,
                                    @Qualifier("topicExchange") TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("#.info");
    }

    @Bean
    public Binding topicBindingError2(@Qualifier("topicQueue2") Queue queue,
                                      @Qualifier("topicExchange") TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("*.error");
    }
}

2. 发送 topic 消息

java 复制代码
@GetMapping("/topics")
public String topics(@RequestParam String routingKey) {
    rabbitTemplate.convertAndSend(
            MqConstants.TOPIC_EXCHANGE,
            routingKey,
            "hello spring boot: topics " + routingKey
    );
    return "topic 消息发送成功,routingKey:" + routingKey;
}

3. 编写消费者

java 复制代码
package com.example.rabbitmq.listener;

import com.example.rabbitmq.constant.MqConstants;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class TopicListener {

    @RabbitListener(queues = MqConstants.TOPIC_QUEUE1)
    public void listenQueue1(String message) {
        System.out.println("[" + MqConstants.TOPIC_QUEUE1 + "] 收到消息:" + message);
    }

    @RabbitListener(queues = MqConstants.TOPIC_QUEUE2)
    public void listenQueue2(String message) {
        System.out.println("[" + MqConstants.TOPIC_QUEUE2 + "] 收到消息:" + message);
    }
}

测试:

text 复制代码
http://127.0.0.1:8080/producer/topics?routingKey=order.error
http://127.0.0.1:8080/producer/topics?routingKey=order.pay.info

预期日志:

text 复制代码
[topic_queue1] 收到消息:hello spring boot: topics order.error
[topic_queue2] 收到消息:hello spring boot: topics order.error
[topic_queue2] 收到消息:hello spring boot: topics order.pay.info

到这里,Spring Boot 整合 RabbitMQ 的常用写法就整理完了。核心套路其实很固定:

text 复制代码
1. 引入 spring-boot-starter-amqp
2. 配置 RabbitMQ 连接信息
3. 声明 Queue、Exchange、Binding
4. 生产者使用 RabbitTemplate 发送消息
5. 消费者使用 @RabbitListener 监听队列

六、基于 Spring Boot + RabbitMQ 完成应用通信

接下来继续看应用间通信:RabbitMQ 不只是在一个项目内部传递消息,也可以作为不同应用之间的通信中间件。

这里用一个常见的业务场景来演示:

text 复制代码
用户在订单系统下单成功后,订单系统发送消息。
物流系统监听订单创建消息,收到消息后进行发货处理。

在这个场景中:

text 复制代码
order-service       订单系统,生产者
logistics-service   物流系统,消费者
RabbitMQ            中间消息队列

项目结构可以这样设计:

text 复制代码
rabbitmq-communication
├── order-service
└── logistics-service

两个项目都引入 spring-boot-starter-webspring-boot-starter-amqp

七、订单系统 order-service

订单系统负责模拟下单。下单成功之后,它不直接调用物流系统接口,而是往 RabbitMQ 的 order.create 队列发送一条消息。

1. 配置文件

order-service/src/main/resources/application.properties

properties 复制代码
server.port=8080

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/

也可以使用地址形式:

properties 复制代码
spring.rabbitmq.addresses=amqp://guest:guest@127.0.0.1:5672/

2. 声明订单创建队列

java 复制代码
package com.example.order.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

    public static final String ORDER_CREATE_QUEUE = "order.create";

    @Bean
    public Queue orderCreateQueue() {
        return QueueBuilder.durable(ORDER_CREATE_QUEUE).build();
    }
}

这里使用默认交换机,所以只需要声明队列即可。后面发送消息时,routingKey 直接写队列名 order.create

3. 编写下单接口

java 复制代码
package com.example.order.controller;

import com.example.order.config.RabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/order")
public class OrderController {

    private final RabbitTemplate rabbitTemplate;

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

    @GetMapping("/createOrder")
    public String createOrder() {
        // 真实业务中,这里会有参数校验、库存校验、保存订单等操作
        String orderId = UUID.randomUUID().toString();

        String message = "下单成功,订单ID:" + orderId;

        // 使用默认交换机发送消息,routingKey 为队列名
        rabbitTemplate.convertAndSend("", RabbitConfig.ORDER_CREATE_QUEUE, message);

        return "下单成功,订单ID:" + orderId;
    }
}

访问订单系统接口:

text 复制代码
http://127.0.0.1:8080/order/createOrder

只启动订单系统时,可以在 RabbitMQ 管理页面看到 order.create 队列中有消息堆积。等物流系统启动后,消息就会被消费。

八、物流系统 logistics-service

物流系统不需要知道订单系统的接口地址,也不需要订单系统主动调用它。它只需要监听 RabbitMQ 中的 order.create 队列。

1. 配置文件

logistics-service/src/main/resources/application.properties

properties 复制代码
server.port=9090

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/

订单系统占用了 8080 端口,所以物流系统改为 9090。

2. 编写监听器

java 复制代码
package com.example.logistics.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class OrderCreateListener {

    @RabbitListener(queues = "order.create")
    public void listenOrderCreate(String message) {
        System.out.println("物流系统收到订单消息:" + message);

        // 真实业务中,这里可以创建物流单、分配仓库、通知配送系统等
        System.out.println("物流系统开始处理发货流程");
    }
}

启动两个服务后,再访问:

text 复制代码
http://127.0.0.1:8080/order/createOrder

物流系统控制台会打印类似日志:

text 复制代码
物流系统收到订单消息:下单成功,订单ID:c0c25851-6fe0-49dd-bf93-0746677324321
物流系统开始处理发货流程

这就完成了两个不同 Spring Boot 项目之间的消息通信。

这种写法的好处是,订单系统不需要等待物流系统处理完成,下单接口可以更快返回。即使物流系统临时重启,只要队列和消息配置得当,消息也可以先留在 RabbitMQ 中,等物流系统恢复后再继续消费。

九、发送对象消息

前面的例子发送的是字符串。如果业务中想发送一个订单对象,需要考虑序列化。Spring AMQP 推荐使用 JSON 消息转换器,比如 Jackson2JsonMessageConverter

在跨项目通信中,建议把消息对象放在公共模块中,或者至少保证两个项目中的 DTO 包名和字段一致。否则消费者反序列化时可能找不到对应类型。

1. 定义订单消息对象

可以在公共模块中定义,也可以在两个项目中保持相同包名:

java 复制代码
package com.example.common.dto;

import java.io.Serializable;

public class OrderInfo implements Serializable {

    private String orderId;
    private String productName;
    private Long price;

    public OrderInfo() {
    }

    public OrderInfo(String orderId, String productName, Long price) {
        this.orderId = orderId;
        this.productName = productName;
        this.price = price;
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "OrderInfo{" +
                "orderId='" + orderId + '\'' +
                ", productName='" + productName + '\'' +
                ", price=" + price +
                '}';
    }
}

如果项目中使用 Lombok,可以简化成:

java 复制代码
package com.example.common.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderInfo implements Serializable {
    private String orderId;
    private String productName;
    private Long price;
}

2. 配置 JSON 消息转换器

订单系统和物流系统都建议添加这段配置。

java 复制代码
package com.example.order.config;

import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMessageConfig {

    @Bean
    public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                         Jackson2JsonMessageConverter messageConverter) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter);
        return rabbitTemplate;
    }

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory,
            Jackson2JsonMessageConverter messageConverter) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(messageConverter);
        return factory;
    }
}

如果在两个项目中都使用这段配置,消费者监听方法就可以直接接收 OrderInfo 对象。

3. 订单系统发送对象

java 复制代码
package com.example.order.controller;

import com.example.common.dto.OrderInfo;
import com.example.order.config.RabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/order")
public class OrderController {

    private final RabbitTemplate rabbitTemplate;

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

    @GetMapping("/createOrderObject")
    public String createOrderObject() {
        String orderId = UUID.randomUUID().toString();

        OrderInfo orderInfo = new OrderInfo(orderId, "机械键盘", 53600L);

        rabbitTemplate.convertAndSend("", RabbitConfig.ORDER_CREATE_QUEUE, orderInfo);

        return "下单成功,已发送订单对象:" + orderId;
    }
}

这里的 price 使用 Long,单位可以约定为分,这样可以避免小数金额精度问题。

4. 物流系统接收对象

java 复制代码
package com.example.logistics.listener;

import com.example.common.dto.OrderInfo;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "order.create")
public class OrderCreateObjectListener {

    @RabbitHandler
    public void handleOrderCreate(OrderInfo orderInfo) {
        System.out.println("物流系统收到订单对象:" + orderInfo);
        System.out.println("订单ID:" + orderInfo.getOrderId());
        System.out.println("商品名称:" + orderInfo.getProductName());
        System.out.println("商品金额:" + orderInfo.getPrice());
    }
}

也可以把 @RabbitListener 直接放到方法上:

java 复制代码
@Component
public class OrderCreateObjectListener {

    @RabbitListener(queues = "order.create")
    public void handleOrderCreate(OrderInfo orderInfo) {
        System.out.println("物流系统收到订单对象:" + orderInfo);
    }
}

@RabbitListener 用来声明监听哪个队列,@RabbitHandler 用来标记具体处理消息的方法。当一个监听类中有多个处理方法时,@RabbitHandler 更常用。

十、完整测试顺序

建议按照下面的顺序测试:

  1. 先启动单项目 Spring Boot 示例,访问 /producer/work,观察两个监听器竞争消费。
  2. 访问 /producer/fanout,观察两个 fanout 队列都收到同一条消息。
  3. 访问 /producer/direct?routingKey=orangeblackgreen,观察消息按 routingKey 精确路由。
  4. 访问 /producer/topics?routingKey=order.error/producer/topics?routingKey=order.pay.info,观察通配符匹配效果。
  5. 再启动 order-service,访问 /order/createOrder,先确认订单系统能发送消息。
  6. 启动 logistics-service,再次访问订单接口,观察物流系统能消费订单消息。
  7. 最后改成对象消息,配置 JSON 转换器,再测试 /order/createOrderObject

总结

Spring Boot 整合 RabbitMQ 之后,代码比原生 RabbitMQ Java API 更简洁。发送消息主要靠 RabbitTemplate,接收消息主要靠 @RabbitListener

前半部分重点是掌握不同模式的 Spring 写法:

text 复制代码
work:多个消费者监听同一个队列,同一条消息只被消费一次。
fanout:广播给绑定到交换机的所有队列。
direct:根据 routingKey 精确匹配队列。
topic:根据 routingKey 通配符匹配队列。

后半部分重点是理解应用间通信:

text 复制代码
订单系统不直接调用物流系统,而是把订单消息发送到 RabbitMQ。
物流系统监听订单队列,收到消息后处理发货。

这种方式可以降低系统之间的耦合,也能让一些不需要立即完成的业务异步执行。后续如果继续深入,还可以在这个基础上补充消息确认、失败重试、死信队列、延迟队列等高级特性。

相关推荐
过期动态1 小时前
【RabbitMQ高级篇】生产者可靠性、MQ可靠性、消费者可靠性以及延迟队列的实现
java·数据结构·分布式·算法·rabbitmq·ruby
憧憬成为java架构高手的小白1 小时前
苍穹外卖--day07(缓存商品,购物车)
java·spring boot
fengxin_rou1 小时前
【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
spring boot·后端·elasticsearch
超梦dasgg13 小时前
Java 生产环境 MQ 技术选型全解析
java·开发语言·java-rocketmq·java-rabbitmq
绝知此事14 小时前
Netty实战:从零构建高性能TCP通信服务(含心跳检测)
java·网络·spring boot·网络协议·tcp/ip
AI产品实战15 小时前
流程引擎Flowable vs Warm-Flow 选型
spring boot
Circ.17 小时前
SpringBoot 实现文件上传与下载(完整源码 + 详细教程)
java·spring boot·后端
zzqssliu17 小时前
Spring Boot + XXL-JOB 搭建淘宝代购系统任务调度中心
java·spring boot·后端
努力发光的程序员1 天前
互联网大厂Java面试故事:Spring Boot与微服务全栈技术实战问答
java·spring boot·spring cloud·微服务·kafka·hibernate·面试技巧