Spring Boot 整合——RabbitMQ配置消息确认回调、消息转换以及消息异常处理

消息回调

消息队列在实际运用中,作为消息的生产者,很多时候我们需要确认消息是否成功发送到了mq中。同时我们还需要知道,假如消息出现异常时的异常情况。为了满足这个业务场景,我们就需要配置消息回调。

开启消息回调

为了保证消息生产者能够收到消息的回调信息,我么需要修改以下配置,只有添加了下面的配置,才能保证添加相关代码后可以收到系统的回复。

spring:
  rabbitmq:
    # 开启发送确认
    publisher-confirms: true
    # 开启发送失败退回
    publisher-returns: true

目前回调存在ConfirmCallbackReturnCallback两者。他们的区别在于

  1. 如果消息没有到exchange,则ConfirmCallback回调,ack=false,
  2. 如果消息到达exchange,则ConfirmCallback回调,ack=true
  3. exchange到queue成功,则不回调ReturnCallback

rabbitMQ 消息生产者发送消息的流程

生产者 rabbitMQ 发送消息 返回ack确认消息 调用confirmCallback、ReturnCallback函数 生产者 rabbitMQ

配置消息回调

ConfirmCallback的回调

/**
 * 消息发送成功的回调
 * 需要开启
 * # 开启发送确认
 * publisher-confirms: true
 * @author daify
 * @date 2019-07-22 15:44
 **/
@Slf4j
public class RabbitConfirmCallBack 
        implements RabbitTemplate.ConfirmCallback {
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info("消息唯一标识: {}", correlationData);
        log.info("确认状态: {}", ack);
        log.info("造成原因: {}", cause);
    }
}

ReturnCallback的回调

/**
 * 发生异常时的消息返回提醒
 * 需要开启
 * # 开启发送失败退回
 * publisher-returns: true
 * @author daify
 * @date 2019-07-22 15:45
 **/
@Slf4j
public class RabbitReturnCallback 
        implements RabbitTemplate.ReturnCallback {
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息主体: {}", message);
        log.info("回复编码: {}", replyCode);
        log.info("回复内容: {}", replyText);
        log.info("交换器: {}", exchange);
        log.info("路由键: {}", routingKey);
    }
}

将回调配置到模板中

@Configuration
@AllArgsConstructor
public class RabbitInitializingBean implements InitializingBean {

    private RabbitTemplate rabbitTemplate;

    @Override
    public void afterPropertiesSet() throws Exception {
        rabbitTemplate.setConfirmCallback(new RabbitConfirmCallBack());
        rabbitTemplate.setReturnCallback(new RabbitReturnCallback());
    }
}

配置其exchange和queue

我们现在设计这样一种测试场景。这样send3将会触发ReturnCallback,send2的ack应该为false

back.V1
back.V3-发起一个错误的路由key
back.V1
back.V2
back.V3
send1
exchange
send3
back.queue1
back.queue2
send2
不存在此路由

配置queue和exchange

/**
 * @author daify
 * @date 2019-07-22 
 **/
@Configuration
public class BackConfig {
    
    public static final String BACK_QUEUE1 = "back.queue1";
    public static final String BACK_QUEUE2 = "back.queue2";
    public static final String BACK_EXCHANGE = "back.exchange";
    public static final String BACK_EXCHANGE_NO = "back.exchange.no";

    @Bean
    public Queue getBackQueue() {
        return new Queue(BACK_QUEUE1);
    }

    @Bean
    public Queue getBackQueue2() {
        return new Queue(BACK_QUEUE2);
    }
    
    @Bean
    public DirectExchange getBackExchange() {
        return new DirectExchange(BACK_EXCHANGE);
    }

    /**
     * direct模式
     * 消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 
     * 交换器就将消息发到对应的队列中。路由键与队列名完全匹配
     * @return
     */
    @Bean
    public Binding backBinding1() {
        return BindingBuilder
                // 设置queue
                .bind(getBackQueue())
                // 绑定交换机
                .to(getBackExchange())
                // 设置routingKey
                .with("back.V1");
    }


    @Bean
    public Binding backBinding2() {
        return BindingBuilder
                // 设置queue
                .bind(getBackQueue2())
                // 绑定交换机
                .to(getBackExchange())
                // 设置routingKey
                .with("back.V2");
    }

}

配置生产

send1为正常请求,send2无法到达exchange,send1无法到达queue。

@Component
@Slf4j
public class BackSender {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send1(User user) {
        this.rabbitTemplate.convertAndSend(
                BackConfig.BACK_EXCHANGE,
                // routingKey
                "back.V1",
                user);
    }

