使用 Spring AMQP 操作 RabbitMQ

RabbitMQ 采用 Erlang 语言开发,同时具有高可用性、高可靠性、消息低延迟,支持的多种开发语言的等优点,是当前比较流行的综合性最好的消息队列。当然有些杠精肯定会拿 RocketMQ 和 Kafka 等消息队列的相关性能跟 RabbitMQ 进行对比说事儿,这里不进行评价,你们这些杠精开心就好,说服一个人没有意义,把技术学到手,能够解决工作中的实际问题才是最重要的。

RabbitMQ 的官网地址:https://www.rabbitmq.com

AMQP 全称 Advanced Message Queuing Protocol(高级消息队列协议),是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,很符合微服务中独立性的要求。Spring AMQP 是基于 AMQP 协议定义的一套 API 规范,提供了模板来发送和接收消息。默认情况下是基于 RabbitMQ 实现,能够使用 SpringBoot 进行自动装配,大大简化了程序代码的开发。

Spring AMQP 的官网地址:https://spring.io/projects/spring-amqp

本篇博客的 Demo 使用 Spring AMQP 操作 RabbitMQ,在博客最后提供源代码下载。


一、安装部署 RabbitMQ

之前的博客已经介绍了 Docker,为了简化安装过程,因此这里采用 Docker 安装启动 RabbitMQ

我安装 Docker 的虚拟机是 CentOS7,IP 地址是 192.168.216.128

在写本篇博客时,RabbitMQ 的官网上最新版本是 3.12 ,因此使用 Docker 安装的命令如下:

# 反斜杠(\)表示命令换行,因此命令太长,因此换行编写命令比较方便查看
docker run \
    -e RABBITMQ_DEFAULT_USER=jobs \
    -e RABBITMQ_DEFAULT_PASS=qwert \
    --name mq \
    --hostname mq \
    -p 15672:15672 \
    -p 5672:5672  \
    -d rabbitmq:3.12-management

e 表示环境变量,RABBITMQ_DEFAULT_USER 表示默认用户名,RABBITMQ_DEFAULT_PASS 表示默认密码

hostname 表示主机名(这个很重要,因为 RabbitMQ 使用主机名来命名节点,当进行集群部署时用以区分每个节点,当然我们现在先使用单节点部署 RabbitMQ)。

p 表示宿主机映射 Docker 容器内部的端口,这里映射出 2 个端口(如果你防火墙没关闭的话,需要开放这 2 个端口),其中 5672 是程序连接发送和接收消息的端口,15672 是可视化界面的访问端口。

rabbitmq:3.12-management 是当前最新版本的 Docker 镜像名称。

以上操作都完成后,打开浏览器访问 http://192.168.216.128:15672,即可访问到可视化界面:

image

然后输入用户名 jobs ,密码 qwert 即可登录进去。可以查看到 RabbitMQ 的客户端连接、Exchange 交换机、Queue 队列、VirtualHost 虚拟主机等信息,可以直接进行可视化界面操作,有关 RabbitMQ 的相关概念介绍,这里不做过多介绍。


二、搭建工程

搭建一个父工程,包含 2 个 SpringBoot 子工程,分别用来发送和接收消息。

image

publish_msg 是发送消息的项目工程,其发送代码都是在测试类 PublishMessageTest 中编写。

consumer_msg 是接收消息的项目工程,其接收代码都是在 SpringAmqpListener 类中编写。

publish_msg 和 consumer_msg 中的 pom 文件由于需要使用相同的依赖,因此这两个工程的 pom 中没有引入任何依赖,它们所使用的依赖都是在父工程的 pom 中引入的,父工程的完成 pom 文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>spring_amqp</artifactId>
    <version>v1.0</version>
    <modules>
        <module>publish_msg</module>
        <module>consumer_msg</module>
    </modules>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--在此主要使用 lombok 自带的 log 方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--Spring AMQP 消息队列依赖-->
        <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>
        </dependency>
        <!--发送和接收消息,默认使用 jdk 的序列化进行消息转换,
        引入该依赖,是为了配置消息转换使用 json 格式-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>
