12 - component-rabbitmq-starter组件封装与集成

component-rabbitmq-starter组件封装

Rabbitmq :基本上所有的mq的功能都是差不多的,都是为了解决应用解耦,异步处理,流量削峰,等,基本的概念不介绍,其实原有的spring-boot-starter-amqp已经封装的很好了,但是为了项目的需要(统一交换机,队列等后缀,通统一消息体,自定义traceId,方便操作,等等)也是基于spring-boot-starter-amqp 做了进一步的封装。

组件类包结构

│  component-rabbitmq-starter
│  pom.xml
│
├─doc
│      AbstractMessageListener-Sequence.png
│      Ds-Rabbit-Diagram.png
│      README.md
│      失败重试.jpg
│      延时标识-exchange.png
│      延时标识-message-header.jpg
│      消费消息.jpg
│
└─src
    ├─main
    │  ├─java
    │  │  └─com
    │  │      └─fangsheng
    │  │          └─technology
    │  │              └─rabbitmq
    │  │                  │  MessageUtil.java
    │  │                  │
    │  │                  ├─annotation
    │  │                  │      RabbitMQListener.java
    │  │                  │
    │  │                  ├─config
    │  │                  │      RabbitAutoConfiguration.java
    │  │                  │      RabbitListenerContainerProcessor.java
    │  │                  │
    │  │                  ├─constant
    │  │                  │      RabbitMQConstant.java
    │  │                  │
    │  │                  ├─convertor
    │  │                  │      MessageConvert.java
    │  │                  │
    │  │                  ├─dto
    │  │                  │      BaseMessageDTO.java
    │  │                  │      CustomMessagePostProcessor.java
    │  │                  │      DeclareConfigDTO.java
    │  │                  │      MessageDTO.java
    │  │                  │
    │  │                  └─service
    │  │                      ├─consumer
    │  │                      │      AbstractMessageListener.java
    │  │                      │
    │  │                      ├─declare
    │  │                      │      RabbitDeclareUtil.java
    │  │                      │
    │  │                      └─producer
    │  │                              MessageProducer.java
    │  │
    │  └─resources
    │      └─META-INF
    │              spring.factories
    │
    └─test
        └─java

MessageUtil.java

package com.fangsheng.technology.rabbitmq;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;

import java.util.Objects;

/**
 * @author lht
 * @date 2023/11/13 11:21:41
 * Rabbit message 相关工具
 */
@Slf4j
public class MessageUtil {

    public static final MessageUtil INSTANCE = new MessageUtil();

    private MessageUtil() {
    }

    /**
     * 关闭 Channel , Message 非空时,先 手动Ack消息, 再关闭 channel
     * @param channel com.rabbitmq.client.Channel, Rabbit channel
     */
    public void closeChannel(Channel channel) {
        try {
            if (Objects.isNull(channel)) {
                return;
            }
            if (channel.isOpen()) {
                channel.close();
            }
            log.debug("closeChannel ");
        }
        catch (Exception e) {
            log.error("closeChannel error", e);
        }
    }

    /**
     * 手动Ack消息
     * @param channel Channel
     * @param message Message
     */
    public void manualAck(Channel channel, Message message) {
        try {
            log.debug("manualAck start ");
            MessageProperties mp = message.getMessageProperties();
            channel.basicAck(mp.getDeliveryTag(), false);
            log.debug("manualAck end ");
        }
        catch (Exception e) {
            log.error("manualAck error", e);
        }
    }

    /**
     * 手动 Nack 消息, 消息消费失败时, 可以重新进入队列, Reject received messages(拒绝接收到的信息,不处理)
     * @param channel Channel
     * @param message Message
     * @param requeue 是否重新 放入队列
     */
    public void manualNack(Channel channel, Message message, boolean requeue) {
        try {
            log.debug("manualNack requeue:{} start ", requeue);
            MessageProperties mp = message.getMessageProperties();
            channel.basicNack(mp.getDeliveryTag(), false, requeue);
            log.debug("manualNack requeue:{}  end ", requeue);
        }
        catch (Exception e) {
            log.error("manualNack error", e);
        }
    }

}

RabbitMQListener.java

package com.fangsheng.technology.rabbitmq.annotation;

import java.lang.annotation.*;

import static com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant.DEF_MAX_RETRY_COUNT;

/**
 * @author lht
 * @date 2023/11/13 10:26:47
 * RabbitMQ listener,MQ 消费队列监听者注解
 */
@Documented
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RabbitMQListener {

    /**
     * 消费的队列名称,数组
     * @return String[]
     */
    String[] queueNames();

    /**
     * 开启重试,消费失败时, 消息会重新进入队列
     * @return true: 开启重试, false: 不重试
     */
    boolean enableRetry() default true;

    /**
     * enableRetry =true, 最大重试次数, 默认为3次
     * @return 最大重试次数
     */
    long maxRetryCount() default DEF_MAX_RETRY_COUNT;

    /**
     * 开启重试时, 延时时间 基数, 默认=1,
     * delayTime = delayTime * (intervalRadix + retryCount)
     * @return 默认值 = 1
     */
    long intervalRadix() default 1L;

}

RabbitAutoConfiguration.java

package com.fangsheng.technology.rabbitmq.config;

import com.fangsheng.technology.rabbitmq.service.declare.RabbitDeclareUtil;
import com.fangsheng.technology.rabbitmq.service.producer.MessageProducer;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * @author lht
 * @date 2023/11/13 13:53:59
 */
@Configuration
public class RabbitAutoConfiguration {

    @Resource
    private CachingConnectionFactory rabbitConnectionFactory;

    @Bean
    @ConditionalOnMissingBean(value = RabbitListenerContainerProcessor.class)
    public RabbitListenerContainerProcessor dsRabbitListenerContainerProcessor() {
        return new RabbitListenerContainerProcessor();
    }

    @Bean
    public Jackson2JsonMessageConverter converter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        return converter;
    }

    @Bean(name = "dsAmqpAdmin")
    public AmqpAdmin dsAmqpAdmin() {
        return new RabbitAdmin(rabbitConnectionFactory);
    }

    @Bean(name = "dsRabbitTemplate")
    public RabbitTemplate rabbitMQTemplate() {
        RabbitTemplate dsRabbitTemplate = new RabbitTemplate(rabbitConnectionFactory);
        dsRabbitTemplate.setMessageConverter(converter());

        dsRabbitTemplate.afterPropertiesSet();
        return dsRabbitTemplate;
    }

    @Bean(name = "messageProducer")
    @ConditionalOnMissingBean(value = MessageProducer.class)
    public MessageProducer dsMessageProducer() {
        return new MessageProducer();
    }

    @Bean(name = "rabbitDeclareUtil")
    @ConditionalOnMissingBean(value = RabbitDeclareUtil.class)
    public RabbitDeclareUtil rabbitDeclareUtil() {
        return new RabbitDeclareUtil();
    }

}

RabbitListenerContainerProcessor.java

package com.fangsheng.technology.rabbitmq.config;

