Spring Boot整合RabbitMQ

大佬文章,请优先查看!!!

黑马程序员RabbitMQ入门到实战教程,MQ消息中间件



简述

  • Spring支持

    spring-jms提供了对JMS的支持
    spring-rabbit提供了对AMQP的支持
    需要ConnectionFactory的实现来连接消息代理
    提供JmsTemplate、RabbitTemplate来发送消息
    @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
    @EnableJms、@EnableRabbit开启支持

  • Spring Boot自动配置

    JmsAutoConfiguration
    RabbitAutoConfiguration

自定义消息转换器

spring的对象系处理器是由org.springframework.amqp.support.converter.MessageConverter来处理的,而默认实现就是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化,如果要修改只需要定义一个MessageConverter类型的Bean即可。

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;

@Configuration
public class MyAMQPConfig {

	/**
     * json格式消息转换
     *
     * @return
     */
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

rabbitmq配置说明

生产者重连

当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。

如果对于业务性能有要求,建议禁用重试机制。如果需要使用,需要合理配置等待时长和重试次数,也可以考虑使用异步线程来执行发送消息。

#开启超时重试机制
spring.rabbitmq.template.retry.enabled=true
#失败后的初始等待时间
spring.rabbitmq.template.retry.initial-interval=1000
#失败后下次的等待时长倍数,下次等待时长=initial-interval * multiplier
spring.rabbitmq.template.retry.multiplier=1
#最大重试次数
spring.rabbitmq.template.retry.max-attempts=3

生产者确认机制

RabbitMQ中有 Publisher ConfirmPublisher Return两种确认机制。开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:

  • 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功
  • 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
  • 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功
  • 其他情况都会返回NACK,告知投递失败
spring:
	rabbitmq:
		publisher-confirm-type: correloted #开启publisher confirm消息确认模式,并设置confirm类型
		publisher-returns: true #开启publisher return机制,一般不会开启

publisher-confirm-type有三种模式:

none:关闭confirm机制

simple:同步阻塞等待MQ的回执消息

correlated:MQ异步回调方式返回回执消息

Return机制

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey)->{
            log.info("消息发送失败,应答码:{},原因:{},交换机:{},路由键:{},消息:{}",replyCode,replyText,exchange,routingKey,message.toString());
        });
    }
}
Confirm机制

发型消息,指定消息ID、消息ConfirmCallback

@Test
public void testPublisherConfirm() throws InterruptedException {
    // 创建CorrelationData
    CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
    // Future添加ConfirmCallback
    cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
        @Override
        public void onFailure(Throwable e) {
            // Future发送异常时的处理逻辑,属于spring处理异常,不是mq返回的失败,基本不会触发
            log.error("handle message ack fail", e);
        }
        @Override
        public void onSuccess(CorrelationData.Confirm result) {
            // Future接收到回执的处理逻辑,参数中的result就是回执内容
            if (result.isAck()) {
                log.debug("发送消息成功,收到ack...");
            } else {
                log.error("发送消息失败,收到nack, reason:{}", result.getReason());
            }
        }
    });
   	// 发送消息
    rabbitTemplate.convertAndSend("exchange", "routingkey", "hello", cd);
}
小结

生产者消息确认需要额外的网路和系统资源开销,尽量不要使用。如果需要使用,无需开启Publisher-Return机制,因为一般路由失败是自身业务问题。对于nack消息可以有限次数重试,依然失败则记录异常消息。

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMQP已经实现了消息确认功能,并允许我们通过配置文件选择ACK处理方式,有三种方式:

  • none:不处理。即消息投递给消费者后立即ack,消息会立刻从MQ删除。非常不安全,不建议使用。

  • manual:手动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果:

    如果是业务异常,会自动返回nack。

    如果是消息处理或校验异常,自动返回reject。

spring:
	rabbitmq:
		listener:
			simple:
				prefetch:  1
				acknowledge-mode: none # none:关闭ack;manual:手动ack;auto:自动ack

消费者失败重试机制

当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理压力飙升。

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

spring:
	rabbitmq:
		listener:
			simple:
				prefetch: 1
				retry:
					enabled: true # 开启消费者失败重试
					initial-interval: 1000ms # 初始的失败等待时长为1秒
					multiplier: 1 # 下次失败的等待时长倍数,下次等待时长=multiplier*lost-interval
					max-attempts: 3 # 最大重试次数
					stateless: true # true:无状态;false:有状态。如果业务中包含事务,这里改为false
失败消息处理策略

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式。
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。

RepublishMessageRecoverer方式示例:

