还在手写重试机制?试试Spring-Retry吧

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 在工作中,我们经常会碰到需要调用远程方法的业务,这时候,如果超时了,或者异常了,我们都会让其重试几次,达到一定的重试次数以后,就返回异常信息,今天我们就来了解下Spring-Retry的用法以及实现原理是怎么样的

在工作中,我们经常会碰到需要调用远程方法的业务,这时候,如果超时了,或者异常了,我们都会让其重试几次,达到一定的重试次数以后,就返回异常信息,今天我们就来了解下Spring-Retry的用法以及实现原理是怎么样的

Spring-Retry用法

因为Spring-Retry是基于Spring AOP机制实现的,所以需要引入AOP依赖

js复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>spring-retry</artifactId>
    <dependencies>
        <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>
    </dependencies>
</project>

启动类

js复制代码@RestController
//开启Spring-Retry重试机制
@EnableRetry
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
    @Resource
    private RetryService retryService;
    @GetMapping("/test")
    public String test(@RequestParam("code") Integer code) throws Exception{
        retryService.retry(code);
        return "ok";
    }
}
js复制代码package com.coco.service.impl;
import com.coco.service.RetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class RetryServiceImpl implements RetryService {
    /**
     * value:抛出指定异常才会重试
     * include:和value一样,默认为空,当exclude也为空时,默认所有异常
     * exclude:指定不处理的异常
     * maxAttempts:最大重试次数,默认3次
     * backoff:重试等待策略,
     * 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
     * multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
     */
    @Retryable(value = RuntimeException.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
    @Override
    public void retry(int code) throws Exception {
        System.out.println("retry被调用了");
        if (code==0){
            throw new IOException("调用失败,重试");
        }
        System.out.println("调用成功");
    }
    /**
     * Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。
     * 如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
     * 可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
     * 注意事项:
     * 方法的返回值必须与@Retryable方法一致
     * 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
     * 该回调方法与重试方法写在同一个实现类里面
     *
     * 由于是基于AOP实现,所以不支持类里自调用方法
     * 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
     * 方法内不能使用try catch,只能往外抛异常
     * @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。
     */
    @Recover
    public void recover(Exception e, int code){
        System.out.println("回调方法执行!!!!");
        //记日志到数据库 或者调用其余的方法
        System.out.println("异常信息:"+e.getMessage());
    }
}

启动项目,浏览器访问
http://localhost:8080/test?code=0 即可看到效果了

其实Spring-Retry的用法还是很简单的,接下来我们来分析下它的底层是如何实现的

Spring-Retry底层实现原理

其实当你要去查看一个框架的底层实现原理的时候,最难的就是找入口,你首先要找到该从哪里开始分析,这是最难。在这里我分享二个我看源码的小技巧

  • 首先看注解,比如我们这里的启动类上的@EnableRetry
js复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
//注解里我们尤其要关注@Import注解,因为这是Spring将一个Bean注入到容器中的
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {
   /**
    * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed to
    * standard Java interface-based proxies. The default is {@code false}.
    * @return whether to proxy or not to proxy the class
    */
   boolean proxyTargetClass() default false;
}

RetryConfiguration.class实现了InitializingBean接口,那么在这个类初始化之后就会调用afterPropertiesSet()方法

但是看了这个方法之后,我们也很难找到入口的地方,唯一能看到的就是构建AOP的切面和通知

js复制代码@Override
public void afterPropertiesSet() throws Exception {
   this.retryContextCache = findBean(RetryContextCache.class);
   this.methodArgumentsKeyGenerator = findBean(MethodArgumentsKeyGenerator.class);
   this.newMethodArgumentsIdentifier = findBean(NewMethodArgumentsIdentifier.class);
   this.retryListeners = findBeans(RetryListener.class);
   this.sleeper = findBean(Sleeper.class);
   Set<Class<? extends Annotation>> retryableAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
   retryableAnnotationTypes.add(Retryable.class);
   //构建AOP切面和通知
   this.pointcut = buildPointcut(retryableAnnotationTypes);
   this.advice = buildAdvice();
   if (this.advice instanceof BeanFactoryAware) {
      ((BeanFactoryAware) this.advice).setBeanFactory(this.beanFactory);
   }
}

既然我们从注解不能找到入口,那么就从日志入手

  • 看日志

通过日志我们可以看到
RetryOperationsInterceptor.invoke()这段方法,那么在执行重试的时候,肯定也调用这个方法,所以我们直接进入到这个类中,RetryOperationsInterceptor本质是一个拦截器,从类名我们可以推断出,这个拦截器就是拦截有@Retryable注解的方法

所以我们可以直接关注拦截器的核心方法invoke()

js复制代码@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
   String name;
   if (StringUtils.hasText(this.label)) {
      name = this.label;
   }
   else {
      name = invocation.getMethod().toGenericString();
   }
   final String label = name;
   //初始化重试机制的回调函数,这里是重点,在重试执行我们的业务逻辑的时候,就会进入到
   //这里回调函数中,然后执行doWithRetry()方法,但是第一次只是初始化,并不会进入到这里面
   RetryCallback<Object, Throwable> retryCallback = new MethodInvocationRetryCallback<Object, Throwable>(
         invocation, label) {
      @Override
      public Object doWithRetry(RetryContext context) throws Exception {
         context.setAttribute(RetryContext.NAME, this.label);
         if (this.invocation instanceof ProxyMethodInvocation) {
            context.setAttribute("___proxy___", ((ProxyMethodInvocation) this.invocation).getProxy());
            try {
               return ((ProxyMethodInvocation) this.invocation).invocableClone().proceed();
            }
            catch (Exception e) {
               throw e;
            }
            catch (Error e) {
               throw e;
            }
            catch (Throwable e) {
               throw new IllegalStateException(e);
            }
         }
         else {
            throw new IllegalStateException(
                  "MethodInvocation of the wrong type detected - this should not happen with Spring AOP, "
                        + "so please raise an issue if you see this exception");
         }
      }
   };
   //还记得我们在自己RetryServiceImpl中实现了一个方法recover(),并且用@Recover标记
   //如果我们实现了这个方法,那么this.recoverer就不为空,就会进入到if分支里面去
   //最后调用this.retryOperations.execute()方法
   if (this.recoverer != null) {
      ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(),
            this.recoverer);
      try {
         Object recovered = this.retryOperations.execute(retryCallback, recoveryCallback);
         return recovered;
      }
      finally {
         RetryContext context = RetrySynchronizationManager.getContext();
         if (context != null) {
            context.removeAttribute("__proxy__");
         }
      }
   }
   //如果我们自己没有实现recover()方法,那么this.recoverer就等于null,就会直接进入到这里面来了
   return this.retryOperations.execute(retryCallback);
}
js复制代码public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
      RecoveryCallback<T> recoveryCallback) throws E {
   //继续进入doExecute方法
   return doExecute(retryCallback, recoveryCallback, null);
}
js复制代码protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
      RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException {
   //RetryPolicy这个对象包含二个属性
   //maxAttempts:也就是重试的最大次数,当达到这个次数之后就不会再次重试了
   //retryableClassifier:还记得我们加在方法上的@Retryable(value = RuntimeException.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
   //这里设置了一个异常类型,表示的是只有返回的是这个类型的异常才会进行重试
   //如果返回的是其它类型的异常就不会进行重试,所以retryableClassifier这个值就是保存注解
   //里面value设置的异常类型
   RetryPolicy retryPolicy = this.retryPolicy;
   BackOffPolicy backOffPolicy = this.backOffPolicy;
   //初始化我们当前线程重试的上下文
   //在上下文中有一个很重的属性count,初始化的时候这个值为0,后续重试一次,这个值就会加1
   RetryContext context = open(retryPolicy, state);
   //将上下文保存到ThreadLocal中,也是防止并发安全
   RetrySynchronizationManager.register(context);
   Throwable lastException = null;
   boolean exhausted = false;
   try {
      // 给客户一个机会来增强上下文。。。,这里不是重点
      boolean running = doOpenInterceptors(retryCallback, context);
      if (!running) {
         throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
      }
      BackOffContext backOffContext = null;
      Object resource = context.getAttribute("backOffContext");
      if (resource instanceof BackOffContext) {
         backOffContext = (BackOffContext) resource;
      }
      if (backOffContext == null) {
         backOffContext = backOffPolicy.start(context);
         if (backOffContext != null) {
            context.setAttribute("backOffContext", backOffContext);
         }
      }
     //核心方法
     //这里就是重试机制实现的核心实现,首先这里是一个while循环
     //我们看第一个方法canRetry(retryPolicy, context),意思就是是否可以重试
      while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
         try {
            lastException = null;
            //如果可以重试,就会执行doWithRetry()方法
            //在之前我们分析RetryOperationsInterceptor类中的invoke()方法的时候,在那里
            //已经实现了回调方法,所以此时就会进入到那个回调方法中
            return retryCallback.doWithRetry(context);
         }
         catch (Throwable e) {
            lastException = e;
            try {
               registerThrowable(retryPolicy, state, context, e);
            }
            catch (Exception ex) {
               throw new TerminatedRetryException("Could not register throwable", ex);
            }
            finally {
               doOnErrorInterceptors(retryCallback, context, e);
            }
            if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
               try {
                  backOffPolicy.backOff(backOffContext);
               }
               catch (BackOffInterruptedException ex) {
                  lastException = e;
                  throw ex;
               }
            }
            if (shouldRethrow(retryPolicy, context, state)) {
               if (this.logger.isDebugEnabled()) {
                  this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
               }
               throw RetryTemplate.<E>wrapIfNecessary(e);
            }
         }
         if (state != null && context.hasAttribute(GLOBAL_STATE)) {
            break;
         }
      }
      exhausted = true;
      return handleRetryExhausted(recoveryCallback, context, state);
   }
   catch (Throwable e) {
      throw RetryTemplate.<E>wrapIfNecessary(e);
   }
   finally {
      //清除上下文信息
      close(retryPolicy, context, state, lastException == null || exhausted);
      doCloseInterceptors(retryCallback, context, lastException);
      //将ThreadLocal中的上下文信息清除1掉
      RetrySynchronizationManager.clear();
   }
}