import com.fangsheng.technology.rabbitmq.annotation.RabbitMQListener;
import com.fangsheng.technology.rabbitmq.convertor.MessageConvert;
import com.fangsheng.technology.rabbitmq.service.consumer.AbstractMessageListener;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @author lht
 * @date 2023/11/13 11:57:21
 */
@Configuration
public class RabbitListenerContainerProcessor implements BeanPostProcessor, ApplicationContextAware {

    /**
     * MQ 消费者map key: getClass().getName(), value: RabbitListener
     */
    public static final ConcurrentMap<String, RabbitMQListener> RABBIT_LISTENER_MAP = new ConcurrentHashMap<>(128);

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = context;
    }

    @Resource
    private CachingConnectionFactory rabbitConnectionFactory;

    @Resource
    private AmqpAdmin dsAmqpAdmin;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 被RabbitListener 注解修饰的class
        Class<?> annotatedClazz = bean.getClass();

        RabbitMQListener dsRabbitListener = AnnotationUtils.findAnnotation(annotatedClazz, RabbitMQListener.class);

        // 非 RabbitMQListener 注解修饰的class, 直接返回
        if (dsRabbitListener == null) {
            return bean;
        }

        // 是否是 AbstractMessageListener 实现类
        if (AbstractMessageListener.class.isAssignableFrom(annotatedClazz)) {
            RABBIT_LISTENER_MAP.put(MessageConvert.INSTANCE.calcBeanName(bean.toString()), dsRabbitListener);
            injectToContext(annotatedClazz, bean, dsRabbitListener);
        }

        return bean;
    }




    /**
     * 创建SimpleMessageListenerContainer 并注入到Spring 中, 使用 rabbitConnectionFactory,
     * AmqpAdmin
     * @param annotatedClazz Class<?>
     * @param bean rabbitListener & AbstractMessageListener 实现类 bean
     * @param rabbitListener rabbitListener
     */
    private void injectToContext(Class<?> annotatedClazz, Object bean, RabbitMQListener rabbitListener) {
        String[] queueNames = rabbitListener.queueNames();

        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(SimpleMessageListenerContainer.class);

        builder.addPropertyValue("connectionFactory", rabbitConnectionFactory);

        // 绑定 队列名称
        builder.addPropertyValue("queueNames", queueNames);
        // 手动Ack 消息
        builder.addPropertyValue("acknowledgeMode", AcknowledgeMode.MANUAL);
        builder.addPropertyValue("amqpAdmin", dsAmqpAdmin);
        builder.addPropertyValue("messageListener", bean);
        builder.setScope(BeanDefinition.SCOPE_SINGLETON);
        builder.setLazyInit(false);

        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;

        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) configurableApplicationContext.getBeanFactory();
        //创建一个AnnotationBeanNameGenerator对象,用于生成带有注解的Bean的名称。
        AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();

        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
        //生成带有注解的Bean的名称是为了确保每个带有注解的Bean都有一个唯一的名称,以便在应用程序中进行识别和管理。
        // 通过使用`AnnotationBeanNameGenerator`类,可以根据注解和Bean定义生成一个唯一的名称,避免重复或冲突。
        // 这样可以方便地管理和访问这些带有注解的Bean,并确保它们按照预期的方式工作。
        String definitionBeanName = generator.generateBeanName(beanDefinition, registry);

        String annotatedClazzName = annotatedClazz.getSimpleName();

        // simpleMessageListenerContainer + AbstractMessageListener 实现类 simpleName
        definitionBeanName = definitionBeanName + StringUtils.capitalize(annotatedClazzName);

        registry.registerBeanDefinition(definitionBeanName, beanDefinition);
    }

}

RabbitMQConstant.java

package com.fangsheng.technology.rabbitmq.constant;

/**
 * @author lht
 * @date 2023/11/13 10:28:47
 * Rabbit 常量
 */
public class RabbitMQConstant{


    /**
     * 默认 最大重试次数=3
     */
    public static final long DEF_MAX_RETRY_COUNT = 3L;

    /**
     * 不需要重试时, retryCount = -100L
     */
    public static final long DO_NOT_RETRY_VALUE = -100L;

    /**
     * 重试 次数Key
     */
    public static final String HEADER_RETRY_KEY = "retryCount";

    /**
     * 最大重试次数
     */
    public static final String HEADER_MAX_RETRY_KEY = "maxRetryCount";

    /**
     * 消息traceId,dsMsgTraceId
     */
    public static final String HEADER_MSG_TRACE_ID_KEY = "dsMsgTraceId";

    /**
     * 死信相关 后缀
     */
    public static final String DEAD_SUFFIX = "@dead";

    /**
     * 队列后缀
     */
    public static final String QUE_SUFFIX = "@que";

    /**
     * Exchange 后缀
     */
    public static final String EXCHANGE_SUFFIX = "@exg";

    /**
     * Routing key 后缀
     */
    public static final String ROUTING_KEY_SUFFIX = "@rtk";

    /**
     * 消息最大优先级
     */
    public static final int MAX_PRIORITY = 10;

    /**
     * 延迟消息 key
     */
    public static final String HEADER_DELAY_KEY = "x-delay";

}

MessageConvert.java

package com.fangsheng.technology.rabbitmq.convertor;

import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import com.fangsheng.technology.rabbitmq.dto.MessageDTO;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.util.StringUtils;

import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * @author lht
 * @date 2023/11/13 11:16:15
 */
public class MessageConvert {

    public static final MessageConvert INSTANCE = new MessageConvert();

    private MessageConvert() {
    }

    /**
     * 从 org.springframework.amqp.core.Message 中 抽取数据,填充到 DsMessageDTO, eg:
     * exchangeName/routingKey 等
     * @param messageDto DsMessageDTO
     * @param message Message
     */
    public void assembleDsMessageDTO(MessageDTO messageDto, Message message) {
        MessageProperties mp = message.getMessageProperties();
        messageDto.setContent(new String(message.getBody(), StandardCharsets.UTF_8));
        messageDto.setHeaders(mp.getHeaders());
        messageDto.setPriority(mp.getPriority());
        messageDto.setMsgId(mp.getMessageId());

        if (!StringUtils.hasLength(messageDto.getExchangeName())) {
            messageDto.setExchangeName(mp.getReceivedExchange());
        }

        if (!StringUtils.hasLength(messageDto.getRoutingKey())) {
            messageDto.setRoutingKey(mp.getReceivedRoutingKey());
        }

    }

    /**
     * 填充 MessageProperties
     * @param messageDto DsMessageDTO
     * @param message org.springframework.amqp.core.Message
     */
    public void assembleMessageProperties(MessageDTO messageDto, Message message) {
        MessageProperties mp = message.getMessageProperties();

        mp.getHeaders().putAll(messageDto.getHeaders());

        mp.setPriority(messageDto.getPriority());

        // 覆盖DelayTime
        //mp.setDelay(NumberUtils.toInt(String.valueOf(messageDto.getDelayTime())));
        if (Objects.nonNull(messageDto.getDelayTime())) {
            mp.getHeaders().put(RabbitMQConstant.HEADER_DELAY_KEY, messageDto.getDelayTime());
        }

        mp.setContentType(MessageProperties.CONTENT_TYPE_JSON);

        mp.setContentEncoding(StandardCharsets.UTF_8.name());

        // messageId
        mp.setMessageId(messageDto.getMsgId());

        if(Objects.nonNull(messageDto.getExpiration())){
            mp.setExpiration(messageDto.getExpiration().toString());
        }

    }

