SpringBoot整合RabbitMQ

整合背景:

  1. RabbitMQ可动态配置,系统自动初始化rabbitMQ的交换机-路由-队列信息。
  2. 满足不同业务模块使用不同类型RabbitMQ交换机的需求。
  3. 统一生产者入口。
  4. 消费者集成MessageListener事件动态监听。

一、Springboot版本和RabbitMQ依赖

XML 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId>
	<version>2.7.13</version>
</dependency>
<!--rabbitMQ-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

二、RabbitMQ动态配置文件

可根据不同的交换机,进行队列,路由的定义。也可以对队列路由进行扩充。目前这里只用到直连类型的交换机。扩充的属性可以看(三)中对应的实体类

XML 复制代码
# 通过yaml配置文件,初始化rabbitMQ的交换机-路由-队列。
# 交换机(exchange)通过路由(routing-key)来确定发送到那个队列(queue)
# 对于 Direct直连类型交换机。匹配路由的规则为全匹配。可以理解为routing-key与queue一对一
# 对于 Fanout扇形类型交换机。则直接分发给所有绑定的队列,此时可以忽略routing-key, 多用于广播消息
# 对于 Topic 主题类型交换机。根据routing-key模糊匹配队列,*匹配任意一个字符,#匹配0个或多个字符
# 在spring cloud多应用中。不同的应用可能需要不同的类型。该配置只是该应用下创建的一个或多个类型。
rabbit:
  modules:
    - exchange:
        name: default.exchange
        #type为RabbitExchangeTypeEnum枚举中的值。不配置默认为Direct
        type: DIRECT
      queue:
        name: default.queue
        arguments:
          # 队列中所有消息的最大存活时间。单位毫秒。 1分钟
          x-message-ttl: 60000
      # routing-key可以为空
      routing-key: default.queue.key

三、配置文件映射

YamlAndPropertySourceFactory.class:该类为了解决读取yaml以及yml配置文件无法映射的问题。如果可以映射配置文件,可以忽略这个

java 复制代码
/**
 * 读取rabbitMQ配置中的交换机,队列等信息
 *
 * @author byp
 * @date 2023/7/27 11:30
 */
@Component
@ConfigurationProperties(prefix = "rabbit")
@PropertySource(value = "${property.source.prefix:classpath}:rabbitmq/rabbit.yaml", factory =YamlAndPropertySourceFactory.class)
@Data
public class RabbitProperties {
    private List<RabbitModuleInfo> modules;
}

RabbitModuleInfo对象:

java 复制代码
/**
 * RabbitMQ 队列和交换机机绑定关系实体对象
 *
 * @author byp
 * @date 2023/7/27 11:31
 */
@Data
public class RabbitModuleInfo {

    /**
     * 路由Key
     */
    private String routingKey;
    /**
     * 队列信息
     */
    private Queue queue;
    /**
     * 交换机信息
     */
    private Exchange exchange;

    /**
     * 交换机信息类
     */
    @Data
    public static class Exchange {
        /**
         * 交换机类型
         * 默认直连交换机
         */
        private RabbitExchangeEnum type = RabbitExchangeEnum.DIRECT;
        /**
         * 交换机名称
         */
        private String name;
        /**
         * 是否持久化
         * 默认true持久化,重启消息不会丢失
         */
        private boolean durable = true;
        /**
         * 当所有队绑定列均不在使用时,是否自动删除交换机
         * 默认false,不自动删除
         */
        private boolean autoDelete = false;
        /**
         * 交换机其他参数
         */
        private Map<String, Object> arguments;
    }

    /**
     * 队列信息类
     */
    @Data
    public static class Queue {
        /**
         * 队列名称
         */
        private String name;
        /**
         * 是否持久化
         * 默认true持久化,重启消息不会丢失
         */
        private boolean durable = true;
        /**
         * 是否具有排他性
         * 默认false,可多个消费者消费同一个队列
         */
        private boolean exclusive = false;
        /**
         * 当消费者均断开连接,是否自动删除队列
         * 默认false,不自动删除,避免消费者断开队列丢弃消息
         */
        private boolean autoDelete = false;
        /**
         * 绑定死信队列的交换机名称
         */
        private String deadLetterExchange;
        /**
         * 绑定死信队列的路由key
         */
        private String deadLetterRoutingKey;
        private Map<String, Object> arguments;
    }

}