</project>

三、消息接收者

之所以先介绍消息接收者,是因为在消息接收者代码中,能够通过注解声明出 Exchange 交换机和 Queue 队列,默认情况下是持久化的,因此只要先启动一次消费者程序,在 RabbitMQ 中就能够永久存储 Exchange 和 Queue ,后续就无所谓发送者和消费者的启动顺序了。首先我们先在 application.yml 中配置好 RabittMQ 的连接信息:

spring:
  rabbitmq:
    host: 192.168.216.128
    port: 5672
    username: jobs
    password: qwert
    virtual-host: /
    listener:
      simple:
        # 每次取 1 条消息进行消费,消费成功后再去取下一条消息
        # 防止多个消费者被均匀分配消息,让消费能力强的消费者能够获取更多的消息
        prefetch: 1

这里需要特别说明的是:最好配置上 prefetch,并且给予一个合理的值。如果不配置的话,当有多个消费者时,消费同一个消息队列时,不管消费者所在的机器性能如何,都会得到均匀数量的消息分配,这显然不是我们所期望的。我们肯定期望性能好的机器上的消费者能够多消费一些消息,因此配置上 prefetch 后,每个消费者都会先取出一定数量的消息消费完成后,再去获取下一批消息,这样就能够实现根据机器性能分配消息了。

下面列出 RabbitMQ 的 5 种消息队列的监听程序代码:

package com.jobs.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;

import java.util.Map;

@Component
public class SpringAmqpListener {

    /* SimpleQueue 接收消息代码 */
    @RabbitListener(queuesToDeclare = @Queue(name = "simple.queue"))
    public void simpleQueuelistener(String msg) {
        System.out.println("接收到simple.queue的消息:" + msg);
    }

    //-------------------------

    /* WorkQueue 接收消息代码,这里写了 2 个监听程序,用以模拟 2 个消费者 */

    @RabbitListener(queuesToDeclare = @Queue(name = "work.queue"))
    public void workQueueListener1(String msg) {
        System.out.println("listener【1】接收到work.queue消息:" + msg);
    }

    @RabbitListener(queuesToDeclare = {@Queue(name = "work.queue")})
    public void workQueueListener2(String msg) {
        System.out.println("listener【2】接收到work.queue消息:" + msg);
    }

    //-------------------------

