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 包含两部分内容 :
- spring-amqp 是一组 API 的抽象和规范
- spring-rabbit 是底层的默认实现
我们可以访问 Spring AMQP 的官方地址 : https://spring.io/projects/spring-amqp
在下面 , 他介绍了 Spring AMQP 的一些特征
- 监听器容器 , 用于异步处理入站信息
- 提供 RabbitTemplate 可以发送和接收消息
- 自动创建队列、交换机、自动进行绑定等内容
1.3 小结
什么是 AMQP ?
应用间消息通信的一种协议 , 与语言和平台无关
二 . Basic Queue 简单队列模型
接下来 , 我们利用 SpringAMQP 来实现 Hello World 案例中的基础消息队列功能
我们先来看一下实现的流程 :
- 在父工程中引入 spring-amqp 的依赖
- 在 publisher 服务中利用 RabbitTemplate 发送消息到 simple.queue 这个队列
- 在 consumer 服务中编写消费逻辑 , 绑定 simple.queue 这个队列
2.1 引入 AMQP 的依赖
我们还是沿用之前提供给大家的 mq-demo
因为 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 工具类进行注入
之后我们就可以编写一个方法来去发送消息
我们需要指定两个信息 :
- 队列的名称
- 消息的内容
那我们就用两个变量去接收它
之后我们就可以调用 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 服务中新建一个类 , 编写消费逻辑
- 在类上面添加 @Component 注解标识当前类为一个 Bean 对象
- 在方法上添加 @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 如何发送消息 ?
- 引入 AMQP 的 starter 依赖
- 配置 RabbitMQ 地址
- 利用 RabbitTemplate 的 convertAndSend 方法发送消息 , 需要指定要发送的队列以及消息内容
Spring AMQP 如何接收消息 ?
- 引入 AMQP 的 starter 依赖
- 配置 RabbitMQ 地址
- 定义 consumer 类 , 添加 @Component 注解
- 类中声明获取消息的方法 , 在方法上添加 @RabbitListener(queues = “”) 注解 , 参数填写要获取的队列名称
注意 : 消息一旦消费 , 就会从队列中删除 , RabbitMQ 并没有消息回溯功能
三 . Work Queue 工作队列模型
注意 : 不一定就是两个消费者 , 只是举个例子
Work Queue 模型也具备消息的发送者与消息的接收者 , 但是图示中有两个消费者 , 那队列中的消息应该交给谁处理呢 ? 而且 MQ 中的消息都是读后即焚的 , 消息一旦给了消费者 1 , 消费者 2 就看不到此条消息了 , 那这样的话每个消费者只能处理一半的消息 .
假设我们一次传输过来 50 条消息 , 那如果只有一个消费者的话 , 假设这个消费者在某一个时间段只能处理 40 条消息 . 那随着时间的累计 , 队列中的消息就会越来越多 , 这就导致后续的消息添加不到队列中从而被丢弃 .
如果多引入了一个消费者 , 那每个人都能处理 40 条消息 , 一起就能处理 80 条消息 , 这样的话对付 50 条消息绰绰有余 .
所以 Work Queue 的好处就是可以提高消息的处理速度 , 避免队列消息堆积 .
那接下来我们通过一个案例来演示一下 WorkQueue
模拟 WorkQueue , 实现一个队列绑定多个消费者
那我们的基本思路如下 :
- 在 publisher 服务中定义测试方法 , 每秒产生 50 条消息 , 发送到 simple.queue 中
- 在 consumer 服务中定义两个消息监听者 , 都监听 simple.queue 队列
- 让消费者 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 工作模型的作用 :
- 多个消费者绑定到一个队列 , 同一条消息只会被一个消费者处理
- 通过设置 spring.rabbitmq.listener.simple.prefetch 来控制消费者预取的消息数量
四 . 发布 - 订阅模型
在之前 , 我们已经学习了简单队列模型以及工作队列模型 , 他们的共同点都是发送出来的消息只能被一个消费者消费 , 因为消息一旦消费完成 , 就会从队列中移除掉 .
而这样的特点 , 就不满足我们在最刚开始提的案例 : 当用户支付完成 , 支付服务就需要通知订单服务、仓储服务、短信服务 , 然后他们各自完成各自的业务 . 也就是说 , 我们发送出来的支付成功的消息 , 需要让这三个服务都能够获取到 , 那我们前两种模型就实现不了这样的功能了 .
所以接下来 , 我们来认识一下发布 - 订阅模型
那我们从图中可以看出 , 消费者依然正常从队列中获取消息 , 一个队列可以绑定多个消费者 .
所以我们只需要关心生产者怎么将消息传递给队列 .
在发布 - 订阅模型中 , 引入了 exchange (交换机) , 它允许将同一信息发送给多个消费者 .
那常见的 exchange 模型如下 :
- Fanout : 广播
- Direct : 路由
- Topic : 话题
那要注意的是 , exchange 交换机只负责对消息进行路由 , 不负责存储消息 . 如果消息丢失 , 交换机也不负责进行存储消息 .
4.1 Fanout Exchange
Fanout Exchange 会将收到的所有消息路由给每一个绑定的队列
那接下来 , 我们不光需要考虑消息的发送与接收 , 还需要考虑怎么让交换机与队列进行绑定 .
案例 : 利用 Spring AMQP 演示 Fanout Exchange 的使用
实现思路 :
- 在 consumer 服务中 , 利用代码声明队列、交换机 , 并将二者绑定
- 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 fanout.queue1 和 fanout.queue2
- 在 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);
}
}
那接下来我们就启动生产者模块和消费者模块 , 来看一下效果
小结
交换机的作用是什么 ?
- 接收 publisher 发送的消息
- 将消息按照规则路由到绑定的队列
- 交换机不能缓存消息 , 如果路由失败的话 , 消息就会丢失
- FanoutExchange 会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的 Bean 都是什么 ?
- 声明队列 : Queue
- 声明交换机 : FanoutExchange
- 绑定关系 : Binding
4.2 Direct Exchange
Direct Exchange 会将收到的消息根据指定的规则路由到指定的队列中 , 因此也被叫做路由模式 .
那我们的消息一般都会被路由到哪个队列中呢 ? 这里面是存在许多规则的
- 每一个队列都会与交换机设置一个 BindingKey (暗号)
- 发布者发送消息时 , 指定消息的 RoutingKey
- 交换机就会将消息路由到 BindingKey 和 RoutingKey 相同的队列 (对暗号)
BindingKey 一般指的是设置到队列上面的暗号 , RoutingKey 一般指的是 publisher 发送出来的暗号
既然我们的队列能够去绑定 key , 那不同的队列能否绑定相同的 key 呢 ?
也是可以的 , 一个队列在绑定交换机的时候 , 可以绑定多个 key
那此时 , queue1 和 queue2 都具有相同的 bindingKey 了 , 如果 publisher 发布的消息 key 为 red 的话 , 就会给这两个队列都发送消息
那这个时候的效果 , 其实就跟 Fanout Exchange 的效果一样了 .
接下来 , 我们就演示一下 Direct Exchange 的用法
我们接下来要实现的效果如下图
实现思路如下 :
- 利用 @RabbitListener 注解声明 Exchange、Queue、RoutingKey
我们之前是通过 Bean 对象来声明的 , 其实是比较麻烦的
- 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 direct.queue1 和 direct.queue2
- 在 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 模块 , 打开单元测试类
我们还是需要先指定交换机名称与消息内容
然后接下来 , 我们向交换机中发送消息即可 , 还是指定三个参数
- 交换机名称
- 发布者的 RountingKey
- 容器内容
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 交换机的差异
- Fanout 交换机将消息路由给每一个绑定的队列
- Direct 交换机根据 RountingKey 来去判断将消息路由给哪个队列
- 如果多个队列具有相同的 RountingKey , 则与 Fanout 相同
基于 @RabbitListener 注解声明队列和交换机有哪些常见的注解
- @QueueBinding
- @Queue
- @Exchange
4.3 Topic Exchange
Topic Exchange 与 Direct Exchange 类似 , 主要的区别就在于 Topic Exchange 的 Binding Key 可以是多个单词 , 单词之间以 .
进行分隔
比如 :
china.news 代表中国的新闻
china.weather 代表中国的天气
japan.news 代表日本的新闻
japan.weather 代表日本的天气
那这样的话 , 词条一多 , 就很麻烦 .
所以在队列与交换机指定 Binding Key 的时候 , 就可以使用通配符 :
那接下来我们通过例子 , 来看一下 Topic Exchange 的效果
实现的思路如下 :
- 在 consumer 模块中利用 @RabbitListener 声明 Exchange、Queue、RountingKey 信息
- 在 consumer 服务中 , 编写两个消费者方法 , 分别监听 topic.queue1 和 topic.queue2
- 在 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 交换机的差异
- Topic 交换机接受的消息 , RountingKey 必须是多个单词 , 单词之间以
.
分隔 - Topic 交换机与队列绑定时的 BindingKey 可以指定通配符
-
代表 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,每种类型都适用于不同的路由场景,为消息的分发提供了灵活的选择。最后,我们还了解了消息转换器的意义,它不仅优化了消息的序列化和反序列化过程,还增强了数据传输的安全性和效率。