YamlAndPropertySourceFactory.class

java 复制代码
/**
 * 获取yaml配置
 *
 * @author byp
 * @date 2023/5/22 10:51
 */
@Configuration
public class YamlAndPropertySourceFactory extends DefaultPropertySourceFactory {

    private static final String YML_RESOURCE_FILE = ".yml";

    private static final String YAML_RESOURCE_FILE = ".yaml";

    @NotNull
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {

        Resource resourceResource = resource.getResource();
        if (Objects.requireNonNull(resourceResource.getFilename()).endsWith(YML_RESOURCE_FILE) || Objects.requireNonNull(resourceResource.getFilename()).endsWith(YAML_RESOURCE_FILE)) {
            List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resourceResource.getFilename(), resourceResource);
            return sources.get(0);
        }
        return super.createPropertySource(name, resource);
    }
}

四、初始化rabbitMQ的交换机-路由-队列

动态参数配置校验(校验可不配置):可根据不同的交换机类型,做校验,这里是对直连交换机的参数做的校验。

java 复制代码
/**
 * rabbitMQ初始化交换机,队列 容器
 *
 * @author byp
 * @date 2023/7/27 11:05
 */
@Slf4j
public class RabbitModuleInitializer implements SmartInitializingSingleton {

    AmqpAdmin amqpAdmin;
    RabbitProperties rabbitProperties;

    public RabbitModuleInitializer(AmqpAdmin amqpAdmin, RabbitProperties rabbitProperties) {
        this.amqpAdmin = amqpAdmin;
        this.rabbitProperties = rabbitProperties;
    }

    @Override
    public void afterSingletonsInstantiated() {
        log.info("RabbitMQ 动态创建和绑定队列、交换机开始");
        declareRabbitModule();
        log.info("RabbitMQ 动态创建和绑定队列、交换机完成");
    }

    /**
     * RabbitMQ 根据配置动态创建和绑定队列、交换机
     */
    private void declareRabbitModule() {
        List<RabbitModuleInfo> rabbitModuleInfos = rabbitProperties.getModules();
        if (CollectionUtils.isEmpty(rabbitModuleInfos)) {
            return;
        }
        for (RabbitModuleInfo rabbitModuleInfo : rabbitModuleInfos) {
            configParamValidate(rabbitModuleInfo);
            // 队列
            Queue queue = convertQueue(rabbitModuleInfo.getQueue());
            // 交换机
            Exchange exchange = convertExchange(rabbitModuleInfo.getExchange());
            // 绑定关系
            String routingKey = rabbitModuleInfo.getRoutingKey();
            String queueName = rabbitModuleInfo.getQueue().getName();
            String exchangeName = rabbitModuleInfo.getExchange().getName();
            Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, exchangeName, routingKey, null);
            // 创建队列
            amqpAdmin.declareQueue(queue);
            // 创建交换机
            amqpAdmin.declareExchange(exchange);
            // 队列 绑定 交换机
            amqpAdmin.declareBinding(binding);
        }
    }

    /**
     * RabbitMQ动态配置参数校验
     *
     * @param rabbitModuleInfo 队列和交换机机绑定关系实体对象
     */
    public void configParamValidate(RabbitModuleInfo rabbitModuleInfo) {
        String routingKey = rabbitModuleInfo.getRoutingKey();
        Assert.isTrue(StringUtils.isNotBlank(routingKey), "RoutingKey 未配置");
        Assert.isTrue(rabbitModuleInfo.getExchange() != null, "routingKey:{}未配置exchange", routingKey);
        Assert.isTrue(StringUtils.isNotBlank(rabbitModuleInfo.getExchange().getName()), "routingKey:{}未配置exchange的name属性", routingKey);
        Assert.isTrue(rabbitModuleInfo.getQueue() != null, "routingKey:{}未配置queue", routingKey);
        Assert.isTrue(StringUtils.isNotBlank(rabbitModuleInfo.getQueue().getName()), "routingKey:{}未配置exchange的name属性", routingKey);

    }

    /**
     * 转换生成RabbitMQ队列
     *
     * @param queue 队列
     * @return Queue
     */
    public Queue convertQueue(RabbitModuleInfo.Queue queue) {
        Map<String, Object> arguments = queue.getArguments();

        // 转换ttl的类型为long
        if (arguments != null && arguments.containsKey("x-message-ttl")) {
            arguments.put("x-message-ttl", Convert.toLong(arguments.get("x-message-ttl")));
        }
        // 是否需要绑定死信队列
        String deadLetterExchange = queue.getDeadLetterExchange();
        String deadLetterRoutingKey = queue.getDeadLetterRoutingKey();
        if (StringUtils.isNotBlank(deadLetterExchange) && StringUtils.isNotBlank(deadLetterRoutingKey)) {
            if (arguments == null) {
                arguments = new HashMap<>(4);
            }
            arguments.put("x-dead-letter-exchange", deadLetterExchange);
            arguments.put("x-dead-letter-routing-key", deadLetterRoutingKey);

        }
        return new Queue(queue.getName(), queue.isDurable(), queue.isExclusive(), queue.isAutoDelete(), arguments);
    }


    /**
     * 转换生成RabbitMQ交换机
     *
     * @param exchangeInfo 交换机信息
     * @return Exchange
     */
    public Exchange convertExchange(RabbitModuleInfo.Exchange exchangeInfo) {

        AbstractExchange exchange = null;
        RabbitExchangeEnum exchangeType = exchangeInfo.getType();
        String exchangeName = exchangeInfo.getName();
        boolean isDurable = exchangeInfo.isDurable();
        boolean isAutoDelete = exchangeInfo.isAutoDelete();

        Map<String, Object> arguments = exchangeInfo.getArguments();

        switch (exchangeType) {
            case DIRECT:
                // 直连交换机
                exchange = new DirectExchange(exchangeName, isDurable, isAutoDelete, arguments);
                break;
            case TOPIC:
                // 主题交换机
                exchange = new TopicExchange(exchangeName, isDurable, isAutoDelete, arguments);
                break;
            case FANOUT:
                //扇形交换机
                exchange = new FanoutExchange(exchangeName, isDurable, isAutoDelete, arguments);
                break;
            case HEADERS:
                // 头交换机
                exchange = new HeadersExchange(exchangeName, isDurable, isAutoDelete, arguments);
                break;
        }
        return exchange;
    }

}

