Spring Retry实现重试机制的示例详解

 更新时间:2023年07月24日 10:57:39   作者:我是小趴菜  
这篇文章主要为大家详细介绍了Spring-Retry的用法以及实现原理是怎么样的,文中的示例代码讲解详细,具有一定的参考价值,需要的可以了解一下

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

Spring-Retry用法

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

<?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>

启动类

@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";
    }
}
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

@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的切面和通知

@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()

@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);
}
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
      RecoveryCallback<T> recoveryCallback) throws E {
   //继续进入doExecute方法
   return doExecute(retryCallback, recoveryCallback, null);
}
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);

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

具体的实现类是SimpleRetryPolicy

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);方法

//这段代码就是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的时候,设置的异常类型一定要一致,否则这个重试机制就不会生效了

以上就是Spring Retry实现重试机制的示例详解的详细内容,更多关于Spring Retry重试的资料请关注脚本之家其它相关文章!

相关文章

  • redis分布式锁RedissonLock的实现细节解析

    redis分布式锁RedissonLock的实现细节解析

    这篇文章主要介绍了redis分布式锁RedissonLock的实现细节解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java Jedis NOAUTH Authentication required问题解决方法

    Java Jedis NOAUTH Authentication required问题解决方法

    这篇文章主要介绍了Java Jedis NOAUTH Authentication required问题解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07
  • Java Excel Poi字体颜色自定义设置代码

    Java Excel Poi字体颜色自定义设置代码

    最近项目使用POI按模板导出Excel,需要设置单元格的字体为红色,下面这篇文章主要给大家介绍了关于Java Excel Poi字体颜色自定义设置的相关资料,需要的朋友可以参考下
    2024-01-01
  • MyBatis的核心配置文件以及映射文件

    MyBatis的核心配置文件以及映射文件

    这篇文章主要介绍了MyBatis的核心配置文件以及映射文件,Mybatis它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低,需要的朋友可以参考下
    2023-05-05
  • 深入讲解java线程与synchronized关键字

    深入讲解java线程与synchronized关键字

    Java 中多线程的同步依靠的是对象锁机制,synchronized关键字就是利用了封装对象锁来实现对共享资源的互斥访问。下面这篇文章主要介绍了java线程与synchronized关键字的相关资料,需要的朋友可以参考下。
    2017-03-03
  • Springboot项目启动成功后可通过五种方式继续执行

    Springboot项目启动成功后可通过五种方式继续执行

    本文主要介绍了Springboot项目启动成功后可通过五种方式继续执行,主要包括CommandLineRunner接口,ApplicationRunner接口,ApplicationListener接口,@PostConstruct注解,InitalizingBean接口,感兴趣的可以了解一下
    2023-12-12
  • Java Swing JList列表框的实现

    Java Swing JList列表框的实现

    这篇文章主要介绍了Java Swing JList列表框的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • Springboot传参详解

    Springboot传参详解

    这篇文章主要介绍了Springboot传参的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-11-11
  • Spring概述和快速构建的方式

    Spring概述和快速构建的方式

    Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架),Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情,本文给大家介绍spring概述和快速构建方式,一起看看吧
    2021-06-06
  • 谈谈为JAXB和response设置编码,解决wechat4j中文乱码的问题

    谈谈为JAXB和response设置编码,解决wechat4j中文乱码的问题

    中文乱码是每个程序员都会遇到的问题,本篇文章主要介绍了谈谈为JAXB和response设置编码,解决wechat4j中文乱码的问题,具有一定的参考价值,有兴趣的可以了解一下。
    2016-12-12

最新评论