    /**
     * 计算/获取 beanName, Class.toString(),
     * 截取 @之前的 beanName
     * @date 2022-08-19 11:42:48
     * @param beanName
     * @return java.lang.String
     */
    public String calcBeanName(String beanName) {

        String splitCharacter = "@";
        int splitIndex = beanName.indexOf(splitCharacter);
        // 不包含 @信息
        if(splitIndex <=0){
            return beanName;
        }
        splitIndex = splitIndex > 0 ? splitIndex : 0;
        String putName = beanName.substring(0, splitIndex);

        return putName;
    }


}

BaseMessageDTO.java

package com.fangsheng.technology.rabbitmq.dto;

import com.fangsheng.technology.dto.DTO;
import lombok.Getter;
import lombok.Setter;

/**
 * @author lht
 * @date 2023/11/13 10:32:16
 * 基础消息体
 */
@Setter
@Getter
public class BaseMessageDTO extends DTO {

    private static final long serialVersionUID = -862257913450357967L;

    /**
     * Exchange,后缀: @exg
     */
    private String exchangeName;

    /**
     * 路由Key,后缀: @rtk
     */
    private String routingKey;

    /**
     * queue,后缀: @que
     */
    private String queueName;

    /**
     * msgID,唯一一个id(重试消费时,不变)
     */
    private String msgId;

    /**
     * traceId(每一条消息,traceId都不一样)
     */
    private String traceId;

    /**
     * 1. this traceId
     *
     */
    public String getTraceId() {
        return this.traceId;
    }

    /**
     * 时间戳
     */
    private Long timeStamp;

    public void setMsgId(String msgId) {
        this.msgId = msgId;
    }

    public void setMsgId(long msgId) {
        this.msgId = String.valueOf(msgId);
    }

    @Override
    public String toString() {
        return "BaseMessageDTO{" + "exchangeName='" + exchangeName + '\'' + ", routingKey='" + routingKey + '\''
                + ", queueName='" + queueName + '\'' + ", msgId='" + msgId + '\'' + ", traceId='" + traceId + '\''
                + ", timeStamp=" + timeStamp + '}';
    }

}

CustomMessagePostProcessor.java

package com.fangsheng.technology.rabbitmq.dto;

import com.fangsheng.technology.rabbitmq.convertor.MessageConvert;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessagePostProcessor;

import java.nio.charset.StandardCharsets;

/**
 * @author lht
 * @date 2023/11/13 11:13:20
 * 发送消息之前,修改消息的前置处理器
 *
 */
public class CustomMessagePostProcessor implements MessagePostProcessor {

    private MessageDTO messageDto;

    public CustomMessagePostProcessor(MessageDTO messageDto) {
        this.messageDto = messageDto;
    }

    /**
     * Change (or replace) the message.
     * @param message the message.
     * @return the message.
     * @throws AmqpException an exception.
     */
    @Override
    public Message postProcessMessage(Message message) {
        String content = messageDto.getContent();

        if(StringUtils.isEmpty(content)){
            content = StringUtils.EMPTY;
        }

        // content --> byte
        // copy messageProperties
        message = MessageBuilder
                .withBody(content.getBytes(StandardCharsets.UTF_8))
                .andProperties(message.getMessageProperties())
                .build();

        // assemble messageDto/ messageProperties --> message
        MessageConvert.INSTANCE.assembleMessageProperties(messageDto, message);

        return message;
    }

}

DeclareConfigDTO.java

package com.fangsheng.technology.rabbitmq.dto;

import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Objects;

/**
 * @author lht
 * @date 2023/11/13 10:49:44
 * 声明/创建配置DTO
 */
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeclareConfigDTO extends BaseMessageDTO {

    private static final long serialVersionUID = 9026445298468047589L;

    /**
     * direct/topic/fanout等
     */
    private String exchangeType;

    /**
     * 死信队列 路由key
     */
    private String deadLetterRoutingKey;

    /**
     * 死信 exchange 名称, 后缀@exg@rtk
     */
    private String deadLetterExchangeName;



    private Integer ttl;

    /**
     * 是否是死信队列
     */
    private Boolean deadQueue;

    /**
     * 是否是延时队列
     */
    private Boolean delayQueue;

    public Boolean getCreateDeadExchange() {
        this.createDeadExchange = Objects.isNull(this.createDeadExchange) ? false : this.createDeadExchange;

        return createDeadExchange;
    }

    public void setCreateDeadExchange(Boolean createDeadExchange) {
        this.createDeadExchange = createDeadExchange;
    }

    /**
     * 是否需要创建 死信Exchange(可以公用一个exchange,用作死信Exchange)
     */
    private Boolean createDeadExchange;

    public String getDeadLetterRoutingKey() {
        if (StringUtils.isBlank(this.deadLetterRoutingKey)) {
            this.deadLetterRoutingKey = String.format("%s%s%s", getRoutingKey(), RabbitMQConstant.ROUTING_KEY_SUFFIX, RabbitMQConstant.DEAD_SUFFIX);
        }
        return this.deadLetterRoutingKey;
    }

    /**
     * 死信 Exchange(为空则创建默认的xxx@exg@dead)
     * @date 2022-08-18 15:08:53
     * @return java.lang.String
     */
    public String getDeadLetterExchangeName() {
        if(StringUtils.isBlank(this.deadLetterExchangeName)) {
            this.deadLetterExchangeName = String.format("%s%s%s", getExchangeName(), RabbitMQConstant.EXCHANGE_SUFFIX, RabbitMQConstant.DEAD_SUFFIX);
        }
        return deadLetterExchangeName;
    }

    public String getExchangeType() {
        return exchangeType;
    }

    public Integer getTtl() {
        ttl = Objects.isNull(ttl) ? 0 : ttl;
        ttl = Math.max(ttl, 0);
        return ttl;
    }

    public Boolean isDeadQueue() {
        this.deadQueue = Objects.isNull(this.deadQueue) ? false : this.deadQueue;
        return this.deadQueue;
    }

    public Boolean isDelayQueue() {
        this.delayQueue = Objects.isNull(this.delayQueue) ? false : this.delayQueue;
        return delayQueue;
    }

    @Override
    public String toString() {
        return "DsDeclareConfigDTO{" + "exchangeType='" + exchangeType + '\'' + ", deadLetterRoutingKey='"
                + deadLetterRoutingKey + '\'' + ", ttl=" + ttl + ", deadQueue=" + deadQueue + ", delayQueue="
                + delayQueue + '}';
    }

}

MessageDTO.java

package com.fangsheng.technology.rabbitmq.dto;

import com.fangsheng.technology.log.mdc.MdcUtil;
import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import com.fangsheng.technology.uuid.UUIDUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.MapUtils;
import org.springframework.util.StringUtils;

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

import static com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant.*;

/**
 * @author lht
 * @date 2023/11/13 10:33:47
 * MQ 的消息 DTO
 */