五、RabbitMQ配置初始化bean以及添加动态监听

java 复制代码
/**
 * rabbitMQ配置
 *
 * @author byp
 * @date 2023/7/27 9:14
 */
@Configuration
public class RabbitConfig {

    /**
     * 通过yaml配置,创建队列、交换机初始化器
     */
    @Bean
    @ConditionalOnMissingBean
    public RabbitModuleInitializer rabbitModuleInitializer(AmqpAdmin amqpAdmin, RabbitProperties rabbitProperties) {
        return new RabbitModuleInitializer(amqpAdmin, rabbitProperties);
    }

    /**
     * 动态监听所有队列
     *
     * @param rabbitConsumerManager 用于消息监听的类
     * @param connectionFactory    MQ连接
     * @return SimpleMessageListenerContainer
     */
    @Bean
    public SimpleMessageListenerContainer mqMessageContainer(RabbitConsumerManager rabbitConsumerManager,
                                                             ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        //所有的队列
        container.setQueueNames(Objects.requireNonNull(RabbitQueueEnum.getAllQueues()));
        container.setExposeListenerChannel(true);
        //设置每个消费者获取的最大的消息数量
        container.setPrefetchCount(100);
        //消费者个数
        container.setConcurrentConsumers(1);
        //设置确认模式为手工确认
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //监听处理类
        container.setMessageListener(rabbitConsumerManager);
        return container;
    }

}

以上五个步骤就完成了根据配置文件初始化rabbitMQ的交换机-路由-队列

六、统一消息生产者入口

