Spring AMQP实践指南:消息发送、接收与转换器配置

本篇文章我们将深入探讨 Spring AMQP——一个强大的消息处理工具,它基于 AMQP 协议,为现代应用提供了灵活的消息队列支持。我们会从 Spring AMQP 的基础知识入手,逐步深入到其高级特性。

我们将一起学习如何配置和使用 Spring AMQP,包括消息的发送与接收,以及如何通过不同类型的交换机(如 Fanout, Direct, Topic )来实现复杂的路由逻辑。此外,我们还会探讨如何通过消息转换器优化数据传输的效率和安全性,从而构建更加健壮和高效的系统。
在这里插入图片描述

一 . 什么是 Spring AMQP ?

1.1 AMQP

AMQP (Advanced Message Queuing Protocol) , 指的是高级消息队列协议 .

那这里指的协议 , 其实就是一种规范 , 是描述应用程序之间传递业务消息的一种规范 , 那这个协议与语言和平台无关 .

在消息队列中 , 实际上就是发送和接收消息的一种协议 .

1.2 Spring AMQP

Spring AMQP 是对 AMQP 协议的一种具体实现 , 它在实现的时候还定义了一套 API 规范 , 并且提供了一套模板来发送和接收消息 .

那 Spring AMQP 包含两部分内容 :

  1. spring-amqp 是一组 API 的抽象和规范
  2. spring-rabbit 是底层的默认实现

我们可以访问 Spring AMQP 的官方地址 : https://spring.io/projects/spring-amqp

在下面 , 他介绍了 Spring AMQP 的一些特征

  • 监听器容器 , 用于异步处理入站信息
  • 提供 RabbitTemplate 可以发送和接收消息
  • 自动创建队列、交换机、自动进行绑定等内容

1.3 小结

什么是 AMQP ?

应用间消息通信的一种协议 , 与语言和平台无关

二 . Basic Queue 简单队列模型

接下来 , 我们利用 SpringAMQP 来实现 Hello World 案例中的基础消息队列功能

我们先来看一下实现的流程 :

  1. 在父工程中引入 spring-amqp 的依赖
  2. 在 publisher 服务中利用 RabbitTemplate 发送消息到 simple.queue 这个队列
  3. 在 consumer 服务中编写消费逻辑 , 绑定 simple.queue 这个队列

2.1 引入 AMQP 的依赖

我们还是沿用之前提供给大家的 mq-demo

mq-demo.zip

因为 publisher 和 customer 服务都需要用到 AMQP 依赖 , 所以我们直接将依赖添加到父工程的 mq-demo 中

<!-- AMQP 依赖, 包含 RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.2 在 publisher 中编写测试方法 , 向 simple.queue 发送信息

我们只需要在生产者模块的 application.yml 中指定 MQ 的地址 , 剩下其他的事情 Spring 都自动帮我们实现了 , 比如创建和连接队列等等 .

我们要做的只是通过 RabbitTemplate 工具类来去发送消息即可

【1】首先配置 MQ 地址 , 在 publisher 服务的 application.yml 中添加配置

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.96.128  # 主机名
    port: 5672 # 发送消息和接收消息的端口号
    virtual-host: / # 虚拟主机
    username: root # 用户名
    password: root # 密码

那这里的虚拟主机需要去 RabbitMQ 的控制中心查看当前帐户拥有哪些虚拟主机

【2】在 publisher 服务中编写测试类 SpringAmqpTest , 并利用 RabbitTemplate 工具类实现消息发送

首先 , 我们需要添加 @SpringBootTest 和 @RunWith(SpringRunner.class) 注解 , 表示这个类是 SpringBoot 的单元测试类

然后将 RabbitTemplate 工具类进行注入

之后我们就可以编写一个方法来去发送消息

我们需要指定两个信息 :

  1. 队列的名称
  2. 消息的内容

那我们就用两个变量去接收它

之后我们就可以调用 rabbitTemplate 的 convertAndSend 方法来去发送消息了

