在SpringBoot集成RabbitMQ中,如何对它实现自定义的操作?

210 阅读8分钟

上次我们说了在windows上安装rabbitmq

小白也能搞定!手把手教你在 Windows 上安装 RabbitMQ

然后我们又学习了rabbitmq的基本名词,我们认识了rabbitmq的web管理页面

RabbitMQ的Web管理页面给我看懵了,这都什么意思啊

这次我们来说一下如何在springboot中使用rabbitmq,其实spring也出过一个spring-rabbit,博主之前也写过一篇文章 spring boot整合RabbitMQ —— 十分钟急速上手 里面用到了spring-rabbit原生的注解,例如@RabbitListener,但是因为是官方封装好的,如果我们需要实现很多自定义的功能,官方的就不能满足我们了

所以我们这次在需要学习的是在springboot中使用自定义的rabbitmq,例如一个队列自动绑定路由key,发送的时候只发送到routingKey时自动转发到队列上等。并且结合这篇文章学习rabbitmq的web上面的内容,看看他们是怎么一一对应上的。

消费者

第一步,创建消费者和生产者服务

消息队列首先重要的是有一个消费者和生产者,所以我们需要创建对应的两个服务

image-20241011201751269

第二步,导入jar包

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.14.3</version>
</dependency>

<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>2.4.17</version>
</dependency>

这里我们主要导入两个依赖

RabbitMQ客户端(com.rabbitmq:amqp-client):用于与RabbitMQ服务器进行通信。 Spring Rabbit(org.springframework.amqp:spring-rabbit):提供了Spring框架对RabbitMQ的支持,简化了消息队列的使用。

第三步,创建一个抽象监听类

package com.masiyi.consumer.listener;

import org.springframework.amqp.core.MessageListener;

/**
 * @Author: masiyi
 * @Date: 2024/9/8
 * @Describe:
 */
public abstract class AbstractListener implements MessageListener{



    /**
     * 获取路由键
     * @return
     */
    public abstract String[] getRoutingKeys();
}

定义了一个抽象监听器AbstractListener,继承自MessageListener接口。该类包含一个抽象方法getRoutingKeys(),用于获取一个或多个路由键(字符串数组)。MessageListener 是 Spring AMQP 框架中的接口,主要用于处理从消息队列接收到的消息。实现这个接口可以定义消息的消费逻辑。当消息到达时,框架会自动调用实现类中的 onMessage 方法来处理消息。

也就是说有了这个抽象类之后,如果我们要新建一个监听消费类,我们都需要实现这个抽象类,并且写入我们的路由键以此来分发到对应的队列中去

第四步,创建消费者监听类

package com.masiyi.consumer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author: masiyi
 * @Date: 2024/9/8
 * @Describe:
 */
@Component
public class Test1Listener extends AbstractListener {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Override
    public void onMessage(Message message) {

        System.out.println("AbstractListener onMessage:getBody" + new String(message.getBody()));
        System.out.println("AbstractListener onMessage:getReceivedRoutingKey" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("AbstractListener onMessage:" + message);

    }
    /**
     * 获取路由键
     *
     * @return
     */
    @Override
    public String[] getRoutingKeys() {
        return new String[]{RoutingKey};
    }

}

这里的作用是我们创建了一个Listener的消费者Test1Listener,并且绑定了两个路由key,也就是说后面的消息如果他们的RoutingKey为:RoutingKey,就会调用我们的 onMessage方法

第五步,绑定RoutingKey到队列中去

package com.masiyi.consumer.config;

import com.masiyi.consumer.listener.AbstractListener;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.Map;
import java.util.function.BiConsumer;

/**
 * @Author: masiyi
 * @Date: 2024/9/4
 * @Describe:
 */
@Configuration
public class RabbitMqConfig {

    @Autowired
    private Map<String, AbstractListener> listenerMap;

    @Autowired
    public ConnectionFactory connectionFactory;
    @Autowired
    private DefaultListableBeanFactory beanFactory;
    @Autowired
    private RabbitProperties rabbitProperties;

