SpringBoot @Retryable注解
背景在调用第三方接口或者使用MQ时,会出现网络抖动,连接超时等网络异常,所以需要重试。为了使处理更加健壮并且不太容易出现故障,后续的尝试操作,有时候会帮助失败的操作最后执行成功。一般情况下,需要我们自行实现重试机制,一般是在业务代码中加入一层循环,如果失败后,再尝试重试,但是这样实现并不优雅。在SpringBoot中,已经实现了相关的能力,通过@Retryable注解可以实现我们想要的结果。@..
背景
在调用第三方接口或者使用MQ时,会出现网络抖动,连接超时等网络异常,所以需要重试。为了使处理更加健壮并且不太容易出现故障,后续的尝试操作,有时候会帮助失败的操作最后执行成功。一般情况下,需要我们自行实现重试机制,一般是在业务代码中加入一层循环,如果失败后,再尝试重试,但是这样实现并不优雅。在SpringBoot中,已经实现了相关的能力,通过@Retryable注解可以实现我们想要的结果。
@Retryable
首先来看一下Spring官方文档的解释:
@Retryable注解可以注解于方法上,来实现方法的重试机制。
POM依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
使用实例
SpringBoot retry的机制比较简单,只需要两个注解即可实现。
启动类
@SpringBootApplication
@EnableRetry
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在启动类上,需要加入@EnableRetry注解,来开启重试机制。
Service类
前面提到过,@Retryable是基于方法级别的,因此在Service中,需要在你希望重试的方法上,增加重试注解。
@Service
@Slf4j
public class DoRetryService {
@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public boolean doRetry(boolean isRetry) throws Exception {
log.info("开始通知下游系统");
log.info("通知下游系统");
if (isRetry) {
throw new RuntimeException("通知下游系统异常");
}
return true;
}
}
来简单解释一下注解中几个参数的含义:
名称 | 含义 |
---|---|
interceptor | Retry interceptor bean name to be applied for retryable method. |
value | Exception types that are retryable. Synonym for includes(). Defaults to empty (and if excludes is also empty all exceptions are retried). |
include | Exception types that are retryable. Defaults to empty (and if excludes is also empty all exceptions are retried). |
exclude | Exception types that are not retryable. Defaults to empty (and if includes is also empty all exceptions are retried). |
label | A unique label for statistics reporting. If not provided the caller may choose to ignore it, or provide a default. |
stateful | Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the retry policy is applied with the same policy to subsequent invocations with the same arguments. If false then retryable exceptions are not re-thrown. |
maxAttempts | the maximum number of attempts (including the first failure), defaults to 3 |
maxAttemptsExpression | an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3 |
backoff | Specify the backoff properties for retrying this operation. The default is a simple specification with no properties. |
exceptionExpression | Specify an expression to be evaluated after the SimpleRetryPolicy.canRetry() returns true - can be used to conditionally suppress the retry. |
listeners | Bean names of retry listeners to use instead of default ones defined in Spring context. |
上面是@Retryable的参数列表,参数较多,这里就选择几个主要的来说明一下:
interceptor
:可以通过该参数,指定方法拦截器的bean名称value
:抛出指定异常才会重试include
:和value一样,默认为空,当exclude也为空时,默认所以异常exclude
:指定不处理的异常maxAttempts
:最大重试次数,默认3次backoff
:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
我们把上面的例子执行一下,来看看效果:
2019-12-25 11:38:02.492 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 开始通知下游系统
2019-12-25 11:38:02.493 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 通知下游系统
2019-12-25 11:38:04.494 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 开始通知下游系统
2019-12-25 11:38:04.495 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 通知下游系统
2019-12-25 11:38:07.496 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 开始通知下游系统
2019-12-25 11:38:07.496 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 通知下游系统
2019-12-25 11:38:11.997 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 开始通知下游系统
2019-12-25 11:38:11.997 INFO 25664 --- [ main] c.f.l.service.impl.DoRetryServiceImpl : 通知下游系统
java.lang.RuntimeException: 通知下游系统异常
...
...
...
可以看到,三次之后抛出了RuntimeException
的异常。
@Recover
当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法,此方法里的异常一定要是@Retryable方法里抛出的异常,否则不会调用这个方法。
@Recover
public boolean doRecover(Throwable e, boolean isRetry) throws ArithmeticException {
log.info("全部重试失败,执行doRecover");
return false;
}
对于@Recover注解的方法,需要特别注意的是:
1、方法的返回值必须与@Retryable方法一致
2、方法的第一个参数,必须是Throwable
类型的,建议是与@Retryable配置的异常一致,其他的参数,需要与@Retryable方法的参数一致
/**
* Annotation for a method invocation that is a recovery handler. A suitable recovery
* handler has a first parameter of type Throwable (or a subtype of Throwable) and a
* return value of the same type as the <code>@Retryable</code> method to recover from.
* The Throwable first argument is optional (but a method without it will only be called
* if no others match). Subsequent arguments are populated from the argument list of the
* failed method in order.
*/
@Recover不生效的问题
在测试过程中,发现@Recover无法生效,执行时抛出异常信息:
org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.ArithmeticException: / by zero
at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:61)
at org.springframework.retry.interceptor.RetryOperationsInterceptor$ItemRecovererCallback.recover(RetryOperationsInterceptor.java:141)
at org.springframework.retry.support.RetryTemplate.handleRetryExhausted(RetryTemplate.java:512)
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:351)
at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:180)
at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:115)
at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy157.doRetry(Unknown Source)
追踪一下异常的信息,进入到RecoverAnnotationRecoveryHandler
中,找到报错的方法public T recover(Object[] args, Throwable cause)
,看一下其实现:
发现报错处,是因为method
为空而导致的,明明我已经在需要执行的方法上注解了@Recover,为什么还会找不到方法呢?很奇怪,再来深入追踪一下:
打断点到这,发现methods列表是空的,那么methods列表是什么时候初始化的呢?继续追踪:
发现了初始化methods列表的地方,这里会扫描注解了@Recover注解的方法,将其加入到methds列表中,那么为什么没有扫描到我们注解了的方法呢?
很奇怪,为什么明明注解了@Recover,这里却没有扫描到呢?
我有点怀疑Spring扫描的部分,可能有什么问题了,回头去看看@EnableRetry
是怎么说的:
终于找到问题的所在了,看一下@EnableRetry
中的proxyTargetClass
参数的含义:
true:表示使用基于类的代理。在这种模式下,Spring AOP 会使用CGLIB来生成目标对象的子类作为代理对象。
这种方式能够确保被代理的类(即被注解的类)的所有方法都能被拦截,包括非公开的方法。
false:表示使用基于接口的代理。在这种模式下,Spring AOP 将使用JDK动态代理来生成代理对象。
这种方式要求被代理的类必须实现至少一个接口,只有接口中定义的方法才能被拦截。
默认的情况下,该参数是不开启的,问题原因就是这个,我们来实验一下,把这个参数改成true
:
@EnableRetry(proxyTargetClass = true)
再次运行,果然没有问题了。
由此得出结论,当使用接口实现的bean时,可以使用默认配置,即false
,如果是非接口的实现,则需要将EnableRetry
的参数改为true
。
结语
本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景为在调用第三方接口或者使用MQ时。由于会出现网络抖动,连接超时等网络异常。
更多推荐
所有评论(0)