然后在需要重试的方法上加上注解:
@Retryable(retryTimes = 4, retryInterval = 2) public String hello(){ long times = helloTimes.incrementAndGet(); log.info("hello times:{}", times); if (times % 4 != 0){ log.warn("发生异常,time:{}", LocalTime.now() ); throw new HelloRetryException("发生Hello异常"); } return "hello " + nameService.getName(); }接着,进行最后一步,编写AOP切面:
@Slf4j @Aspect @Component public class RetryAspect { @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)") private void retryMethodCall(){} @Around("retryMethodCall()") public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException { // 获取重试次数和重试间隔 Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class); int maxRetryTimes = retry.retryTimes(); int retryInterval = retry.retryInterval(); Throwable error = new RuntimeException(); for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){ try { Object result = joinPoint.proceed(); return result; } catch (Throwable throwable) { error = throwable; log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes); } Thread.sleep(retryInterval * 1000); } throw new RetryExhaustedException("重试次数耗尽", error); } }开始测试:
@Autowired private HelloService helloService; @Test public void helloAOP(){ String hello = helloService.hello(); log.info("hello:{}", hello); }输出如下:
hello times:1 发生异常,time:16:49:30.224649800 调用发生异常,开始重试,retryTimes:1 hello times:2 发生异常,time:16:49:32.225230800 调用发生异常,开始重试,retryTimes:2 hello times:3 发生异常,time:16:49:34.225968900 调用发生异常,开始重试,retryTimes:3 hello times:4 hello:hello Frank这样就相当优雅了,一个注解就能搞定重试,简直不要更棒。
Spring 的重试注解实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。
那让我们先来看看这个轮子究竟好不好使。
先引入重试所需的jar包:
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?竟然抄袭我的注解! [手动滑稽] )
@Retryable public String hello(){ long times = helloTimes.incrementAndGet(); log.info("hello times:{}", times); if (times % 4 != 0){ log.warn("发生异常,time:{}", LocalTime.now() ); throw new HelloRetryException("发生Hello异常"); } return "hello " + nameService.getName(); }默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。
但Spring里的重试机制还支持很多很有用的特性,比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。
@Retryable{value = {HelloRetryException.class}} public String hello(){2 ... }也可以使用include和exclude来指定包含或者排除哪些异常进行重试。
可以用maxAttemps指定最大重试次数,默认为3次。
可以用interceptor设置重试拦截器的bean名称。
可以通过label设置该重试的唯一标志,用于统计输出。
可以使用exceptionExpression来添加异常表达式,在抛出异常后执行,以判断后续是否进行重试。
此外,Spring中的重试机制还支持使用backoff来设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数。
举个例子:
@Retryable(value = {HelloRetryException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2)) public String hello(){ ... }该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。
重试机制还支持使用@Recover 注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。
这里值得注意的是,想要@Recover 注解生效的话,需要跟被@Retryable 标记的方法在同一个类中,且被@Retryable 标记的方法不能有返回值,否则不会生效。
并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。
@Recover public boolean recover(Exception e) { log.error("达到最大重试次数",e); return false; }