将失败处理策略改为RepublishMessageRecoverer,首先定义接收失败消息的交换机、队列及其绑定关系;然后定义RepublishMessageRecoverer

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate reabbitTempate) {
    return new RepublishMessageRecoverer(rabbitTemplate, "hguo.error.exchanges", "error.routing.key");
}

RabbitMQ整合

引入spring-boot-starter-amqp依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.yml配置

spring:
  rabbitmq:
    host: # 主机地址
    username: # 用户名guest
    password: # 密码guest
    port: 5672 # 默认端口5672

启动类添加启动注解

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 自动配置
 *  1、RabbitAutoConfiguration
 *  2、有自动配置了连接工厂ConnectionFactory;
 *  3、RabbitProperties 封装了 RabbitMQ的配置
 *  4、 RabbitTemplate :给RabbitMQ发送和接受消息;
 *  5、 AmqpAdmin : RabbitMQ系统管理功能组件;
 *  	AmqpAdmin:创建和删除 Queue,Exchange,Binding
 *  6、@EnableRabbit +  @RabbitListener 监听消息队列的内容
 *
 */
@EnableRabbit  //开启基于注解的RabbitMQ模式
@SpringBootApplication
@MapperScan({"com.example.demo.mapper"})
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}

MQ配置(队列、交换机声明)

import lombok.RequiredArgsConstructor;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * rabbitMq配置
 *
 */
@Configuration
public class RabbitMqConfig {

    private final CachingConnectionFactory connectionFactory;

    public RabbitMqConfig(CachingConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }


    /**
     * 消息监听器工厂
     *
     * @return
     */
    @Bean(name = "mqListenerContainer")
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory() {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // 限流 一次性从队列中最大能拉取消息数
        factory.setPrefetchCount(50);
        return factory;
    }

    // 其他方式声明监听器
/*    public SimpleMessageListenerContainer getObject() throws Exception {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setAmqpAdmin(amqpAdmin);
        container.setConnectionFactory(connectionFactory);
        container.setQueues(queue);
        container.setPrefetchCount(20);
        container.setConcurrentConsumers(20);
        container.setMaxConcurrentConsumers(100);
        container.setDefaultRequeueRejected(Boolean.FALSE);
        container.setAdviceChain(createRetry());
        container.setAcknowledgeMode(autoAck ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL);
//        container.stop();
        if (Objects.nonNull(consumer)) {
            container.setMessageListener(consumer);
        }
        return container;
    }*/

    /**
     * 动态生成队列
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        return new RabbitAdmin(connectionFactory);
    }

    /**
     * 声明队列
     *
     * @return
     */
    @Bean
    public Queue createQueue() {

        /*
         * 第一种方式:
         *
         * durable():代表需要持久化
         * exclusive(): 代表该队列独占(只允许有一个consumer监听)
         * autoDelete(): 代表需要自动删除(没有consumer自动删除)
         * withArgument(): 队列的其他参数
         */
//        return QueueBuilder.durable("boot_work_queue").exclusive().autoDelete().withArgument("key", "val").build();

        /*
         * 第二种方式:通过new Queue对象来创建队列
         *
         * name:队列名称
         * durable:队列是否持久化,默认是true
         * exclusive:是否独占,队列是否设置为排他队列,默认是false。为true时设置为排他队列,只对首次声明它的连接可见,
         *            其他连接无法声明相同名称的其他队列,并且在连接断开时自动删除,即使持久化也会被删除
         * autoDelete:队列是否自动删除,默认false。为true时,当没有消费者使用此队列,该队列会自动删除
         *
         * 一般设置一下队列的持久化就好,其余两个就是默认false
         * */
        return new Queue("chat.room.queue", true, false, false);
    }


    /**
     * 声明死信队列
     *
     * @return
     */
    @Bean
    public Queue deadQueue() {
        Map<String, Object> map = new HashMap<>();
        // 队列中的每一个消息未被消费则5秒后过期,被自动删除并移到死信队列
        map.put("x-message-ttl", 5000);
        return new Queue("chat.dead.queue", true, false, false, map);
    }


    /**
     * 声明发布订阅模式交换机
     *
     * @return
     */
    @Bean
    FanoutExchange fanoutExchange() {

        /*
         * 第一种方式: 通过ExchangeBuilder构建交换机
         *
         * 通过ExchangeBuilder声明交换机
         * 每种类型交换机有对应方法,如:fanoutExchange()、topicExchange()
         * - durable: 是否持久化
         * - autoDelete: 是否自动删除
         * - withArgument: 交换机其他参数
         * */
//        return ExchangeBuilder.fanoutExchange("boot_fanout_exchange").durable(true).build();
//        return ExchangeBuilder.directExchange("boot_direct_exchange").durable(true).autoDelete().withArgument("key","val").build();

        /*
         * 第二种方式:通过new FanoutExchange对象声明交换机
         *
         * name:交换机名称
         * durable:是否持久化(默认false)
         * autoDelete:是否自动删除(默认false)
         * */
        return new FanoutExchange("fanout.exchange", true, false);
    }