    /* fanout 接收消息代码,这里写了 2 个监听程序,用以模拟 2 个消费者 */

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue1"),
            exchange = @Exchange(name = "jobs.fanout", type = ExchangeTypes.FANOUT)
    ))
    public void listenerFanoutQueue1(String msg) {
        System.out.println("接收到fanout.queue【1】消息:" + msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "fanout.queue2"),
            exchange = @Exchange(name = "jobs.fanout", type = ExchangeTypes.FANOUT)
    ))
    public void listenerFanoutQueue2(String msg) {
        System.out.println("接收到fanout.queue【2】消息:" + msg);
    }

    //-------------------------

    /* direct 接收消息代码,这里写了 2 个监听程序,用以模拟 2 个消费者 */

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "jobs.direct", type = ExchangeTypes.DIRECT),
            key = {"big", "middle"}
    ))
    public void listenerDirectQueue1(String msg) {
        System.out.println("接收到direct.queue【1】消息:" + msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "jobs.direct", type = ExchangeTypes.DIRECT),
            key = {"small", "middle"}
    ))
    public void listenerDirectQueue2(String msg) {
        System.out.println("接收到direct.queue【2】消息:" + msg);
    }

    //-------------------------

    /* toppic 接收消息代码,这里写了 2 个监听程序,用以模拟 2 个消费者
       toppic 表达式中的占位符:# 表示一个或多个单词,* 表示一个单词 */

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "jobs.topic", type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listenerTopicQueue1(String msg) {
        System.out.println("接收到topic.queue【1】消息:" + msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "jobs.topic", type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listenerTopicQueue2(String msg) {
        System.out.println("接收到topic.queue【2】消息:" + msg);
    }

    //-------------------------

    /* 自定义接收消息代码,接收自定义对象的消息,通过配置消息转换,使用 Json 格式进行传输 */
    @RabbitListener(queuesToDeclare = @Queue(name = "object.queue"))
    public void listenerObjectQueue(Map<String, Object> msg) {
        System.out.println("接收到object.queue的消息:" + msg);
    }
}

启动消费者 SpringBoot 程序,就可以在可视化界面中,看到声明的 Exchange 和 Queue 了:

image

image


四、消息发送者

首先在 application.yml 中配置好 RabbitMQ 的连接信息:

spring:
  rabbitmq:
    host: 192.168.216.128
    port: 5672
    username: jobs
    password: qwert
    virtual-host: /

我们在测试类 PublishMessageTest 中编写为各种类型消息队列发送消息的代码:

package com.jobs.publishtest;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
public class PublishMessageTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //给 SimpleQueue 类型的消息队列发送消息
    @Test
    public void SendMsgToSimpleQueue() {
        String queueName = "simple.queue";
        String message = "hello,simple.queue";
        rabbitTemplate.convertAndSend(queueName, message);
    }

    //给 WorkQueue 类型的消息队列发送消息,
    //WorkQueue 实际上也是 SimpleQueue 类型的消息队列,只不过有多个消费者来接收消息而已
    @Test
    public void SendMessageWorkQueue() {
        String queueName = "work.queue";
        String message = "hello,work.queue__";
        for (int i = 1; i <= 20; i++) {
            rabbitTemplate.convertAndSend(queueName, message + i);
        }
    }

    //给 Fanout 类型的消息队列发送消息
    @Test
    public void sendFanoutExchange() {
        String exchangeName = "jobs.fanout";
        String message = "打雷了,下雨了,快来收衣服了";
        rabbitTemplate.convertAndSend(exchangeName, "", message);
    }

    //给 Direct 类型的消息队列发送消息
    @Test
    public void sendDirectExchange() {
        String exchangeName = "jobs.direct";
        String message1 = "hello,big";
        rabbitTemplate.convertAndSend(exchangeName, "big", message1);
        String message2 = "hello,middle";
        rabbitTemplate.convertAndSend(exchangeName, "middle", message2);
        String message3 = "hello,small";
        rabbitTemplate.convertAndSend(exchangeName, "small", message3);
    }

    //给 Topic 类型的消息队列发送消息
    @Test
    public void sendTopicExchange() {
        String exchangeName = "jobs.topic";
        String message1 = "hello,usa新闻";
        rabbitTemplate.convertAndSend(exchangeName, "usa.news", message1);
        String message2 = "hello,china新闻";
        rabbitTemplate.convertAndSend(exchangeName, "china.news", message2);
    }

    //发送自定义对象类型消息,这里的 object.queue 实际上是 SimpleQueue 类型的消息队列
    @Test
    public void sendObjectMessage() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "乔豆豆");
        map.put("age", 39);
        rabbitTemplate.convertAndSend("object.queue", map);
    }
}

最后我们启动消费者 SpringBoot 程序后,依次运行发送者 SpringBoot 程序中的测试类中的方法,查看控制台打印的内容验证运行成果。以上就是使用 Spring AMQP 操作 RabbitMQ 实现 5 种消息队列的消息发送和接收的程序代码,有兴趣的话可以上网查询一下使用原生 api 方法操作 RabbitMQ 的代码进行对比一下,可以发现 Spring AMQP 的代码要简单很多,非常容易。

本篇博客的 Demo 源代码下载地址:https://files.cnblogs.com/files/blogs/699532/spring_amqp.zip

posted @ 2023-06-23 20:48  乔京飞  阅读(11400)  评论(0编辑  收藏  举报