@Setter
@Getter
@Builder
@AllArgsConstructor
public class MessageDTO extends BaseMessageDTO {

    private static final long serialVersionUID = -7150557480074781651L;

    /**
     * 消息内容
     */
    private String content;

    /**
     * 消息Headers, Map<String, Object>
     */
    private Map<String, Object> headers;

    /**
     * 设置消息优先级
     */
    private Integer priority;

    /**
     * header: x-delay, 延迟时间
     */
    private Long delayTime;

    /**
     * 设置消息过期时间 单位:ms(毫秒)
     */
    private Long expiration;

    public MessageDTO() {
        super();
        this.setTimeStamp(System.currentTimeMillis());
    }

    /**
     * 延迟时间, 默认值 = 0
     * @date 2022-09-15 20:02:17
     * @return java.lang.Integer
     */
    public Long getDelayTime() {
        Long delay = Objects.nonNull(delayTime) ? delayTime : 0L;
        return delay;
    }

    public Map<String, Object> getHeaders() {
        this.headers = MapUtils.isNotEmpty(this.headers) ? this.headers : new HashMap<>(64);
        return this.headers;
    }

    public Integer getPriority() {
        this.priority = Objects.isNull(this.priority) ? 0 : priority;
        this.priority = this.priority <= 0L ? 0 : priority;
        return this.priority;
    }

    public void incrementPriority() {
        int priority = getPriority();
        priority = priority + 1;
        setPriority(priority);
    }

    /**
     * 消息是否可以 继续重试/处理, retryCount < maxRetryCount,可以继续处理(默认maxRetryCount=3)
     * @return true: 可以继续处理, false: 不能处理消息
     */
    public Boolean couldBeRetry() {
        boolean couldBeRetry = false;

        long retryCount = getRetryCount();
        long maxRetryCount = getMaxRetryCount();
        // 默认为 -100L(header不包含retryCount)
        if (retryCount <= 0L) {
            return true;
        }

        // 重试次数 < 最大重试次数, 不继续处理
        if (retryCount < maxRetryCount) {
            return true;
        }

        return couldBeRetry;
    }

    /**
     * 累加 retryCount,retryCount <= 0L时, retryCount = 0L, retryCount > maxRetryCount时, 不累加
     */
    public void incrementRetryCount() {
        long retryCount = getRetryCount();

        retryCount = Math.max(retryCount, 0L);
        long maxRetryCount = getMaxRetryCount();

        // >最大重试次数, 不需要 重试
        if (retryCount > maxRetryCount) {
            return;
        }

        // 需要 retry, retryCount +=1
        retryCount = retryCount + 1L;
        headers.put(HEADER_RETRY_KEY, retryCount);

        // 修改后 保存
        setHeaders(headers);
    }

    /**
     * 从 header 中获取重试次数(key:retryCount), 不包含时, retryCount = -100l,
     * DsRabbitConstant.DO_NOT_RETRY_VALUE
     * @return long
     */
    public long getRetryCount() {
        MessageDTO messageDTO = this;
        Map<String, Object> headers = messageDTO.getHeaders();

        // 不包含 retryCount,
        if (!headers.containsKey(HEADER_RETRY_KEY)) {
            return RabbitMQConstant.DO_NOT_RETRY_VALUE;
        }

        Object retryValue = headers.get(HEADER_RETRY_KEY);
        Number retry = (Number) retryValue;
        return retry.longValue();
    }

    public void setMaxRetryCount(long maxRetryCount) {
        MessageDTO messageDTO = this;
        Map<String, Object> headers = messageDTO.getHeaders();

        maxRetryCount = maxRetryCount <= 0 ? DEF_MAX_RETRY_COUNT : maxRetryCount;

        // 最大重试次数
        headers.put(HEADER_MAX_RETRY_KEY, maxRetryCount);

        // 修改后 保存
        setHeaders(headers);
    }


    /**
     * 最大重试次数, 默认为 3, 可以覆盖, header: key: maxRetryCount
     * @return def: 3
     */
    public long getMaxRetryCount() {
        MessageDTO messageDTO = this;
        Map<String, Object> headers = messageDTO.getHeaders();

        // 不包含 maxRetryCount, 默认为3
        if (!headers.containsKey(HEADER_MAX_RETRY_KEY)) {
            return DEF_MAX_RETRY_COUNT;
        }

        Object retryValue = headers.get(HEADER_MAX_RETRY_KEY);
        Number maxRetry = (Number) retryValue;
        // 最大重试次数 无效, 返回 3
        if (maxRetry.longValue() <= 0L) {
            return DEF_MAX_RETRY_COUNT;
        }
        return maxRetry.longValue();
    }

    /**
     * 处理消息的TraceId 1. 从 this 当前对象获取 2. 从 Header 可以: dsMsgTraceId 获取 3. 从 thread local mdc
     * 获取 4. 生成唯一id, 并赋值给 traceId
     */
    public String processTraceId() {
        Map<String, Object> headers = this.getHeaders();

        String dsMsgTraceId = "";

        // 1. 从 this 获取
        dsMsgTraceId = getTraceId();

        if (StringUtils.hasLength(dsMsgTraceId)) {
            return dsMsgTraceId;
        }

        // 2. 从Header 种获取
        Object traceIdObj = headers.get(HEADER_MSG_TRACE_ID_KEY);

        if (Objects.nonNull(traceIdObj)) {
            dsMsgTraceId = traceIdObj.toString();
        }

        if (StringUtils.hasLength(dsMsgTraceId)) {
            // header 的traceId, 设置到 dto中
            setTraceId(dsMsgTraceId);
            return dsMsgTraceId;
        }

        // 3. 从thread local MDC 当中获取
        dsMsgTraceId = MdcUtil.getTraceId();

        if (StringUtils.hasLength(dsMsgTraceId)) {
            // mdc 的traceId, 设置到 dto中
            setTraceId(dsMsgTraceId);
            return dsMsgTraceId;
        }

        dsMsgTraceId = UUIDUtil.getIdStrLong();

        // 无法获取到traceId时, 才从新生成/赋值
        setTraceId(dsMsgTraceId);

        return dsMsgTraceId;
    }

    /**
     * 1. set traceId 2. add to header
     */
    @Override
    public void setTraceId(String traceId) {
        super.setTraceId(traceId);
        getHeaders().put(HEADER_MSG_TRACE_ID_KEY, traceId);
    }

    @Override
    public String toString() {
        return "DsMessageDTO{" + "content='" + content + '\'' + ", msgId='" + getMsgId() + '\'' + ", headers=" + headers
                + ", priority=" + priority + ", delayTime=" + delayTime + "} ";
    }

}

AbstractMessageListener.java

package com.fangsheng.technology.rabbitmq.service.consumer;

import com.fangsheng.technology.log.mdc.MdcUtil;
import com.fangsheng.technology.rabbitmq.MessageUtil;
import com.fangsheng.technology.rabbitmq.annotation.RabbitMQListener;
import com.fangsheng.technology.rabbitmq.convertor.MessageConvert;
import com.fangsheng.technology.rabbitmq.dto.MessageDTO;
import com.fangsheng.technology.rabbitmq.service.producer.MessageProducer;
import com.fangsheng.technology.uuid.UUIDUtil;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Objects;