    /**
     * 声明路由模式交换机
     *
     * @return
     */
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange("direct.exchange", true, false);
    }

    /**
     * 声明主题模式交换机
     */
    @Bean
    TopicExchange topicExchange() {
        return new TopicExchange("topic.exchange", true, false);
    }


    /**
     * 交换机与队列进行绑定
     */
    @Bean
    public Binding bindQueueExchange() {

        /*
         * 第一种方式: 通过BindingBuilder绑定
         *
         * bind(Queue): 需要绑定的queue
         * to(Exchange): 需要绑定到哪个交换机
         * with(String): routing key
         * noargs(): 进行构建
         */
//        return BindingBuilder.bind(testQueue()).to(directExchange()).with("article").noargs();
//        return BindingBuilder.bind(testQueue()).to(directExchange()).with(testQueue().getName());

        /*
         * 第二种方式:通过new Binding对象绑定
         *
         * destination: 绑定的队列
         * destinationType: 绑定的类型 Binding.DestinationType.QUEUE: 绑定的类型为queue(交换机不仅可以绑定queue还可以绑定exchange)
         * exchange: 哪个交换机需要绑定
         * routingKey: routing key
         * arguments: 其他参数
         */
        return new Binding("chat.room.queue", Binding.DestinationType.QUEUE, "fanout.exchange", "chat.room.key", null);
    }

}

动态创建队列与交换机

一般都是通过注入Bean的方式去声明交换机、队列,应用启动时去自动创建。

如果需要动态创建,比如通过接口、或者业务代码自己去操作,这个使用就需要使用RabbitMQ提供的操作接口。如果是基于Spring Boot,则可以直接使用其提供的AmqpAdmin,其对RabbitMQ的原生接口进行了二次封装,使用起来十分方便。AmqpAdmin接口,只有一个实现类RabbitAdmin

import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * RabbitMq动态配置
 * 动态生成队列和交换机
 *
 */
@Controller
public class RabbitMqDynamicController {

    /**
     * 实现了AmqpAdmin接口
     */
    @Autowired
    private RabbitAdmin rabbitAdmin;

    /**
     * 通过AmqpAdmin:创建和删除 Queue,Exchange,Binding
     */
    @Autowired
    private AmqpAdmin amqpAdmin;

    @PostMapping("/dynamic")
    public void dynamicConfig() {
        //创建mq队列
        Queue test3Queue = new Queue("test3_Queue", true, false, false);
        rabbitAdmin.declareQueue(test3Queue);
        //创建交换机
        DirectExchange direct1Exchange = new DirectExchange("direct1_Exchange", true, false);
        rabbitAdmin.declareExchange(direct1Exchange);
        //绑定交换机和队列,并设置Routing key
        Binding binding = BindingBuilder.bind(test3Queue).to(direct1Exchange).with(test3Queue.getName());
        rabbitAdmin.declareBinding(binding);
    }

    /**
     * 删除mq队列
     *
     * @return
     */
    @PostMapping("/deleteMq")
    public String deleteMq(String mq) {
        rabbitAdmin.deleteQueue(mq);
        return "ok";
    }

    @PostMapping("/")
    public void amqpAdminCreate(){

        // 创建Exchange
        amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));

        /*
        * 创建消息队列
        * - Queue是类可以直接new,构造器第一个参数:队列名,第二参数:是否持久化存在,若没有指定参数则随机给队列名
        * */
        amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));

        /*
        * 创建绑定规则
        * - 参数1:目的地(队列)
        * - 参数2:绑定的类型->队列
        * - 参数3:Exchange
        * - 参数4:路由件
        * - 参数5:参数没有为null
        * */
        amqpAdmin.declareBinding(new Binding("amqpadmin.queue", Binding.DestinationType.QUEUE,"amqpadmin.exchange","amqp.haha",null));

        //amqpAdmin.deleteExchange();  // 删除交换器
        //amqpAdmin.deleteQueue();  // 删除队列
    }

}

自定义消息转换器

默认使用jdk序列化方式

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 消息转换器
 *
 */