那 publisher 的代码如下 :

package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMessage() {
        // 指定队列名称
        String queueName = "simple.queue";
        // 指定消息内容
        String message = "Hello,Spring AMQP";

        // 利用 rabbitTemplate 发送消息
        // 第一个参数: 队列的名称
        // 第二个参数: 消息的内容
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

我们此时运行一下 , 然后回到 RabbitMQ 的控制页面来看一下 , 是否有新的消息加入了

2.3 在 consumer 中编写消费逻辑 , 监听 simple.queue

【1】在 consumer 服务中编写 application.yml , 添加 mq 的连接信息

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.96.128  # 主机名
    port: 5672 # 发送消息和接收消息的端口号
    virtual-host: / # 虚拟主机
    username: root # 用户名
    password: root # 密码

【2】在 comsumer 服务中新建一个类 , 编写消费逻辑

  1. 在类上面添加 @Component 注解标识当前类为一个 Bean 对象

  1. 在方法上添加 @RabbitListener(queues = “”) 注解 , 在参数中填写要获取消息的队列的名称

那 consumer 的代码如下 :

package com.example.demo.listener;


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


// 1. 添加 @Component 注解
@Component
public class SpringRabbitListener {

    // 2. 指定要获取消息的队列
    // 可以填多个队列
    @RabbitListener(queues = "simple.queue")
    public void receiveMessage(String message) { // 3. 参数用 String 类型接收消息
        // 4. 打印获取到的消息
        System.out.println("Spring 的消费者接收到的消息: [" + message + "]");
    }
}

那我们为了要测试消费者的逻辑 , 就需要运行 customer 整个模块

我们再回到 RabbitMQ 的控制后台看一下

这也就代表消息一旦被消费 , 就会从队列中被删除 , RabbitMQ 是没有消息回溯功能的

2.4 小结

Spring AMQP 如何发送消息 ?

  1. 引入 AMQP 的 starter 依赖
  2. 配置 RabbitMQ 地址
  3. 利用 RabbitTemplate 的 convertAndSend 方法发送消息 , 需要指定要发送的队列以及消息内容

Spring AMQP 如何接收消息 ?

  1. 引入 AMQP 的 starter 依赖
  2. 配置 RabbitMQ 地址
  3. 定义 consumer 类 , 添加 @Component 注解
  4. 类中声明获取消息的方法 , 在方法上添加 @RabbitListener(queues = “”) 注解 , 参数填写要获取的队列名称

注意 : 消息一旦消费 , 就会从队列中删除 , RabbitMQ 并没有消息回溯功能

三 . Work Queue 工作队列模型

注意 : 不一定就是两个消费者 , 只是举个例子

Work Queue 模型也具备消息的发送者与消息的接收者 , 但是图示中有两个消费者 , 那队列中的消息应该交给谁处理呢 ? 而且 MQ 中的消息都是读后即焚的 , 消息一旦给了消费者 1 , 消费者 2 就看不到此条消息了 , 那这样的话每个消费者只能处理一半的消息 .

假设我们一次传输过来 50 条消息 , 那如果只有一个消费者的话 , 假设这个消费者在某一个时间段只能处理 40 条消息 . 那随着时间的累计 , 队列中的消息就会越来越多 , 这就导致后续的消息添加不到队列中从而被丢弃 .

如果多引入了一个消费者 , 那每个人都能处理 40 条消息 , 一起就能处理 80 条消息 , 这样的话对付 50 条消息绰绰有余 .

所以 Work Queue 的好处就是可以提高消息的处理速度 , 避免队列消息堆积 .

那接下来我们通过一个案例来演示一下 WorkQueue

模拟 WorkQueue , 实现一个队列绑定多个消费者

那我们的基本思路如下 :

  1. 在 publisher 服务中定义测试方法 , 每秒产生 50 条消息 , 发送到 simple.queue 中
  2. 在 consumer 服务中定义两个消息监听者 , 都监听 simple.queue 队列
  3. 让消费者 1 每秒处理 50 条消息 , 消费者 2 每秒处理 10 条消息

3.1 消息发送

这次我们利用 for 循环 , 发送 50 条消息

package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void workQueue() throws InterruptedException {
        // 指定容器名称
        String queueName = "simple.queue";
        // 指定消息内容
        String message = "Hello,Spring AMQP - ";

        // 利用 for 循环发送 50 条消息
        for (int i = 0; i < 50; i++) {
            // 利用 rabbitTemplate 发送消息
            // 第一个参数: 队列的名称
            // 第二个参数: 消息的内容
            rabbitTemplate.convertAndSend(queueName, message + i);
            
            // 避免消息发送太快, 延迟一下
            // 这样的话 1s 能发送 50 条消息
            Thread.sleep(20);
        }
    }
}