/**
 * @author lht
 * @date 2023/11/13 11:42:40
 * 消费者 AbstractMessageListener, 需要实现 processMessage()
 */
@Slf4j
@Component
public abstract class AbstractMessageListener implements ChannelAwareMessageListener {

    public static final String LOG_FORMAT = "%s ";

    public static final String LOG_FORMAT_ERR = "%s errors";

    /**
     * 默认延迟时间: 10秒钟
     */
    public static final long DEFAULT_DELAY_TIME = 10000L;

    @Resource
    private MessageProducer dsMessageProducer;

    @Override
    public void onMessage(Message message, @Nullable Channel channel) {
        boolean processSuccess = false;
        MessageDTO messageDto = new MessageDTO();
        // 是否处理消息,default=true
        boolean canBeProcess = true;
        try {
            log.debug(String.format(LOG_FORMAT, "onMessage start "));

            // 填充 DsMessageDTO
            MessageConvert.INSTANCE.assembleDsMessageDTO(messageDto, message);

            // 处理 traceId
            messageDto.processTraceId();

            // 设置traceId 到 mdc
            getAndSetTraceId(messageDto);

            // check 该消息是否需要继续消费处理
            // 不需要处理时,直接 return, finally 中 close/ack消息
            canBeProcess = canBeProcess(messageDto);
            log.info("onMessage canBeProcess:{}, message data:{} ", canBeProcess, messageDto);

            log.info(String.format(LOG_FORMAT, "onMessage processMessage start "));
            // 处理消息
            processSuccess = processMessage(messageDto, message, channel);
            log.info(String.format(LOG_FORMAT, "onMessage processMessage end "));
        }
        catch (Exception e) {
            processSuccess = false;
            log.error(String.format(LOG_FORMAT_ERR, "onMessage processMessage "), e);
        }
        finally {
            doFinally(canBeProcess, processSuccess, messageDto, message, channel);
            log.debug(String.format(LOG_FORMAT, "onMessage end "));

        }
    }

    private void doFinally(boolean canBeProcess, boolean processSuccess, MessageDTO messageDto, Message message,
                           Channel channel) {
        try {
            // 处理成功, 调用onSuccess
            if (processSuccess) {
                log.debug(String.format(LOG_FORMAT, "onSuccess start "));
                onSuccess(messageDto, message, channel);
                log.debug(String.format(LOG_FORMAT, "onSuccess end "));
            }
        }
        catch (Exception e) {
            processSuccess = false;
            log.error(String.format(LOG_FORMAT_ERR, "doFinally"), e);
        }
        finally {
            log.info(String.format(LOG_FORMAT + " %s", "doFinally processMessage result :", processSuccess));
            MessageConvert.INSTANCE.assembleMessageProperties(messageDto, message);

            // 处理之后, ack 消息
            MessageUtil.INSTANCE.manualAck(channel, message);

            // 处理失败/需要处理时, 重新发送消息到队列当中, retryCount/priority 累加
            if (!processSuccess) {
                // 需要处理时,再调用onFailure
                if (canBeProcess) {
                    log.debug(String.format(LOG_FORMAT, "doFinally onFailure start "));
                    // 处理失败时,回调处理
                    onFailure(messageDto, message, channel);
                    log.debug(String.format(LOG_FORMAT, "doFinally onFailure end "));
                }
            }
            MessageUtil.INSTANCE.closeChannel(channel);
        }
    }

    /**
     * 消息是否可以被消费/处理, 默认: retryCount > maxRetryCount, 则不消费消息
     * @param messageDto DsMessageDTO
     * @return true: 继续处理,false: 不处理消息
     */
    public Boolean canBeProcess(MessageDTO messageDto) {
        // 覆盖 maxRetryCount
        RabbitMQListener dsRabbitListener = findAnnotation();
        if(Objects.nonNull(dsRabbitListener)) {
            long maxRetryCount = dsRabbitListener.maxRetryCount();
            long oldMaxRetryCount = messageDto.getMaxRetryCount();
            // 调整 最大重试次数, 消费者设置之后, 优先使用消费者
            if(maxRetryCount > oldMaxRetryCount) {
                log.warn("onMessage set maxRetryCount from:{} , to: {}", oldMaxRetryCount, maxRetryCount);
                messageDto.setMaxRetryCount(maxRetryCount);
            }

        }

        return messageDto.couldBeRetry();
    }

    /**
     * 具体的 消息处理,需要自己实现
     * @param messageDto DsMessageDTO
     * @param message org.springframework.amqp.core.Message
     * @param channel com.rabbitmq.client
     * @return 消息处理是否成功(true : 不需要重试, 需要重试, 重新消费消息)
     */
    public abstract boolean processMessage(MessageDTO messageDto, Message message, Channel channel);

    /**
     * 获取 实现DsAbstractMessageListener的class(this.getClass())的 注解信息
     * @date 2022-08-19 12:47:25
     * @return com.datasure.cloudsure.rabbit.annotation.DsRabbitListener
     */
    public RabbitMQListener findAnnotation() {
        Class clazz = this.getClass();
        return AnnotationUtils.findAnnotation(clazz, RabbitMQListener.class);
    }

    /**
     * 消息处理成功, 可以调用此方法的 重载.
     * @param messageDto DsMessageDTO
     * @param message Message
     * @param channel Channel
     */
    public void onSuccess(MessageDTO messageDto, Message message, Channel channel) {
        // process success business
    }

    /**
     * 消息处理失败, 可以调用此方法的 重载, 比如 重新放入到队列当中. 默认:处理失败, 重新发送消息到队列当中, retryCount/priority 累加
     * @param messageDto DsMessageDTO
     * @param message Message
     * @param channel Channel
     */
    public void onFailure(MessageDTO messageDto, Message message, Channel channel) {
        // 通过 this.getClass(), 获取 DsRabbitListener 注解信息
        RabbitMQListener rabbitListener = findAnnotation();

        if(Objects.isNull(rabbitListener)) {
            return;
        }
        boolean enableRetry = rabbitListener.enableRetry();
        String msg = String.format("onFailure enableRetry:%s ack ", enableRetry);

        log.info(msg);

        // 没有开启重试,不处理
        if (!enableRetry) {
            return;
        }

        // 开启了重试, 但是重试次数已经超过 maxRetryCount, 不处理
        boolean couldBeRetry = messageDto.couldBeRetry();
        if (!couldBeRetry) {
            msg = String.format("onFailure enableRetry:%s,couldBeRetry:%s, ack ", true, false);
            log.info(msg);
            return;
        }
        // exchangeName/routingKey
        MessageConvert.INSTANCE.assembleDsMessageDTO(messageDto, message);

        // 重试次数累加
        messageDto.incrementRetryCount();

        long retryCount = messageDto.getRetryCount();

        // 默认的 延迟时间, 发送消息时, 提供
        long originalDelayTime = messageDto.getDelayTime();

        originalDelayTime = originalDelayTime <= 0L ? DEFAULT_DELAY_TIME : originalDelayTime;

        // 重试 基数
        long intervalRadix = rabbitListener.intervalRadix();

        // delayTime = delayTime * (intervalRadix + retryCount)
        long delayTime = originalDelayTime * (intervalRadix + retryCount);

        // 重试时间
        long retryTime = retryIntervalTime(retryCount, rabbitListener.maxRetryCount());

        // 覆盖 间隔时间时, 使用自定义间隔时间
        delayTime = retryTime <= 0L ? delayTime : retryTime;

        messageDto.setDelayTime(delayTime);

        // 调整优先级
        messageDto.incrementPriority();

        // 消息重新发送时/消费失败,重试, traceId 重新赋值
        messageDto.setTraceId(UUIDUtil.getIdStrLong());

        // 重新发消息到队列
        dsMessageProducer.send(messageDto);
        msg = String.format("onFailure enableRetry:%s,couldBeRetry:%s,retryCount:%s, delayTime:%s ack ", true, true, retryCount, delayTime);
        log.info(msg);
    }