@Configuration
public class MessageConverterConfig {
    /**
     * 使用json转换方式
     *
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

消息实体

注意:如果消费者接受的消息是实体类对象,需要将类序列化

import lombok.Data;

import java.io.Serializable;

/**
 * 测试:消息实体
 *
 */
@Data
public class Book implements Serializable {

    private static final long serialVersionUID = 7512574760433830393L;

    /**
     * 书名
     */
    private String name;
    /**
     * 价格
     */
    private String price;

}

生产者推送消息

import com.alibaba.fastjson.JSON;
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 推送消息控制器
 *
 */
@RestController
@Slf4j
@RequestMapping("/producer")
public class ProducerController {

    private final RabbitTemplate rabbitTemplate;

    public ProducerController(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @PostMapping("/testSend")
    public void testSendMessage(@RequestBody Book book) {
        log.info("生产者发送消息:{}", JSON.toJSONString(book));
        /*
         * 对象被默认序列化以后发送出去
         * object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq
         * */
        rabbitTemplate.convertAndSend("chat.fanout.exchange", "chat.room.key", book);
    }
}

消费者监听消息

  • 方式一
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 使用@RabbitListener注解在类上监听
 *
 */
@Component
@RabbitListener(queues = "chat.room.queue")
@Slf4j
public class TestMqReceiver {

    /**
     * 监听消息
     *
     * @param message 消息体
     */
    @RabbitHandler
    public void process(Book message) {
        log.info("@RabbitListener注解在类上监听消息:{}", message);
    }
    
}
2023-12-07 22:37:14.414  INFO 20184 --- [nio-8001-exec-2] c.m.r.controller.ProducerController      : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:37:18.695  INFO 20184 --- [ntContainer#0-1] c.m.rabbitmq.listener.TestMqReceiver     : @RabbitListener注解在类上监听消息:Book(name=神雕侠侣, price=100)
  • 方式二
import com.alibaba.fastjson.JSONObject;
import com.mazha.rabbitmq.pojo.domain.Book;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 使用@RabbitListener注解方法上监听消息
 *
 */
@Component
@Slf4j
public class TestMqReceiver2 {

    /**
     * 监听指定的消息队列(数组类型,可以指定监听多个)
     *
     * @param book
     */
    @RabbitListener(queues = {"chat.room.queue"})
    public void receive(Book book) {
        log.info("使用@RabbitListener注解方法上监听消息:{}", JSONObject.toJSONString(book));
    }

}
2023-12-07 22:37:29.836  INFO 20184 --- [nio-8001-exec-1] c.m.r.controller.ProducerController      : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:37:29.838  INFO 20184 --- [ntContainer#1-1] c.m.rabbitmq.listener.TestMqReceiver2    : 使用@RabbitListener注解方法上监听消息:{"name":"神雕侠侣","price":"100"}
  • 方式三
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * rabbitTemplate.receiveAndConvert()方法接收消息
 *
 */
@Controller
@RequestMapping("/testMqReceiver3")
@Slf4j
@RequiredArgsConstructor
public class TestMqReceiver3 {

    private final RabbitTemplate rabbitTemplate;

    /**
     * 接收数据
     */
    @GetMapping("/receive")
    public Object receiveMessage() {
        Object message = rabbitTemplate.receiveAndConvert("chat.room.queue");
        log.info("receiveAndConvert方法方式监听消息:{}", JSON.toJSONString(message));
        return message;
    }
}
2023-12-07 22:43:28.158  INFO 15420 --- [nio-8001-exec-1] c.m.r.controller.ProducerController      : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:43:28.171  INFO 15420 --- [nio-8001-exec-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [192.168.56.103:5672]
2023-12-07 22:43:28.199  INFO 15420 --- [nio-8001-exec-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#59ed3e6c:0/SimpleConnection@133f6b8e [delegate=amqp://guest@192.168.56.103:5672/, localPort= 61478]
2023-12-07 22:44:04.910  INFO 15420 --- [nio-8001-exec-2] c.m.r.controller.ProducerController      : 生产者发送消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:44:18.177  INFO 15420 --- [nio-8001-exec-3] c.m.rabbitmq.listener.TestMqReceiver3    : receiveAndConvert方法方式监听消息:{"name":"神雕侠侣","price":"100"}
2023-12-07 22:44:25.370  INFO 15420 --- [nio-8001-exec-4] c.m.rabbitmq.listener.TestMqReceiver3    : receiveAndConvert方法方式监听消息:{"name":"神雕侠侣","price":"100"}
posted @ 2024-03-03 17:52  Lz_蚂蚱  阅读(334)  评论(0)    收藏  举报