    public void send2(User user) {
        this.rabbitTemplate.convertAndSend(
                BackConfig.BACK_EXCHANGE_NO,
                // routingKey
                "back.V2",
                user);
    }

    public void send3(User user) {
        this.rabbitTemplate.convertAndSend(
                BackConfig.BACK_EXCHANGE,
                // routingKey
                "back.V3",
                user);
    }
}


测试内容

调用接口发送请求

现在我们调用接口:http://localhost:8000/back/send1。 发送消息

控制台可以看到输出内容:

INFO 80928 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 消息唯一标识: null
INFO 80928 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 确认状态: true
INFO 80928 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 造成原因: null
【back-receiveDirect1监听到消息】User(id=1, name=Direct, age=100)

此时只触发了ConfirmCallback,ack为true


现在我们调用接口:http://localhost:8000/back/send2。 发送消息

控制台可以看到输出内容:

INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 消息唯一标识: null
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 确认状态: false
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 造成原因: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'back.exchange.no' in vhost '/', class-id=60, method-id=40)

此时只触发了ConfirmCallback,ack为false


现在我们调用接口:localhost:8000/back/send3。发送消息

控制台可以看到输出内容:

INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitReturnCallback      : 消息主体: (Body:'[B@6351224c(byte[221])' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitReturnCallback      : 回复编码: 312
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitReturnCallback      : 回复内容: NO_ROUTE
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitReturnCallback      : 交换器: back.exchange
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitReturnCallback      : 路由键: back.V3
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 消息唯一标识: null
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 确认状态: true
INFO 74172 --- [7.99.100.0:5672] d.s.r.c.config.RabbitConfirmCallBack     : 造成原因: null

此时触发了ConfirmCallback,ack为true,也触发了ReturnCallback,并返回了异常信息


消息转换

我们之前在发送消息的时候使用了convertAndSend,而我们查看其源码会发现下面内容

	@Override
	public void convertAndSend(String exchange, String routingKey, final Object object, CorrelationData correlationData)
			throws AmqpException {
		send(exchange, routingKey, convertMessageIfNecessary(object), correlationData);
	}
	
	protected Message convertMessageIfNecessary(final Object object) {
		if (object instanceof Message) {
			return (Message) object;
		}
		return getRequiredMessageConverter().toMessage(object, new MessageProperties());
	}
	
	public MessageConverter getMessageConverter() {
		return this.messageConverter;
	}

convertAndSend使用了默认的消息类型转换器,而实际上rabbitMQ也开放了自定义消息转换器的功能。

自定义的消息转换器

要实现自己的消息转换器,只需要继承MessageConverter接口即可。接口提供了两个方法一个是发送消息时候使用,一个是接收消息使用。

@Slf4j
public class MyMessageConverter implements MessageConverter {

    /**
     * 将java对象和属性对象转换成Message对象
     * @param o
     * @param messageProperties
     * @return
     * @throws MessageConversionException
     */
    @Override
    public Message toMessage(Object o, MessageProperties messageProperties) throws MessageConversionException {
        log.info("=======toMessage=========");
        String s = null;
        if (o instanceof User) {
            s = JSON.toJSONString(o);
        } else {
            s = o.toString();
        }
        return new Message(s.getBytes(),messageProperties);
    }

    /**
     * 将消息对象转换成java对象。
     * @param message
     * @return
     * @throws MessageConversionException
     */
    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        log.info("=======fromMessage=========");
        String content = null;
        try {
            content = new String(message.getBody());
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (content == null) {
            //都没有符合,则转换成字节数组
            return message.getBody();
        } else {
            User user = JSON.parseObject(content, User.class);
            return user;
        }
    }
}

配置监听内容

在SimpleMessageListenerContainer的配置中我们将监听队列设置为converter.queue1。并设置了消息转换器MyMessageConverter以及处理类MessageHandler,和配套的处理方法onMessage。而发送消息的转换器我们要配置在RabbitTemplate中。

@Configuration
public class ConverterListenerConfig {

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(new MyMessageConverter());
        return template;
    }

    
    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames("converter.queue1");

        MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageHandler());
        // 指定消息转换器
        adapter.setMessageConverter(new MyMessageConverter());
        // 设置处理器的消费消息的默认方法
        adapter.setDefaultListenerMethod("onMessage");
        container.setMessageListener(adapter);

        return container;
    }
}


测试内容

现在我们调用接口:http://localhost:8000/converter/send1 发送消息

控制台可以看到输出内容:

INFO 80924 --- [nio-8000-exec-2] d.s.r.c.config.MyMessageConverter        : =======toMessage=========
INFO 80924 --- [enerContainer-1] d.s.r.c.config.MyMessageConverter        : =======fromMessage=========
INFO 80924 --- [enerContainer-1] d.s.r.converter.config.MessageHandler    : ---------onMessage-------------
INFO 80924 --- [enerContainer-1] d.s.r.converter.config.MessageHandler    : {"age":100,"id":"1","name":"Converter"}

可以看到User对象呗JSON处理后,又以字符串形式被接收。


配置错误处理

我们现在再回头看看@RabbitListener我们看到它的注解里面内容。之前的例子,使用@RabbitListener注解实现了对消息队列的监听,而实际上可以发现这个注解提供了更多的配置。其中一个参数errorHandler允许开发指定错误处理的逻辑

支持错误处理的监听

下面的代码里面,指定了错误的处理逻辑bean的名称:myReceiverListenerErrorHandler,当消息处理出现异常的时候,异常处理就会产生作用。

@Component
@Slf4j
public class DirectReceiver {

    /**
     * queues是指要监听的队列的名字
     * @param user
     */
    @RabbitListener(queues = DirectConfig.DIRECT_QUEUE1,
    // 注意此类为spring容器中的bean名称
    errorHandler = "myReceiverListenerErrorHandler",
    returnExceptions = "false")
    public void receiveDirect1(User user, Channel channel, Message message) {
        log.info("【receiveDirect1监听到消息】" + user);
        try {
            //告诉服务器收到这条消息 无需再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            log.info("receiver success");
        } catch (IOException e) {
            e.printStackTrace();
            //丢弃这条消息
            log.info("receiver fail");
        }
        throw new RuntimeException("业务异常");
    }

    /**
     * queues是指要监听的队列的名字
     * @param user
     */
    @RabbitListener(queues = DirectConfig.DIRECT_QUEUE2,
            // 注意此类为spring容器中的bean名称
            errorHandler = "myReceiverListenerErrorHandler",
            returnExceptions = "false")
    public void receiveDirect2(User user,Channel channel, Message message)
            throws UnsupportedEncodingException {
        log.info("【receiveDirect2监听到消息】" + user);
        String s = new String(message.getBody(),"UTF-8");
        log.info(s);
        try {
            // 注意此时因为已经关闭了手动确认,所以下面代码可以不使用
            // 告诉服务器收到这条消息 无需再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
            // channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("receiver success");
        } catch (Exception e) {
            e.printStackTrace();
            //丢弃这条消息
            System.out.println("receiver fail");
        }
    }
}

错误处理的方法

上面的myReceiverListenerErrorHandler。其实是被注入到容器中RabbitListenerErrorHandler的实现类。

@Slf4j
@Component(value = "myReceiverListenerErrorHandler")
public class MyReceiverListenerErrorHandler implements RabbitListenerErrorHandler {
    
    private static ConcurrentSkipListMap<Object, AtomicInteger> map = new ConcurrentSkipListMap();
    
    /**
     */
    @Override 
    public Object handleError(Message amqpMessage,
                                        org.springframework.messaging.Message <?> message,
                                        ListenerExecutionFailedException exception)
            throws Exception {
        log.error("消息接收发生了错误,消息内容:{},错误信息:{}",
                JSON.toJSONString(message.getPayload()), 
                JSON.toJSONString(exception.getCause().getMessage()));
        throw new AmqpRejectAndDontRequeueException("超出次数");
    }
}

测试内容

调用接口发送请求

现在我们调用接口:http://localhost:8000/err/send1 发送消息

控制台可以看到输出内容:

ERROR 63476 --- [cTaskExecutor-1] d.s.r.e.h.MyReceiverListenerErrorHandler : 消息接收发生了错误,消息内容:{"age":100,"id":"1","name":"Direct"},错误信息:"业务异常"
WARN 63476 --- [cTaskExecutor-1] s.a.r.l.ConditionalRejectingErrorHandler : Execution of Rabbit message listener failed.

消息进入了异常处理


现在我们调用接口:http://localhost:8000/err/send2 发送正确的消息

控制台可以看到输出内容:

INFO 61860 --- [cTaskExecutor-1] d.s.r.errhandler.direct.DirectReceiver   : 【receiveDirect2监听到消息】User(id=12, name=DirectV2, age=200)
receiver success

可以看到消息并没有被错误处理


本篇文章涉及的源码下载地址:https://gitee.com/daifyutils/springboot-samples

  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值