翻源码
源码之下无秘密。
首先我们看一下前面找到的 Debug 入口:
org.springframework.retry.support.RetryTemplate#doExecute
从日志里面可以直观的看出,这个方法里面肯定就包含我要找的 for 循环。
但是...
很遗憾,并不是 for 循环,而是一个 while 循环。问题不大,意思差不多:
打上断点,然后把项目跑起来,跑到断点的地方我最关心的是下面的调用堆栈:
被框起来了两部分,一部分是 spring-aop 包里面的内容,一部分是 spring-retry。
然后我们看到 spring-retry 相关的第一个方法:
恭喜你,如果说前面通过日志找到了第一个打断点的位置,那么通过第一个断点的调用堆栈,我们找到了整个 retry 最开始的入口处,另外一个断点就应该打在下面这个方法的入口处:
org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor#invoke
说真的,观察日志加调用栈这个最简单的组合拳用好了,调试绝大部分源码的过程中都不会感觉特别的乱。
找到了入口了,我们就从接口处接着看源码。
这个 invoke 方法一进来首先是试着从缓存中获取该方法是否之前被成功解析过,如果缓存中没有则解析当前调用的方法上是否有 @Retryable 注解。
如果是被 @Retryable 修饰的,返回的 delegate 对象则不会是 null。所以会走到 retry 包的代码逻辑中去。
然后在 invoke 这里有个小细节,如果 recoverer 对象不为空,则执行带回调的。如果为空则执行没有 recoverCallback 对象方法。
我看到这几行代码的时候就大胆猜测: @Recover 注解并不是必须的。
于是我兴奋的把这个方法注解掉并再次运行项目,发现还真是,有点不一样了:
在我没有看其他文章、没有看官方介绍,仅通过一个简单的示例就发掘到他的一个用法之后,这属于意外收获,也是看源码的一点小乐趣。
其实源码并没有那么可怕的。
但是看到这里的时候另外一个问题就随之而来了:
这个 recoverer 对象看起来就是我写的 channelNotResp 方法,但是它是在什么时候解析到的呢?
按下不表,后面再说,当务之急是找到重试的地方。
在当前的这个方法中再往下走几步,很快就能到我前面说的 while 循环中来:
主要关注这个 canRetry 方法:
org.springframework.retry.RetryPolicy#canRetry
点进去之后,发现是一个接口,拥有多个实现:
简单的介绍一下其中的几种含义是啥:
- AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
- NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
- SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
- TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
- ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
- CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
- CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即不可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
那么这里问题又来了,我们调试源码的时候这么有多实现,我怎么知道应该进入哪个方法呢?
记住了,接口的方法上也是可以打断点的。你不知道会用哪个实现,但是 idea 知道:
这里就是用的 SimpleRetryPolicy 策略,即这个策略是 Spring-retry 的默认重试策略。
t == null || retryForException(t)) && context.getRetryCount() < this.maxAttempts
这个策略的逻辑也非常简单:
- 1.如果有异常,则执行 retryForException 方法,判断该异常是否可以进行重试。
- 2.判断当前已重试次数是否超过最大次数。
在这里,我们找到了控制重试逻辑的地方。
上面的第二点很好理解,第一点说明这个注解和事务注解 @Transaction 一样,是可以对指定异常进行处理的,可以看一眼它支持的选项:
注意 include 里面有句话我标注了起来,意思是说,这个值默认为空。且当 exclude 也为空时,默认是所有异常。
所以 Demo 里面虽然什么都没配,但是抛出 TimeoutException 也会触发重试逻辑。
又是一个通过翻源码挖掘到的知识点,这玩意就像是探索彩蛋似的,舒服。
看完判断是否能进行重试调用的逻辑之后,我们接着看一下真正执行业务方法的地方:
org.springframework.retry.RetryCallback#doWithRetry