    /**
     * 消息消费时, 设置 traceId , 1. 从 DsMessageDTO 当中获取traceId 2. traceId 有效时, 设置到MDC 当中
     */
    public void getAndSetTraceId(MessageDTO dsMessageDto) {
        String dsMsgTraceId = dsMessageDto.getTraceId();
        if (StringUtils.hasLength(dsMsgTraceId)) {
            MdcUtil.setContextMap(dsMsgTraceId);
        }
    }

    /**
     * 重试 间隔时间, 可以被重写, 根据自己的业务,设置 重试间隔时间
     * @param retryCount 重试次数
     * @param maxRetryCount 最大重试 次数
     * @return 重试 间隔时间, 默认= 0L
     */
    protected long retryIntervalTime(long retryCount, long maxRetryCount) {

        return 0L;
    }

}

RabbitDeclareUtil.java

package com.fangsheng.technology.rabbitmq.service.declare;

import com.fangsheng.technology.dto.Response;
import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import com.fangsheng.technology.rabbitmq.dto.DeclareConfigDTO;
import com.rabbitmq.client.AMQP;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.util.Collections;

/**
 * @author lht
 * @date 2023/11/13 11:51:08
 * 创建 Exchange/Queue/bind exchange/queue
 */
@Slf4j
@Component
public class RabbitDeclareUtil {

    @Resource
    private AmqpAdmin dsAmqpAdmin;

    @Resource
    private RabbitTemplate dsRabbitTemplate;

    /**
     * 声明/创建 Exchange
     * @param configDto DsDeclareConfigDTO
     * @return Response
     */
    public Response declareExchange(DeclareConfigDTO configDto) {
        log.info("declareExchange with data start :{}", configDto);
        Assert.notNull(configDto.getExchangeName(), "The exchange name must not be null");

        Exchange exchange = null;
        // 需要创建 死信Exchange
        if(configDto.getCreateDeadExchange()){
            // 创建的 死信exchange, 不需要设置为 延时属性
            ExchangeBuilder builder;
            if (StringUtils.isBlank(configDto.getExchangeType())) {
                configDto.setExchangeType(ExchangeTypes.DIRECT);
            }

            String exchangeName = getExchangeName(configDto);

            exchangeName = String.format("%s%s", exchangeName , RabbitMQConstant.DEAD_SUFFIX);

            builder = new ExchangeBuilder(exchangeName, configDto.getExchangeType());

            exchange = builder.durable(true).build();
            dsAmqpAdmin.declareExchange(exchange);
            log.info("declareExchange dead exchange data end :{}", exchange.getName());
        }

        // 正常的 exchange, 根据需要, 设置为 是否延时
        exchange = createExchange(configDto,configDto.isDelayQueue());
        dsAmqpAdmin.declareExchange(exchange);
        log.info("declareExchange dead exchange data end :{}", exchange.getName());


        return Response.buildSuccess();
    }

    private Exchange createExchange(DeclareConfigDTO configDto, boolean isDelayQueue) {
        Exchange ex;
        ExchangeBuilder builder;
        if (StringUtils.isBlank(configDto.getExchangeType())) {
            configDto.setExchangeType(ExchangeTypes.DIRECT);
        }

        String exchangeName = getExchangeName(configDto);

        // re-set the type to DIRECT, on delayQueue
        if (isDelayQueue) {
            configDto.setExchangeType(ExchangeTypes.DIRECT);
            builder = new ExchangeBuilder(exchangeName, configDto.getExchangeType());
            ex = builder
                    .durable(true)
                    .delayed()
                    .build();
            return ex;
        }

        builder = new ExchangeBuilder(exchangeName, configDto.getExchangeType());

        ex = builder.durable(true).build();
        return ex;
    }

    /**
     * 拼接 Exchange name + @exg
     * @date 2022-08-18 11:32:39
     * @param configDto DsDeclareConfigDTO
     * @return java.lang.String
     */
    private static String getExchangeName(DeclareConfigDTO configDto) {
        String exchangeName = String.format("%s%s", configDto.getExchangeName(), RabbitMQConstant.EXCHANGE_SUFFIX);
        return exchangeName;
    }

    /**
     * 声明/创建队列,
     * 如果是死信队列,<br>
     * 1. 自动创建 死信 exchange <br>
     * 2. 创建 死信 queue
     * 3. 先将 死信 exchange/死信routingKey/死信queue 绑定<br>
     * 4. 再将 exchange/routingKey/queue 绑定 <br>
     * deadLetterRoutingKey 不存在,则使用默认的 xxx@rtk@dead
     * @param configDto DsDeclareConfigDTO
     * @return Response.buildSuccess
     */
    public Response declareQueue(DeclareConfigDTO configDto) {
        log.info("declareQueue with data start :{}", configDto);

        checkName(configDto);

        String queueName = getQueueName(configDto);


        QueueInformation existQueue = getQueueByName(queueName);

        // 暂时注释 Assert.isNull(existQueue, "The queue has exist : " + queueName);

        /// 如果为死信队列
        if(configDto.isDeadQueue()) {
            // 死信 exchange 处理
            declareExchange(configDto);

            // 创建 queue, 并绑定 死信exchange/routingKey
            createQueue(configDto, true);
        } else {
            // 直接 声明 queue
            createQueue(configDto, false);
        }


        log.info("declareQueue with data end :{}", configDto);

        return Response.buildSuccess();
    }

    /**
     * 拼接获取Queue name + @que
     * @date 2022-08-18 11:33:39
     * @param configDto DsDeclareConfigDTO
     * @return java.lang.String
     */
    private static String getQueueName(DeclareConfigDTO configDto) {
        String queueName = String.format("%s%s", configDto.getQueueName(), RabbitMQConstant.QUE_SUFFIX);
        return queueName;
    }

    /**
     * 拼接获取Routing key + @rtk
     * @date 2022-08-18 12:02:32
     * @param configDto 配置对象
     * @return java.lang.String
     */
    private static String getRoutingKey(DeclareConfigDTO configDto) {
        String routingKey = String.format("%s%s", configDto.getRoutingKey(), RabbitMQConstant.ROUTING_KEY_SUFFIX);

        return routingKey;
    }