3.2 消息接收

这次我们创建两个方法来作为两个消费者 , 让消费者 1 每秒能处理 50 个消息 , 让消费者 2 每秒能处理 10 个消息 .

这样的话消费者 1 + 消费者 2 每秒能处理 60 条消息 , 针对生产者的 50s 消息绰绰有余

package com.example.demo.listener;


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

import java.time.LocalDateTime;

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void workQueue1(String message) throws InterruptedException {
        System.out.println("消费者 1 接收到的消息: [" + message + "]" + LocalDateTime.now());// 可以在后面加上当前时间
        // 通过休眠指令能达到每秒处理 50 个消息
        Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void workQueue2(String message) throws InterruptedException {
        System.err.println("消费者 2 接收到的消息: [" + message + "]" + LocalDateTime.now());// 通过 err 打印更加醒目
        // 通过休眠指令能达到每秒处理 10 个消息
        Thread.sleep(200);
    }
}

那我们将生产者和消费者都运行起来

我们来看一下运行效果

我们仔细观察一下 , 50 条消息总共接收的时长为 5s

但是我们刚刚分析过了啊 , 每秒能处理 60 条数据呢 , 怎么 50 条数据处理了 5s 呢 ?

我们再来看一下消费者 1 所处理的数据 , 我们会发现 : 消费者 1 所处理的数据都是奇数数据

而消费者 2 所处理的数据都是偶数数据

我们原本以为的是能者多劳 , 消费者 1 厉害那就消费者 1 多干点 , 结果变成 50 个消息 , 两个消费者平均分了 .

那这种分配方式就没有考虑到消费者之间的能力 , 这主要是由于 RabbitMQ 的消息预取机制造成的 , 它采用的方案是将任务按照轮训的方式发送 , 并不会考虑消费者是否执行完毕 .

3.3 控制消息预取的上限

我们可以修改消费者的 application.yml 文件 , 设置 preFetch 的值 , 就可以控制预取消息的上限

logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
spring:
  rabbitmq:
    host: 192.168.96.128  # 主机名
    port: 5672 # 发送消息和接收消息的端口号
    virtual-host: / # 虚拟主机
    username: root # 用户名
    password: root # 密码
    listener:
      simple:
        prefetch: 1 # 每次只能得到一条消息, 处理完成并且返回 ACK 之后, 才能获取下一个消息

那这样的话 , 没处理完任务的消费者就不给它分配任务 , 就达到了能者多劳的效果

我们可以重新运行一下生产者与消费者 , 来看一下效果

这样的话 , 消费者 2 只处理了一小部分的消息 , 其余部分全部交给消费者 1 来去实现了 , 并且整个过程只花费了 1s

2.4 小结

Work Queue 工作模型的作用 :

  1. 多个消费者绑定到一个队列 , 同一条消息只会被一个消费者处理
  2. 通过设置 spring.rabbitmq.listener.simple.prefetch 来控制消费者预取的消息数量

四 . 发布 - 订阅模型

在之前 , 我们已经学习了简单队列模型以及工作队列模型 , 他们的共同点都是发送出来的消息只能被一个消费者消费 , 因为消息一旦消费完成 , 就会从队列中移除掉 .

