RabbitMQ 源码解析:异常处理

1,770 阅读6分钟
原文链接: yemengying.com

这应该是过年假期的最后一篇,如果不是,那你一定看到了假博客。(๑•̀ㅂ•́)و✧

在消费 RabbitMq 中的 Message 时,常常会出现异常,可能是 Message 本身格式不对,或者由于某些原因无法被处理。我一般都是 catch 异常然后抛个 AmqpRejectAndDontRequeueException (以下简称 ARADRE ),也出啥问题,不过还是仔细看下,rabbitmq 是如何对待消费消息时出现的异常,是会将消息直接丢弃还是有其他操作。

其实 Spring-amqp 官方文档上对于 RabbitMq 是如何处理异常的说的已经很明白了,不过都是大段的文字可能不太好理解,还是配着代码看一下。

代码版本: 1.6.3.RELEASE

根据官方文档,当 listener 在消费消息时抛出一个异常的时候,该异常会被包装在 ListenerExecutionFailedException 中抛出,并根据 listenerContainerdefaultRequeueRejected 设定的值来决定是否将该消息重新加入队列,默认是会重新加入队列。

ListenerExecutionFailedException结构ListenerExecutionFailedException结构

需要注意的是,如果抛出的异常是 ARADRE 或其他被 RabbitMq 认为是致命错误的异常,即便 defaultRequeueRejected 的值为 true , 该消息也不会重新加入队列,而是会被直接丢弃或加入 dead-letter-exchange 中(如果有配置 dead-letter-exchange)。

来源https://derickbailey.com/2016/03/28/dealing-with-dead-letters-and-poison-messages-in-rabbitmq/来源https://derickbailey.com/2016/03/28/dealing-with-dead-letters-and-poison-messages-in-rabbitmq/

在 1.6.3. RELEASE 中被 RabbitMq 认为是致命错误的异常有以下 6 种:

  • o.s.amqp…MessageConversionException
  • o.s.messaging…MessageConversionException
  • o.s.messaging…MethodArgumentNotValidException
  • o.s.messaging…MethodArgumentTypeMismatchException
  • java.lang.NoSuchMethodException
  • java.lang.ClassCastException

也就是说,当抛出以上异常及 ARADRE 时,该消息一定不会重新入队,即便 defaultRequeueRejected 的值为 true。

下面看看 Spring-RabbitMq 是如何实现的:

在源码中,异常在 AbstractMessageListenerContainer 中被包装在 ListenerExecutionFailedException 中之后还会经由 ErrorHandlerhandleError 方法处理, 默认的 ErrorHandler 是 ConditionalRejectingErrorHandler

我们也可以实现自己的 ErrorHandler 来控制需要丢弃消息的异常,只要实现 org.springframework.util.ErrorHandler 接口,然后将listenerContainer 中的 errorHandler 参数指定我们自定义的 handler 即可。

ConditionalRejectingErrorHandler 中配置有 FatalExceptionStrategy,会调用 FatalExceptionStrategy 中的 isFatal 方法来判断异常是不是属于致命异常。

ConditionalRejectingErrorHandler 的具体实现如下:

public class ConditionalRejectingErrorHandler implements ErrorHandler {
private final FatalExceptionStrategy exceptionStrategy;    
@Override
public void handleError(Throwable t) {
 if (this.logger.isWarnEnabled()) {
      this.logger.warn("Execution of Rabbit message listener failed.", t);
    }
   // 如果是致命异常,则转为 AmqpRejectAndDontRequeueException 抛出
 if (!this.causeChainContainsARADRE(t) && this.exceptionStrategy.isFatal(t)) {
       throw new AmqpRejectAndDontRequeueException("Error Handler converted exception to fatal", t);
   }
   }
/**
 * @return true if the cause chain already contains an
 * {@link AmqpRejectAndDontRequeueException}.
 */
 private boolean causeChainContainsARADRE(Throwable t) {
  Throwable cause = t.getCause();
 while (cause != null) {
     if (cause instanceof AmqpRejectAndDontRequeueException) {
           return true;
        }
       cause = cause.getCause();
   }
   return false;
   }
/**
 * Default implementation of {@link FatalExceptionStrategy}.
 * @since 1.6.3
 */
 public class DefaultExceptionStrategy implements FatalExceptionStrategy {
    // 判断传入参数 是不是 致命异常
  @Override
   public boolean isFatal(Throwable t) {
       if (t instanceof ListenerExecutionFailedException
               && isCauseFatal(t.getCause())) {
            if (ConditionalRejectingErrorHandler.this.logger.isWarnEnabled()) {
             ConditionalRejectingErrorHandler.this.logger.warn(
                          "Fatal message conversion error; message rejected; "
                            + "it will be dropped or routed to a dead letter exchange, if so configured: "
                          + ((ListenerExecutionFailedException) t).getFailedMessage());
           }
           return true;
        }
       return false;
   }
   private boolean isCauseFatal(Throwable cause) {
     return cause instanceof MessageConversionException
              || cause instanceof org.springframework.messaging.converter.MessageConversionException
              || cause instanceof MethodArgumentNotValidException
             || cause instanceof MethodArgumentTypeMismatchException
             || cause instanceof NoSuchMethodException
               || cause instanceof ClassCastException
              || isUserCauseFatal(cause);
 }
   /**
  * 通过重写该方法来添加自定义的异常
  * Subclasses can override this to add custom exceptions.
    * @param cause the cause
    * @return true if the cause is fatal.
   */
 protected boolean isUserCauseFatal(Throwable cause) {
       return false;
   }
  }
  }