在上述中我们发现有两个核心的方法,一个就是 canRetry(retryPolicy, context),还有一个就是retryCallback.doWithRetry(context);

js复制代码protected boolean canRetry(RetryPolicy retryPolicy, RetryContext context) {
   //进入这个方法
   return retryPolicy.canRetry(context);
}

具体的实现类是SimpleRetryPolicy

js复制代码public boolean canRetry(RetryContext context) {
   Throwable t = context.getLastThrowable();
   //retryForException(t):判断返回的异常是否跟我们注解设置的异常类型一致,
   //                      在分析RetryPolicy对象中有个属性就保存了我们注解设置的异常类型
   //context.getRetryCount() < getMaxAttempts():重试次数是否已经达到了我们设置的最大次数
   return (t == null || retryForException(t)) && context.getRetryCount() < getMaxAttempts();
}

如果返回的异常类型与我们设置的一样,并且重试次数还没有达到,那么就会进入到while循环中执行retryCallback.doWithRetry(context);方法

js复制代码
//这段代码就是RetryOperationsInterceptor拦截器中的invoke()方法,我把这段代码截取出来了
RetryCallback<Object, Throwable> retryCallback = new MethodInvocationRetryCallback<Object, Throwable>(invocation, name) {
    //执行这段方法
    public Object doWithRetry(RetryContext context) throws Exception {
        context.setAttribute("context.name", this.label);
        if (this.invocation instanceof ProxyMethodInvocation) {
            context.setAttribute("___proxy___", ((ProxyMethodInvocation)this.invocation).getProxy());
            try {
                // 这里就是执行我们自己的业务逻辑了,如果有异常就抛出,然后在重试机制的
                // while循环中捕获,继而判断异常是否符合并且重试次数是否达到,如果条件符合
                //就继续重试执行,如果不符合,就不会再重试了
                return ((ProxyMethodInvocation)this.invocation).invocableClone().proceed();
            } catch (Exception var3) {
                throw var3;
            } catch (Error var4) {
                throw var4;
            } catch (Throwable var5) {
                throw new IllegalStateException(var5);
            }
        } else {
            throw new IllegalStateException("MethodInvocation of the wrong type detected - this should not happen with Spring AOP, so please raise an issue if you see this exception");
        }
    }
};

所以在我们使用Spring-Retry的时候,设置的异常类型一定要一致,否则这个重试机制就不会生效了

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
181 2
|
4月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
18天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
28 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
14天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
27 2
|
5月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
58 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
5月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
122 0
|
2月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
182 2
|
2月前
|
数据采集 监控 Java
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
本文是关于SpringBoot日志的详细教程,涵盖日志的定义、用途、SLF4J框架的使用、日志级别、持久化、文件分割及格式配置等内容。
161 0
SpringBoot日志全方位超详细手把手教程,零基础可学习 日志如何配置及SLF4J的使用......
下一篇
无影云桌面