而这样的特点 , 就不满足我们在最刚开始提的案例 : 当用户支付完成 , 支付服务就需要通知订单服务、仓储服务、短信服务 , 然后他们各自完成各自的业务 . 也就是说 , 我们发送出来的支付成功的消息 , 需要让这三个服务都能够获取到 , 那我们前两种模型就实现不了这样的功能了 .

所以接下来 , 我们来认识一下发布 - 订阅模型

那我们从图中可以看出 , 消费者依然正常从队列中获取消息 , 一个队列可以绑定多个消费者 .

所以我们只需要关心生产者怎么将消息传递给队列 .

在发布 - 订阅模型中 , 引入了 exchange (交换机) , 它允许将同一信息发送给多个消费者 .

那常见的 exchange 模型如下 :

  • Fanout : 广播
  • Direct : 路由
  • Topic : 话题

那要注意的是 , exchange 交换机只负责对消息进行路由 , 不负责存储消息 . 如果消息丢失 , 交换机也不负责进行存储消息 .

4.1 Fanout Exchange

Fanout Exchange 会将收到的所有消息路由给每一个绑定的队列

那接下来 , 我们不光需要考虑消息的发送与接收 , 还需要考虑怎么让交换机与队列进行绑定 .

案例 : 利用 Spring AMQP 演示 Fanout Exchange 的使用

实现思路 :

  1. 在 consumer 服务中 , 利用代码声明队列、交换机 , 并将二者绑定
  2. 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 fanout.queue1 和 fanout.queue2
  3. 在 publisher 中编写测试方法 , 向 fanout 发送消息

① 在 consumer 服务声明 Exchange、Queue、绑定关系

我们的思路是在 consumer 服务中创建一个类 , 在类上添加 @Configuration 注解表示当前类为配置类 , 并且声明交换机、队列和绑定关系对象 Binging

首先 , 我们在这个类上添加 @Configuration 注解 , 标识当前的类是一个配置类

然后我们就需要声明队列以及交换机信息了 , 我们的交换机名称叫做 fanout , 队列叫做 fanout.queue1 和 fanout.queue2 了

那接下来我们就可以将队列与交换机进行绑定了 , 我们需要将要进行绑定的队列与交换机作为参数传入进来

然后在方法中通过 BindingBuilder 工厂类进行绑定 , 先调用 bind 方法传入要绑定的队列 , 再调用 to 方法传入要绑定的交换机

package com.example.demo.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {
    // 交换机的声明
    // 返回值类型为 FanoutExchange 交换机类型
    @Bean
    public FanoutExchange fanoutExchange() {
        // 参数填写交换机的名称
        return new FanoutExchange("fanout");
    }

    // 队列的声明
    // 返回值类型为 Queue 队列类型
    // 注意选择 org.springframework.amqp.core.Queue 这个包进行导入
    @Bean
    public Queue fanoutQueue1() {
        return new Queue("fanout.queue1");
    }

    @Bean
    public Queue fanoutQueue2() {
        return new Queue("fanout.queue2");
    }

    // 将交换机与队列进行绑定
    // 返回值类型为 Binging 类型
    // 注意选择 org.springframework.amqp.core.Queue 这个包进行导入
    @Bean
    public Binding fanoutBinging1(Queue fanoutQueue1, FanoutExchange fanoutExchange) { // 将需要绑定的队列与交换机作为参数传入进来
        // 利用 BindingBuilder 工厂类的 bind 方法进行绑定
        // bind 方法传入要绑定的队列
        // to 方法传入要绑定的交换机
        return BindingBuilder
                .bind(fanoutQueue1)
                .to(fanoutExchange);
    }

    @Bean
    public Binding fanoutBinging2(Queue fanoutQueue2, FanoutExchange fanoutExchange) { // 将需要绑定的队列与交换机作为参数传入进来
        // 利用 BindingBuilder 工厂类的 bind 方法进行绑定
        // bind 方法传入要绑定的队列
        // to 方法传入要绑定的交换机
        return BindingBuilder
                .bind(fanoutQueue2)
                .to(fanoutExchange);
    }
}