代码比较长,简单来说,就是 ConditionalRejectingErrorHandler 的 handleError 会先判断接到的异常中的 cause 是不是 ARADRE,如果不是再调用 FatalExceptionStrategy 的 isFatal 方法,判断是不是致命异常中的一种,如果是,则将异常转为 ARADRE 抛出,该消息也就不会重新入队。

如果想要把自定义的异常加入到 fatalException, 一个简单的办法就是提供新的 FatalExceptionStrategy ,只要继承 ConditionalRejectingErrorHandler.DefaultExceptionStrategy 并重写 isUserCauseFatal(Throwable cause) 方法,在方法里对于需要丢弃消息的异常返回 true即可。

再简单看下,RabbitMq 判断是否需要将消息重入队列的部分逻辑。

// We should always requeue if the container was stopping
boolean shouldRequeue = this.defaultRequeuRejected || ex instanceof MessageRejectedWhileStoppingException;
Throwable t = ex;
while (shouldRequeue && t != null) {
 if (t instanceof AmqpRejectAndDontRequeueException) {
           shouldRequeue = false;
  }
   t = t.getCause();
   }

根据上面的代码,如果处理消息时出现异常,在判断是否需要入队时,会将 shouldRequeue 变量等于 this.defaultRequeuRejected ||ex instanceof MessageRejectedWhileStoppingException 的值,然后如果异常是 ARADRE, 不管之前 shouldRequeue 的值是什么,都会被置为 false。最后根据 shouldRequeue 的值来决定是否需要重新入队。

本来还想画个图的,不过要去和 cookie 玩会,有空再补吧~~

最后。。Cookie 宝宝祝大家新春快乐~~ cookie 宝宝cookie 宝宝

下一篇 为Rabbitmq中的Jackson2JsonMessageConverter自定义ClassMapper

这应该是过年假期的最后一篇,如果不是,那你一定看到了假博客。(๑•̀ㅂ•́)و✧ 在消费 RabbitMq 中的 Message 时,常常会出现异常,可能是 Message 本身格式不对,或者由于某些原因无法被处理。我一般都是 catch 异常然后抛个 AmqpRejectAndDontRequeu

新年第一篇~~ 🐣🐥🐤🐔 消息队列算是各个系统间通信比较常见的方式了。我们公司用的是是基于 AMQP 协议的 RabbitMq。在 Spring-AMQP 中比较重要的类就是 Message,因为要发送的消息必须要构造成一个 Message 对象来进行传输。Message 对象包括两部分 B

和上篇内容并不重复 🙃 最近由于规则引擎有问题,导致产线上的一个 job 会抛 NullPointerException。本来这是个已知的问题,也没什么,已经联系对应的人去修复了。可由此发现了另外一个问题, fireman 的告警邮件只有异常的名称,而没有异常堆栈。 这就很令人懵圈了,因为不知道是

最近提的 PR 都有关于 Log 的 comment,不能忍,以下内容总结整理自明佳的 Comment 和网络资料,只是为了以后提 PR 之前过来扫一眼,尽量避免 Log 上的疏忽,不一定适用于所有人。 在程序中的适当位置打 Log 的重要性就不用多说了,很多人应该都体会过线上有 Bug 却由于没有

第一次转别人的博客,想看原文的请移步三“观”茅庐,我才不会告诉你们原文有大神高清无码照的!!! 先说点自己想说的, EVE 是毕业后第一份工作做的第一个产品(也可以说第一份工作做得唯一的产品,后面的那个我实在不想承认是我写的🙄),做 EVE 的那段日子是到目前为止毕业后最开心的时光,学习到了很多东

普通程序员,毕业于东中国正常大学(校友都懂哈),目前从事Java开发,对python和搜索也有所了解。生活大爆炸和Running man脑残粉,因为很喜欢李光洙(外号长颈鹿),所以博客取名为Giraffe’s Home,用来记录学习思考中的一些收获,也许会有错误,也许并不完美,但这不就是成长的过程么

三”观”茅庐(一个做过后端,前端,目前在做移动端的程序猴的博客)默默小屋(性情温顺,主业写代码)James Pan’s Blog(真★大神的博客)Jiaxi’s Home(share various techniques & life)仓鼠君(萌萌哒仓鼠君的blog)

欢迎大家留言~ 一起交流,学习,努力,成长吧 🎉🎉🎉~~