    /**
     * 创建Queue,
     * 如果创建死信queue, 则需要绑定 死信 exchange/死信 routingKey
     * @param configDto DsDeclareConfigDTO
     * @param bindDeadQueue 是否绑定 死信queue
     */
    private void createQueue(DeclareConfigDTO configDto, boolean bindDeadQueue) {
        Queue queue;
        QueueBuilder queueBuilder;

        // 拼接 queue name
        String queueName = getQueueName(configDto);

        // 如果为死信队列, 则额外创建 死信队列, 并指定之前创建的 死信Exchange,
        // queue 绑定 死信exchange/死信 routingKey
        if (bindDeadQueue) {
            // 先创建 死信queue(普通的 queue)
            String deadQueueName = String.format("%s%s", queueName, RabbitMQConstant.DEAD_SUFFIX);
            QueueBuilder deadQueueBuilder =  QueueBuilder
                    // 死信 queueName@dead
                    .durable(deadQueueName);

            dsAmqpAdmin.declareQueue(deadQueueBuilder.build());

            log.info("declareQueue queue name :{}", deadQueueName);

            // 死信 exchange 必须先创建 ,
            // 死信 routingKey 为空则使用默认的,
            // 绑定 到 死信 exchange
            queueBuilder = QueueBuilder
                    // 持久化 queue
                    .durable(queueName)
                    .deadLetterExchange(configDto.getDeadLetterExchangeName())
                    .deadLetterRoutingKey(configDto.getDeadLetterRoutingKey());
        } else {
            // 构造 queue
            queueBuilder = QueueBuilder.durable(queueName);

            if (configDto.getTtl() > 0) {
                queueBuilder.ttl(configDto.getTtl());
            }
        }

        queue = queueBuilder.build();

        // 声明queue
        dsAmqpAdmin.declareQueue(queue);
        log.info("declareQueue queue name :{}", queue.getName());

    }

    private void checkName(DeclareConfigDTO configDto) {
        Assert.notNull(configDto.getQueueName(), "The queue name must not be null");
    }

    /**
     * getQueueByName
     * @param queueName String name 不会拼接 @que
     * @return QueueInformation
     */
    public QueueInformation getQueueByName(String queueName) {
        QueueInformation queueInformation = dsAmqpAdmin.getQueueInfo(queueName);
        return queueInformation;
    }

    /**
     * bind Exchange/queue/routingKey
     * @param configDto DsDeclareConfigDTO
     * @return Response
     */
    public Response bindQueue(DeclareConfigDTO configDto) {
        log.info("bindQueue with data start :{}", configDto);

        checkName(configDto);
        Assert.notNull(configDto.getExchangeName(), "The exchange name must not be null");
        Assert.notNull(configDto.getRoutingKey(), "The routingKey must not be null");

        String queueName = getQueueName(configDto);

        String exchangeName = getExchangeName(configDto);

        String routingKey = getRoutingKey(configDto);

        QueueInformation existsQueue = getQueueByName(queueName);

        Assert.notNull(existsQueue, "The existsQueue must not be null");

        dsRabbitTemplate.execute(channel -> {
            AMQP.Exchange.DeclareOk ok = null;
            try {
                log.info("bindQueue check exchange start :{}", configDto);
                ok = channel.exchangeDeclarePassive(exchangeName);
                log.info("bindQueue check exchange end :{}", configDto);
            }
            catch (Exception e) {
                log.error("bindQueue check Exchange exists errors", e);
                // 有异常,重新 创建Exchange
                declareExchange(configDto);
            }

            log.info("bindQueue queueBind start :{}", configDto);
            channel.queueBind(queueName, exchangeName, routingKey, Collections.<String, Object>emptyMap());
            log.info("bindQueue queueBind end :{}", configDto);

            // 如果是死信队列标识,则需要将 死信exchange/死信routingKey/死信queue 绑定在一起
            if (configDto.isDeadQueue()) {
                String deadQueueName = String.format("%s%s", queueName, RabbitMQConstant.DEAD_SUFFIX);
                String deadExchangeName = configDto.getDeadLetterExchangeName();
                String deadRoutingKey = configDto.getDeadLetterRoutingKey();

                log.info("bindQueue deadQueue:{} start :{}", deadQueueName, configDto);
                channel.queueBind(deadQueueName, deadExchangeName, deadRoutingKey, Collections.<String, Object>emptyMap());
                log.info("bindQueue deadQueue:{} start :{}", deadQueueName, configDto);
            }


            return null;
        });

        return Response.buildSuccess();
    }

}

MessageProducer.java

package com.fangsheng.technology.rabbitmq.service.producer;

import com.fangsheng.technology.dto.Response;
import com.fangsheng.technology.log.mdc.MdcUtil;
import com.fangsheng.technology.rabbitmq.dto.CustomMessagePostProcessor;
import com.fangsheng.technology.rabbitmq.dto.MessageDTO;
import com.fangsheng.technology.uuid.UUIDUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

/**
 * @author lht
 * @date 2023/11/13 11:43:57
 * 消息生产者相关
 */
@Component
public class MessageProducer {

    @Resource
    private RabbitTemplate dsRabbitTemplate;

    /**
     * 发送消息, DsMessageDTO 包含: exchangeName/routingKey/content.
     * content-type:application/json
     * @param messageDto DsMessageDTO
     * @return Response
     */
    public Response send(MessageDTO messageDto) {

        String exchangeName = messageDto.getExchangeName();

        String routingKey = messageDto.getRoutingKey();

        getAndSetIdBeforeSend(messageDto);

        CustomMessagePostProcessor messagePostProcessor = new CustomMessagePostProcessor(messageDto);

        dsRabbitTemplate.convertAndSend(exchangeName, routingKey, messageDto, messagePostProcessor);

        return Response.buildSuccess();
    }

    public void getAndSetIdBeforeSend(MessageDTO messageDto) {
        // msgId empty
        if (!StringUtils.hasLength(messageDto.getMsgId())) {
            messageDto.setMsgId(UUIDUtil.getIdStrLong());
        }

        // traceId
        // 1. dto
        // 2. mdc thread local
        if (!StringUtils.hasLength(messageDto.getTraceId())) {
            String traceId = null;
            traceId = MdcUtil.getTraceId();

            // 从当前线程,传递到 message 中
            // 消息重试时, traceId 应该每次 设置新的
            if (StringUtils.hasLength(traceId)) {
                messageDto.setMsgId(UUIDUtil.getIdStrLong());
            }

        }

    }

}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fangsheng.technology.rabbitmq.config.RabbitAutoConfiguration

component-rabbitmq-starter组件集成

集成于 core-server

本项目对MQ 的使用场景: 1、状态的兜底:比如涉及了一个第三方接口的调用,我们启动一个任务,在调用第三方接口前我们要把任务改为启动中,然后往MQ发一条延迟的消息,等第三方回调过来后根据第三方的结果来将任务的状态切换,如果在延迟的时间内没有收到第三方的回调我们也需要将任务状态回滚。 2、策略定时:结合 elastic - job 定时任务制定策略,比如前段页面有这样一个功能,配置同步第三方数据的策略,可以动态的修改,支持 30分钟,一个小时,一天 等,因为是动态会变化的仅仅依考固定的定时任务不太行,可以结合MQ,定时任务根据最小的时间单位固定定时取出配置,根据配置的策略是分钟,还是小时,天,然后根据当前时间计算出下一次同步时间后将任务放到MQ中延迟消费。