接下来 , 我们运行 customer 模块

然后来到 RabbitMQ 的控制台看一眼

点击 fanout 交换机 , 我们还能看到绑定关系

② 接收消息

接下来回到 SpringRabbitListener 类 , 编写获取消息的代码

package com.example.demo.listener;


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

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "fanout.queue1")
    public void receiveFanoutMessage1(String message) {
        System.out.println("fanout.queue1 的消费者接收到的消息: [" + message + "]");
    }

    @RabbitListener(queues = "fanout.queue2")
    public void receiveFanoutMessage2(String message) {
        System.out.println("fanout.queue2 的消费者接收到的消息: [" + message + "]");
    }
}

③ 发送消息

我们回到 publisher 模块中的单元测试类

我们依然还是需要先指定交换机名称以及消息的内容

然后将消息发送给交换机 , 我们依然使用 convertAndSend 方法 , 但是这次我们传的参数有些不同

第一个参数 : 交换机名称

第二个参数 : 不认识 , 先填个 “”

第三个参数 : 消息的内容

package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.HashMap;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendFanoutExchange() {
        // 指定交换机名称
        String exchangeName = "fanout";
        // 指定消息的内容
        String message = "This is my message";

        // 向交换机发送消息
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }
}

那接下来我们就启动生产者模块和消费者模块 , 来看一下效果

小结

交换机的作用是什么 ?

  1. 接收 publisher 发送的消息
  2. 将消息按照规则路由到绑定的队列
  3. 交换机不能缓存消息 , 如果路由失败的话 , 消息就会丢失
  4. FanoutExchange 会将消息路由到每个绑定的队列

声明队列、交换机、绑定关系的 Bean 都是什么 ?

  1. 声明队列 : Queue
  2. 声明交换机 : FanoutExchange
  3. 绑定关系 : Binding

4.2 Direct Exchange

Direct Exchange 会将收到的消息根据指定的规则路由到指定的队列中 , 因此也被叫做路由模式 .

那我们的消息一般都会被路由到哪个队列中呢 ? 这里面是存在许多规则的

  1. 每一个队列都会与交换机设置一个 BindingKey (暗号)
  2. 发布者发送消息时 , 指定消息的 RoutingKey
  3. 交换机就会将消息路由到 BindingKey 和 RoutingKey 相同的队列 (对暗号)

BindingKey 一般指的是设置到队列上面的暗号 , RoutingKey 一般指的是 publisher 发送出来的暗号

既然我们的队列能够去绑定 key , 那不同的队列能否绑定相同的 key 呢 ?

也是可以的 , 一个队列在绑定交换机的时候 , 可以绑定多个 key

那此时 , queue1 和 queue2 都具有相同的 bindingKey 了 , 如果 publisher 发布的消息 key 为 red 的话 , 就会给这两个队列都发送消息

那这个时候的效果 , 其实就跟 Fanout Exchange 的效果一样了 .

接下来 , 我们就演示一下 Direct Exchange 的用法

我们接下来要实现的效果如下图

实现思路如下 :

  1. 利用 @RabbitListener 注解声明 Exchange、Queue、RoutingKey

我们之前是通过 Bean 对象来声明的 , 其实是比较麻烦的

  1. 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 direct.queue1 和 direct.queue2
  2. 在 publisher 中编写测试方法 , 向 direct 交换机发送消息

① 利用 @RabbitListener 声明交换机和队列

我们先把基本的方法编写出来

然后接下来 , 我们来编写 @RabbitListener 的参数 , 我们需要在参数中声明队列、交换机、绑定的 key

我们之前在 FanoutConfig 类中生成过一个 binding 类型的方法 , 用来进行交换机和队列的绑定

那我们其实可以直接在 @RabbitTemplate 的参数中直接通过 bindings 进行绑定