    @Bean
    public void initConsumer() {
        BiConsumer<String, AbstractListener> consumer = this::initRabbitmqConsumer;
        for (Map.Entry<String, AbstractListener> entry : listenerMap.entrySet()) {
            String queueName = entry.getKey() + "-queue";
            AbstractListener listener = entry.getValue();
            consumer.accept(queueName, listener);
        }
    }


    private void initRabbitmqConsumer(String queueName, AbstractListener listener) {
        Queue queue = new Queue(queueName, true);
        this.beanFactory.registerSingleton(queueName, queue);
        Arrays.stream(listener.getRoutingKeys()).forEach(routingKey ->
                this.beanFactory.registerSingleton(listener + "#" + routingKey, BindingBuilder
                        .bind(queue)
                        .to(new DirectExchange("amq.direct"))
                        .with(String.valueOf(routingKey))));
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(queueName);
        container.setMessageListener(listener);
        // 设置手动确认模式
//        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);

        String containerName = listener.getClass().getSimpleName() + "Container";
        this.beanFactory.registerSingleton(containerName, container);
    }
}

1、通过initConsumer方法遍历监听器映射,为每个监听器创建队列和绑定关系。

这里我们创建(定义)了一个叫initRabbitmqConsumer的BiConsumer方法,BiConsumer方法在定义的时候并不会执行里面的代码,而是在initConsumer方法中的 consumer.accept(queueName, listener);这行代码才会执行里面的方法。

2、initRabbitmqConsumer方法创建持久化队列,并将其绑定到amq.direct交换机。

在上面的成员变量中,我们直接注入了一个map类型的集合,这里要注意的是spring会把所有AbstractListener的子类给注入进来,key是每个类的名字,value则是每个类的代理对象。

@Autowired
private Map<String, AbstractListener> listenerMap;

3、创建SimpleMessageListenerContainer实例,设置连接工厂、队列名及消息监听器,并注册到Spring容器中。

在initRabbitmqConsumer方法实际的执行中

private void initRabbitmqConsumer(String queueName, AbstractListener listener) {

这个方法接受两个参数:queueName 表示队列名称,listener 是一个实现了 AbstractListener 接口的类,它应该包含处理消息的方法。

Queue queue = new Queue(queueName, true);
this.beanFactory.registerSingleton(queueName, queue);

这里创建了一个新的队列,并注册到 Spring Bean 工厂。true 参数表示队列是持久化的。

Arrays.stream(listener.getRoutingKeys())
     .forEach(routingKey -> 
         this.beanFactory.registerSingleton(listener + "#" + routingKey, BindingBuilder
             .bind(queue)
             .to(new DirectExchange("amq.direct"))
             .with(String.valueOf(routingKey))));

这段代码遍历 listener 提供的路由键列表,并为每个路由键创建一个绑定。绑定将队列与交换机关联起来,这里使用的是默认的直接交换机 "amq.direct"。每个绑定都会注册到 Spring Bean 工厂,以 listener#routingKey 的形式命名。

SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(queueName);
container.setMessageListener(listener);

创建了一个 SimpleMessageListenerContainer 对象,它负责监听队列中的消息。设置连接工厂和队列名,并将 listener 注册为消息处理器。

String containerName = listener.getClass().getSimpleName() + "Container";
this.beanFactory.registerSingleton(containerName, container);

SimpleMessageListenerContainer 注册到 Spring Bean 工厂,以 listener 类的简短名称加上 "Container" 后缀命名。

// 设置手动确认模式
// container.setAcknowledgeMode(AcknowledgeMode.MANUAL);

注释掉了手动确认模式的设置。在手动确认模式下,消费者需要显式地确认收到的消息已被处理。如果注释去掉,那么消费者将在处理完消息后通知 RabbitMQ 服务器消息已经被处理。

这个方法的主要目的是配置一个简单的 RabbitMQ 消费者,它会监听指定队列,并且对每个路由键创建一个绑定。当消息到达时,AbstractListener 的实现将会处理这些消息。

通过这个配置类,我们就将每个监听者类创建了一个对应的queue队列,并且把 getRoutingKeys方法里面的RoutingKey值作为RoutingKey绑定到这个队列上。

第六步,创建其他的监听者消费类

package com.masiyi.consumer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author: masiyi
 * @Date: 2024/9/8
 * @Describe:
 */
@Component
public class Test2Listener extends AbstractListener {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Override
    public void onMessage(Message message) {

        System.out.println("AbstractListener onMessage:getBody" + new String(message.getBody()));
        System.out.println("AbstractListener onMessage:getReceivedRoutingKey" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("AbstractListener onMessage:" + message);

    }
    /**
     * 获取路由键
     *
     * @return
     */
    @Override
    public String[] getRoutingKeys() {
        return new String[]{"RoutingKey3","RoutingKey4"};
    }

}

这个和 [Test1Listener]类是一样的,唯一的区别就是他们绑定的RoutingKey不一样,也就是说如果发送的RoutingKey为RoutingKey3 或者 RoutingKey4则会在Test2Listener中的onMessage方法消费,对应的队列也是Test2Listener的队列

第七步,配置我们的rabbitmq的信息

在yml文件中加入下面的代码:

spring:
  rabbitmq:
    addresses: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /local

server:
  port: 8081

这是一个 YAML 格式的配置文件,用于配置 Spring Boot 应用程序中的 RabbitMQ 相关属性以及服务器端口。以下是各个部分的解释:

spring:
  rabbitmq:
    addresses: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /local

这部分配置了 RabbitMQ 的相关信息:

  • addresses: RabbitMQ 服务器地址,默认值是 localhost,表示本地主机。
  • port: RabbitMQ 服务器端口,默认值是 5672,这是 RabbitMQ 默认的端口号。
  • username: 访问 RabbitMQ 的用户名,默认值是 guest,也是 RabbitMQ 的默认用户名。
  • password: 访问 RabbitMQ 的密码,默认值是 guest,也是 RabbitMQ 的默认密码。
  • virtual-host: 虚拟主机路径,就是我们上一次创建的虚拟主机(相当于数据库的库名)。

第八步,启动消费者

我们来启动消费者,然后打开rabbitmq的web控制台

image-20241014194831047

可以看到,因为我们的RabbitMqConfig类的配置,加上我们创建了两个消费者类,所以服务在初始化的时候就创建了两个对应的队列

我们点击队列进去可以看到这个队列绑定了我们的两个路由key

image-20241014195023474

而test2Listener-queue队列也和我们想的一样,绑定了RoutingKey3 和 RoutingKey4的路由值

image-20241014195052311

生产者

第一步,创建一个接口

package com.masiyi.producer.controller;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: masiyi
 * @Date: 2024/9/21
 * @Describe:
 */
@RestController
public class TestController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping("/send")
    public String test(@RequestParam String msg) {
        rabbitTemplate.send("amq.direct", "RoutingKey3", new Message(msg.getBytes()));
        return "test";
    }
}

这个controller类,我们直接注入了RabbitTemplate类,这个类是spring-rabbit里面的类,通过这个类我们直接发送了一个路由值为 RoutingKey3的消息

第二步,启动生产者,调用send方法

image-20241014195818451

第三步,查消费者情况

image-20241014195937882

我们可以看到消费者的控制台成功打印出了消息内容和一些消息的基本情况,例如我们可以知道消息体是一个长度为 6 的字节数组,路由键为 RoutingKey3,并且消息是从 test2Listener-queue 队列中接收到的。

第四步,查看web端的情况

image-20241014200241534

我们可以看到消息成功发出,并且消息成功被消费

但是上面的Queued messages是什么东西都没有,那是因为我们消费的太快了,消息没来得及在队列里面就马上被消费了,如果我们改一下消费者的代码,让他睡一会

    @Override
    public void onMessage(Message message) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("AbstractListener onMessage:getBody" + new String(message.getBody()));
        System.out.println("AbstractListener onMessage:getReceivedRoutingKey" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("AbstractListener onMessage:" + message);

    }

这个时候就可以看到Queued messages有东西啦!!

image-20241014200501657

总结

就像我们开头说的,其实我们这篇博客可以解决一些rabbitmq的定制化的一些东西,如加日志,动态队列订阅,多队列监听,消息过滤等等,也方便我们更加理解rabbitmq里面的每一个名词的意思、更好地学习rabbitmq

在这里插入图片描述