消息回调
消息队列在实际运用中,作为消息的生产者,很多时候我们需要确认消息是否成功发送到了mq中。同时我们还需要知道,假如消息出现异常时的异常情况。为了满足这个业务场景,我们就需要配置消息回调。
开启消息回调
为了保证消息生产者能够收到消息的回调信息,我么需要修改以下配置,只有添加了下面的配置,才能保证添加相关代码后可以收到系统的回复。
spring:
rabbitmq:
# 开启发送确认
publisher-confirms: true
# 开启发送失败退回
publisher-returns: true
目前回调存在ConfirmCallback
和ReturnCallback
两者。他们的区别在于
- 如果消息没有到exchange,则ConfirmCallback回调,ack=false,
- 如果消息到达exchange,则ConfirmCallback回调,ack=true
- exchange到queue成功,则不回调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
配置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