然后将 bindings 的类型作为注解 , 添加到后面

那 IDEA 又给我们了一些提示 , 我们只需要关注前三个提示即可 : 队列、交换机、绑定的 key

那按照我们刚才的方式 , 这里的写法应该是这样

// 方法名 = @返回值类型()
value = @Queue()
exchange = @Exchange()
key = {} // 返回值为 String[], 直接以数组的形式表示即可

然后他又给予了新的提示

我们在参数中继续去按照提示往下写

value = @Queue(name = "") // 返回值为 String, 直接在后边填写对应的值即可
exchange = @Exchangename(name = "")
key = {} // 返回值为 String[], 直接以数组的形式表示即可

那整个部分写完 , 就长这个样子

但是这还不够 , 我们还需要声明一下交换机的类型 , 是 Fanout Exchange 还是 Direct Exchange 呢 ?

默认情况下 , 就是 Direct Exchange 类型的 . 如果我们需要格外指定的话 , 就需要在后面添加 type 类型 .

我们可以直接填写 direct 类型 , 也可以通过枚举方式来填写

那消费者的代码就粘贴到这里了

package com.example.demo.listener;


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.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "direct", type = ExchangeTypes.DIRECT),
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String message) {
        System.out.println("direct.queue1 的消费者接收到的消息: [" + message + "]");
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String message) {
        System.out.println("direct.queue2 的消费者接收到的消息: [" + message + "]");
    }
}

接下来我们就可以启动消费者模块了

我们可以去 RabbitMQ 的控制中心看一下

我们再去 direct 交换机中看一下绑定关系

② 消息发送

我们来到 publisher 模块 , 打开单元测试类

我们还是需要先指定交换机名称与消息内容

然后接下来 , 我们向交换机中发送消息即可 , 还是指定三个参数

  1. 交换机名称
  2. 发布者的 RountingKey
  3. 容器内容
package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendDirectExchange() {
        // 指定交换机名称
        String exchangeName = "direct";
        // 指定消息的内容
        String message = "This is red color.";

        // 向交换机发送消息
        rabbitTemplate.convertAndSend(exchangeName, "red", message);
    }
}

那我们就可以依次演示一下 red、blue、yellow 这三个 RountingKey

小结

描述一下 Direct 交换机与 Fanout 交换机的差异

  1. Fanout 交换机将消息路由给每一个绑定的队列
  2. Direct 交换机根据 RountingKey 来去判断将消息路由给哪个队列
  3. 如果多个队列具有相同的 RountingKey , 则与 Fanout 相同

基于 @RabbitListener 注解声明队列和交换机有哪些常见的注解

  1. @QueueBinding
  2. @Queue
  3. @Exchange

4.3 Topic Exchange

Topic Exchange 与 Direct Exchange 类似 , 主要的区别就在于 Topic Exchange 的 Binding Key 可以是多个单词 , 单词之间以 . 进行分隔

比如 :

china.news 代表中国的新闻
china.weather 代表中国的天气
japan.news 代表日本的新闻
japan.weather 代表日本的天气

那这样的话 , 词条一多 , 就很麻烦 .

所以在队列与交换机指定 Binding Key 的时候 , 就可以使用通配符 :

  • 代表 0 个或者任意个数单词

    • 代表一个单词

那接下来我们通过例子 , 来看一下 Topic Exchange 的效果

实现的思路如下 :

  1. 在 consumer 模块中利用 @RabbitListener 声明 Exchange、Queue、RountingKey 信息
  2. 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 topic.queue1 和 topic.queue2
  3. 在 publisher 中编写测试方法 , 向 topic 交换机发送消息

那接下来 , 我们一点一点来看

① 消息接收

我们来到 consumer 模块 , 来去进行接收消息的代码的编写

我们还是需要声明 Exchange、Queue、RountingKey

那这次 RountingKey 我们就用通配符来表示

那我们可以重启一下 customer 模块 , 然后去 RabbitMQ 的控制中心看一下