java 复制代码
/**
 * rabbit MQ 发送消息
 *
 * @author byp
 * @date 2023/7/27 8:58
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class RabbitProducerManager {
    private final RabbitTemplate rabbitTemplate;
    private final AmqpAdmin amqpAdmin;

    /**
     * 指定队列发送消息
     * 【注】 rabbitQueueEnum 与 rabbitRoutingEnum 的值可以考虑使用相同的,方便发送消息与接受消息。
     *
     * @param message         消息体
     * @param rabbitQueueEnum 队列枚举
     */
    public void sendMessage(RabbitQueueEnum rabbitQueueEnum, RabbitRoutingEnum rabbitRoutingEnum, String message) {
        if (!isExistQueue(rabbitQueueEnum)) {
            //可以改成没有队列的话,自动创建队列。
            throw new BusinessException("发送消息失败,队列为空");
        }
        rabbitTemplate.convertAndSend("default.exchange", rabbitRoutingEnum.getCode(), message);
        log.info("向路由:{}, 发送消息成功:{}", rabbitRoutingEnum.getCode(), message);
    }


    /**
     * 判断队列是否存在
     *
     * @param rabbitQueueEnum 队列枚举
     * @return boolean  true-存在
     */
    public boolean isExistQueue(RabbitQueueEnum rabbitQueueEnum) {
        boolean falg = true;
        if (StringUtils.isBlank(rabbitQueueEnum.getCode())) {
            throw new BusinessException("队列名称为空");
        }
        Properties queueProperties = amqpAdmin.getQueueProperties(rabbitQueueEnum.getCode());
        if (queueProperties == null) {
            falg = false;
        }
        return falg;
    }

}

七、(消费者)MessageListener动态监听

java 复制代码
/**
 * rabbitMQ 消费者
 * 不统一处理消费,各应用自己创建监听消息消费
 *
 * @author byp
 * @date 2023/7/27 9:39
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class RabbitConsumerManager implements ChannelAwareMessageListener {
    private final ApplicationEventPublisher publisher;
    @Override
    public void onMessage(Message message, Channel channel) throws IOException {
        MessageProperties messageProperties = message.getMessageProperties();
        //消息唯一标识
        long deliveryTag = messageProperties.getDeliveryTag();
        try {
            //路由
            String routingKey = messageProperties.getReceivedRoutingKey();
            //渠道
            String consumerQueue = messageProperties.getConsumerQueue();
            //消息体
            String msg = new String(message.getBody());
            log.info("RabbitConsumerManager监听到消息标识:{}, 队列:{}, 路由:{}, 的消息:{}", deliveryTag, consumerQueue,
                    routingKey, msg);
            //将消息发送到spring的事件监听中
            JSONObject json = new JSONObject();
            json.put("deliveryTag", deliveryTag);
            json.put("routingKey", routingKey);
            json.put("consumerQueue", consumerQueue);
            json.put("msg", msg);
            json.put("channel", channel);
            //这一步可以根据routingKey进行事件分类。在监听中根据不同的类进行事件处理
            publisher.publishEvent(json);
            //这里确认消息消费不合适,这里并非是真实的业务消费场景。
            //channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            channel.basicReject(deliveryTag, false);
            log.error("获取rabbitMQ消息报错");
            e.printStackTrace();
        }
    }
}

八、消息处理

java 复制代码
/**
 * rabbitMQ consumer事件监听
 *
 * @author byp
 * @date 2023/7/28 11:11
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class MessageListener {

    @EventListener
    public void handleMessage(JSONObject json) throws IOException {
        log.info("接受到消息json:{}", json);
        //标识消息在队列中的顺序
        long deliveryTag = json.getLong("deliveryTag");
        Channel channel = (Channel) json.get("channel");
        try {
            //TODO 业务逻辑
            //标记消费成功, false-只确认当前消息 ture-批量确认,当前标记之前的都确认消费
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            //拒绝消费并放入queue中,这时消息会进入循环,直到消费掉。 第二个参数表示是否放回队列中
            channel.basicReject(deliveryTag, true);
            log.error("消息确认失败,消息标识:{}", deliveryTag);
        }
    }

}
相关推荐
张某布响丸辣11 分钟前
SQL中的时间类型:深入解析与应用
java·数据库·sql·mysql·oracle
喜欢打篮球的普通人16 分钟前
rust模式和匹配
java·算法·rust
java小吕布30 分钟前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
慢生活的人。36 分钟前
SpringSecurity+jwt+captcha登录认证授权总结
java·认证·rbac·权限·验证
Goboy1 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
向阳12181 小时前
LeetCode40:组合总和II
java·算法·leetcode
云空1 小时前
《InsCode AI IDE:编程新时代的引领者》
java·javascript·c++·ide·人工智能·python·php
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:复制 PDF 文档中的页面
java·pdf·.net
Dr_eamboat1 小时前
【Java】枚举类映射
java·开发语言·python
李少兄1 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端