Spring 重试机制
Spring 中的重试功能可以为我们实际项目做哪些事情?
# 背景
大家在做项目的时候,往往会遇到一些接口由于网络抖动等问题导致接口响应超时等,这时候我们会希望能够按照一定的规则进行接口 请求重试。
# 分析
一般情况下,以上描述的情况,我们可能需要后台的定时任务去重新发起调用,以达到目的,这样无疑会增加开发成本,并且还得考虑
请求报文的保存等等问题。这时我们可以使用Spring
提供的功能来完成这个需求。
# 实现
假设我们现在有一个接口,在这个接口流程中会出现一些异常,比如超时异常(数据库的异常、远程调用的异常等),出现这样的异常 我们就希望能够自动发起重新调用的功能。
# 创建工程
我们为了测试方便就直接创建一个简单的Spring Boot
的工程就可以了。创建的时候引入如下依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2
3
4
5
6
7
8
9
说明:由于
Spring
重试机制的基于注解的AOP
实现,所以我们需要映入AOP
的依赖,我们是Spring Boot
项目直接使用AOP
的启动器就可以了。
# 启动注解
由于我们是继续Spring Boot
来开发这部分代码,所以我们需要配置开启重试机制的的注解,代码如下:
@SpringBootApplication
@EnableRetry
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
2
3
4
5
6
7
8
9
说明:
Spring
重试机制主要是使用注解@Retryable
来实现的
# @Retryable
该注解的源码如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
String recover() default "";
String interceptor() default "";
Class<? extends Throwable>[] value() default {};
Class<? extends Throwable>[] include() default {};
Class<? extends Throwable>[] exclude() default {};
String label() default "";
boolean stateful() default false;
int maxAttempts() default 3;
String maxAttemptsExpression() default "";
Backoff backoff() default @Backoff;
String exceptionExpression() default "";
String[] listeners() default {};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
说明:
value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所有异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略, 默认使用@Backoff,@Backoff的value默认为1000, 以毫秒为单位的延迟(默认 1000)
multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
# 注解@Retryable切面类
该注解主要的切面拦截器如下代码,感兴趣的小伙伴可以自行查看源码,分析。
AnnotationAwareRetryOperationsInterceptor#invoke()
# @Recover
Spring-Retry
还提供了@Recover
注解,用于@Retryable
重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
可以看到传参里面写的是Exception e
,这个是作为回调的标识(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
注意事项:
1、方法的返回值必须与@Retryable方法一致
2、方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
3、该回调方法与重试方法写在同一个实现类里面
4、由于是基于AOP实现,所以不支持类里自调用方法
5、如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
6、方法内不能使用try catch,只能往外抛异常
7、@Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常。
该注解的代码如下:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Recover {
}
2
3
4
5
6
7
# 注解@Recover切面类
AnnotationAwareRetryOperationsInterceptor#getRecoverer()
# 写一个Demo
# 创建接口
首先我们创建一个接口。代码如下:
public interface RetryService {
/**
* 重试方法
* @param str
* @return
* @throws Exception
*/
String retry(String str) throws Exception;
/**
* 回调方法
* @param e
* @param str
* @return
*/
String recover(Exception e,String str);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 接口实现
上述接口的实现的代码如下:
@Service
@Slf4j
public class RetryServiceImpl implements RetryService {
/**
* 重试方法
* @param str
* @return
*/
@Override
@Retryable(value = Exception.class,maxAttempts = 5,backoff = @Backoff(delay = 2000,multiplier = 1.5))
public String retry(String str) throws Exception {
log.info("Service 请求入参为:{}",str);
log.info("进入测试方法,目前时间为:{}",new Date());
if ("succ".equals(str)){
return "succ";
}else {
throw new Exception("异常了!");
}
}
/**
* 重试次数完成后,回调的方法
* @param e
* @param str
* @return
*/
@Override
@Recover
public String recover(Exception e, String str) {
log.info("异常出现后的回调操作,入参为:{},当前时间为:{}", str,LocalDate.now());
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 测试
我们使用postman
进行测试
# 创建测试类
我们创建一个控制器来测试功能。代码如下:
@RestController
@Slf4j
public class RetryController {
@Resource
RetryService retryService;
@GetMapping("/re")
public String retry(@RequestParam("str") String str) throws Exception{
log.info("Controller 请求入参为:{}",str);
return retryService.retry(str);
}
}
2
3
4
5
6
7
8
9
10
11
说明:我们的入参是一个字符串,若传入的字符串非
succ
那么手动抛出异常,我们观察日志,看是否框架自动发起了重试。
# 测试
我们启动Sping Boot
项目,并且进行测试,来看一下效果。
使用postman
请求我们的re方法,我们观察一下日志。请求地址localhost:8111/re
,日志如下:
2023-07-17 21:05:19.488 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.controller.RetryController : Controller 请求入参为:111
2023-07-17 21:05:19.516 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : Service 请求入参为:111
2023-07-17 21:05:19.516 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 进入测试方法,目前时间为:Mon Jul 17 21:05:19 CST 2023
2023-07-17 21:05:21.518 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : Service 请求入参为:111
2023-07-17 21:05:21.519 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 进入测试方法,目前时间为:Mon Jul 17 21:05:21 CST 2023
2023-07-17 21:05:24.522 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : Service 请求入参为:111
2023-07-17 21:05:24.523 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 进入测试方法,目前时间为:Mon Jul 17 21:05:24 CST 2023
2023-07-17 21:05:29.024 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : Service 请求入参为:111
2023-07-17 21:05:29.025 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 进入测试方法,目前时间为:Mon Jul 17 21:05:29 CST 2023
2023-07-17 21:05:35.779 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : Service 请求入参为:111
2023-07-17 21:05:35.779 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 进入测试方法,目前时间为:Mon Jul 17 21:05:35 CST 2023
2023-07-17 21:05:35.790 INFO 2007 --- [nio-8111-exec-2] o.t.s.l.service.impl.RetryServiceImpl : 异常出现后的回调操作,入参为:111,当前时间为:2023-07-17
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,出现异常后框架自动发起了重试,在重试次数使用完成后,回调了异常处理的方法。
# 小结
本篇文章,我们简单的在实现层面整理了使用Spring-Retry
的重试机制,后续有机会带着大家分析一下这部分源码,详细代码,大家可以直接进行下载使用源码地址,
希望对大家有所帮助。