② 消息发送

我们来到 publisher 模块 , 找到 SpringAmqpTest 单元测试类

然后我们进行消息的发送 , 那我们可以分别模拟 china.news、china.weather、japan.news 这三种情况

我们先把基本的代码给到大家

package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendTopicExchange() {
        // 指定交换机名称
        String exchangeName = "topic";
        // 指定消息的内容
        String message = "China is the TOP1.";

        // 向交换机发送消息
        rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
    }

}

然后接下来就可以运行了

测试 china.news 的情况

测试 china.weather 的情况

测试 japan.news 的情况

小结

描述一下 Direct 交换机与 Topic 交换机的差异

  1. Topic 交换机接受的消息 , RountingKey 必须是多个单词 , 单词之间以 . 分隔
  2. Topic 交换机与队列绑定时的 BindingKey 可以指定通配符
  3. 代表 0 个或者多个词

    • 代表 1 个词

五 . 消息转换器

我们之前所有的案例 , 发送的消息其实都是 Object 类型的 , 我们可以通过快捷键 Ctrl + P 来查看一下参数列表

那这就代表 RabbitMQ 支持传输所有类型的数据吗 ?

我们可以试验一下

5.1 消费者

我们在 consumer 模块下的 FanoutConfig 类中 , 声明一个新的队列

package com.example.demo.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {
    @Bean
    public Queue objectQueue() {
        return new Queue("object.queue");
    }
}

那队列创建成功之后 , 我们就可以去生产者部分来去发送消息了

5.2 生产者

package com.example.demo.helloworld;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.HashMap;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    // 将 RabbitTemplate 对象进行注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testObjectSend() {
        // 指定消息的内容
        // 测试其他类型的数据能否传输
        Map<String, Object> message = new HashMap<>();
        message.put("name", "张三");
        message.put("id", 1);

        // 向交换机发送消息
        rabbitTemplate.convertAndSend("object.queue", message);
    }
}

此时我们可以运行生产者与消费者 , 来看一下效果

我们去 RabbitMQ 的控制中心 , 来看一下存入的消息是什么

这是因为默认情况下 Spring 采用的序列化方式是 JDK 序列化 , 我们也可以从上面的 content_type 观察到序列化的方式 .

那使用默认的 JDK 序列化存在以下问题 :

  • 数据体积过大
  • 有安全漏洞
  • 可读性差

那所以我们就需要使用别的序列化方式来去替换掉默认的 JDK 序列化

我们先把这个错误的数据删除掉

5.3 配置 JSON 转换器

首先 , 我们需要在父工程下的 pom.xml 中添加依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

然后我们需要在生产者部分和消费者部分都去配置消息转换器 , 以此来覆盖掉默认的配置

我们在生产者和消费者的启动类中添加 @Bean 对象即可

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

然后我们在消费者模块接收一下消息

package com.example.demo.listener;


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.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "object.queue")
    public void objectQueue(Map<String, Object> message) { // 不要忘记参数类型变成了 Map
        System.out.println("获取到的数据 : [" + message + "]");
    }
}

那我们接下来 , 可以先运行一下生产者部分 , 然后去 RabbitMQ 的控制中心看一下消息的内容

生产者部分无需再进行任何改动 , 底层会自动帮我们切换到 Jackson 的方式来去序列化


本篇文章我们一起了解了Spring AMQP——这一强大的消息队列技术。我们从AMQP协议的基础出发,逐步探索了Spring AMQP的实现细节,包括其核心组件和API的使用。通过实际案例,我们学习了如何在Spring应用中配置和操作消息队列,实现了消息的有效发送与接收。

我们详细讨论了Spring AMQP支持的多种交换机类型,包括Fanout、Direct和Topic,每种类型都适用于不同的路由场景,为消息的分发提供了灵活的选择。最后,我们还了解了消息转换器的意义,它不仅优化了消息的序列化和反序列化过程,还增强了数据传输的安全性和效率。

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加勒比海涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值