在core-server的pom引入管理依赖

    <properties>
        <component-rabbitmq-starter.version>1.0.0-SNAPSHOT</component-rabbitmq-starter.version>
    </properties>

    <dependencyManagement>
        <dependencies>
        	...
            <dependency>
                <groupId>com.fangsheng.technology</groupId>
                <artifactId>component-rabbitmq-starter</artifactId>
                <version>${component-rabbitmq-starter.version}</version>
            </dependency>
            ...
        </dependencies>
    </dependencyManagement>

在app和infra层pom引入真实依赖

            <dependency>
                <groupId>com.fangsheng.technology</groupId>
                <artifactId>component-rabbitmq-starter</artifactId>
            </dependency>

测试代码

│  core-server-app
│  pom.xml
│
└─src
    └─main
        └─java
            └─com
                └─fangsheng
                    └─technology
                        └─app
                            │
                            ├─hero
                               ├─constant
                               │      HeroMQConstant.java
                               │
                               └─consumer
                                       RabbitMQTestListener.java

HeroMQConstant.java

package com.fangsheng.technology.app.hero.constant;

/**
 * @author fangsheng
 * @title HeroMQConstant
 * @description
 * @create 2023/11/13 23:11
 */
public class HeroMQConstant {

    /** 交换机(创建成功后后添加@exg后缀) */
    public static final String EXCHANGE = "test_exchange_name";
    /** 路由key(创建成功后后添加@rkt后缀) */
    public static final String ROUTING_KEY = "test_routing_key";
    /** 队列(创建成功后后添加@que后缀) */
    public static final String QUEUE = "test";

}

RabbitMQTestListener.java

package com.fangsheng.technology.app.hero.consumer;

import com.alibaba.fastjson.JSON;
import com.fangsheng.technology.app.hero.constant.HeroMQConstant;
import com.fangsheng.technology.rabbitmq.annotation.RabbitMQListener;
import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import com.fangsheng.technology.rabbitmq.dto.MessageDTO;
import com.fangsheng.technology.rabbitmq.service.consumer.AbstractMessageListener;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.stereotype.Component;

/**
 * @author fangsheng
 * @title RabbitMQTestListener
 * @description
 * @create 2023/11/13 22:50
 */
@Slf4j
@Component
@RabbitMQListener(queueNames = HeroMQConstant.QUEUE + RabbitMQConstant.QUE_SUFFIX,enableRetry = true,maxRetryCount = 3)
public class RabbitMQTestListener extends AbstractMessageListener {

    @Override
    public boolean processMessage(MessageDTO messageDto, Message message, Channel channel) {
        log.info("获取到 MQ 消息为 {}", JSON.toJSONString(messageDto));
        return true;
//        return false;  如果返回为false 表示消费失败,会再次进入消费
    }

    @Override
    protected long retryIntervalTime(long retryCount, long maxRetryCount) {

        return 60*1000L;
    }
}

application.yml

spring:
  rabbitmq:
    host: 192.168.17.176
    port: 5672
    username: guest
    password: guest
    template:
      mandatory: true
    listener:
      simple:
        acknowledge-mode: MANUAL

RabbitMQTest.java

在\core-server\core-server-start\src\test\java\com\fangsheng\technology\mq\RabbitMQTest.java

package com.fangsheng.technology.mq;

import com.alibaba.fastjson.JSON;
import com.fangsheng.technology.app.hero.constant.HeroMQConstant;
import com.fangsheng.technology.rabbitmq.constant.RabbitMQConstant;
import com.fangsheng.technology.rabbitmq.dto.DeclareConfigDTO;
import com.fangsheng.technology.rabbitmq.dto.MessageDTO;
import com.fangsheng.technology.rabbitmq.service.declare.RabbitDeclareUtil;
import com.fangsheng.technology.rabbitmq.service.producer.MessageProducer;
import com.fangsheng.technology.start.CoreServerApplication;
import com.fangsheng.technology.uuid.UUIDUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;

/**
 * @author fangsheng
 * @title RabbitMQTest
 * @description mq 的测试类
 * @create 2023/11/13 22:14
 */
@Slf4j
@SpringBootTest(classes = CoreServerApplication.class)
public class RabbitMQTest {


    @Resource
    private RabbitDeclareUtil rabbitDeclareUtil;

    /**
     * 运行当前方法创建队列,交换机的时候需要将 RabbitMQTestListener上的@RabbitMQListener注释掉,因为队列还没创建,
     * 这个时候监听一个还未创建完成的队列会报错。创建成功后再将注释放开,(生产环境的队列创建是从测试环境导入的)
     */
    @Test
    public void testDeclareQueue(){

        DeclareConfigDTO configDTO = new DeclareConfigDTO();

        // 死信队列
        boolean isDeadQueue = false;

        // 延迟队列
        boolean isDelayQueue = true;


        configDTO.setExchangeName(HeroMQConstant.EXCHANGE);
        configDTO.setRoutingKey(HeroMQConstant.ROUTING_KEY);
        configDTO.setQueueName(HeroMQConstant.QUEUE);

        // 死信标识
        configDTO.setDeadQueue(isDeadQueue);

        // 延迟标识(exchange上面)
        configDTO.setDelayQueue(isDelayQueue);
        configDTO.setCreateDeadExchange(true);

        // 创建/声明queue(标识 死信队列时,会创建死信exchange,以及绑定 到死信exchange)
        rabbitDeclareUtil.declareQueue(configDTO);

        // bind 队列/routingKey/queue, 标识为死信队列时,
        // 还需要 将 死信Exchange/死信routingKey/死信queue绑定
        rabbitDeclareUtil.bindQueue(configDTO);

    }

    @Resource
    private MessageProducer producer;

    @Test
    public void pushMessageToMq() throws InterruptedException {

        Map<String, String> hero = Map.of("id", UUIDUtil.getIdStrLong(), "heroName", "剑豪",
                "motto", "我的剑比什么都重要,除了美酒。","date",new Date().toString());
        MessageDTO msgDto = new MessageDTO();
        msgDto.setExchangeName(HeroMQConstant.EXCHANGE+RabbitMQConstant.EXCHANGE_SUFFIX);
        msgDto.setRoutingKey(HeroMQConstant.ROUTING_KEY+RabbitMQConstant.ROUTING_KEY_SUFFIX);
        msgDto.setDelayTime(3000L);
        msgDto.setContent(JSON.toJSONString(hero));
        producer.send(msgDto);
        log.info("-----------send msg to RabbitMQ Success");

        Thread.sleep(1000*60);

    }




}

创建交换机,路由,队列

先注释掉消费者 RabbitMQTestListener 再 运行testDeclareQueue()方法

发送消息

创建交换机,路由,队列成功后将 消费者 RabbitMQTestListener 打开在运行pushMessageToMq()

注意查看控制台的日志输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值