【Spring Retry的使用】

1、概述

Spring Retry 是Spring框架中的一个组件,
它提供了自动重新调用失败操作的能力。这在错误可能是暂时发生的(如网络故障)的情况下很有帮助。

在本文中,我们将看到使用Spring Retry的各种方式:注解、RetryTemplate以及回调。

2、Maven依赖

让我们首先将spring-retry依赖项添加到我们的pom.xml文件中:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.4</version>
</dependency>

我们还需要将Spring AOP添加到我们的项目中:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

可以查看Maven来获取最新版本的spring-retryspring-aspects 依赖。

3、开启Spring Retry

要在应用程序中启用Spring Retry,我们需要将@EnableRetry注释添加到我们的@Configuration类:

@Configuration
@EnableRetry
public class AppConfig { ... }

4、使用Spring Retry

4.1、@Retryable而不用恢复

我们可以使用@Retryable注解为方法添加重试功能:

@Service
public interface MyService {
    @Retryable(value = RuntimeException.class)
    void retryService(String sql);

}

在这里,当抛出RuntimeException时尝试重试。

根据@Retryable的默认行为,重试最多可能发生3次,重试之间有1秒的延迟。

4.2、@Retryable@Recover

现在让我们使用@Recover注解添加一个恢复方法:

@Service
public interface MyService {
    @Retryable(value = SQLException.class)
    void retryServiceWithRecovery(String sql) throws SQLException;
        
    @Recover
    void recover(SQLException e, String sql);
}

这里,当抛出SQLException时重试会尝试运行。 当@Retryable方法因指定异常而失败时,@Recover注解定义了一个单独的恢复方法。

因此,如果retryServiceWithRecovery方法在三次尝试之后还是抛出了SQLException,那么recover()方法将被调用。

恢复处理程序的第一个参数应该是Throwable类型(可选)和相同的返回类型。其余的参数按相同顺序从失败方法的参数列表中填充。

4.3、自定义@Retryable的行为

为了自定义重试的行为,我们可以使用参数maxAttemptsbackoff

@Service
public interface MyService {
    @Retryable( value = SQLException.class, 
      maxAttempts = 2, backoff = @Backoff(delay = 300))
    void retryServiceWithCustomization(String sql) throws SQLException;
}

这样最多将有两次尝试和100毫秒的延迟。

4.4、使用Spring Properties

我们还可以在@Retryable注解中使用properties。

为了演示这一点,我们将看到如何将delaymaxAttempts的值外部化到一个properties文件中。

首先,让我们在名为retryConfig.properties的文件中定义属性:

retry.maxAttempts=2
retry.maxDelay=100

然后我们指示@Configuration类加载这个文件:

@PropertySource("classpath:retryConfig.properties")
public class AppConfig { ... }
// ...

最后,我们可以在@Retryable的定义中注入retry.maxAttemptsretry.maxDelay的值:

@Service 
public interface MyService { 
  @Retryable( value = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}",
            backoff = @Backoff(delayExpression = "${retry.maxDelay}")) 
  void retryServiceWithExternalizedConfiguration(String sql) throws SQLException; 
}

请注意,我们现在使用的是maxAttemptsExpressiondelayExpression而不是maxAttemptsdelay

4.5、验证

@Retryable注解可以在接口上的方法上也能单独在实体的方法上。
以下是测试方法:(可以看到在查询后面加了一个by zero的异常)

 	@Retryable(value = RuntimeException.class)
    public void clearVeryLongDrafts() {
        // 清除超过7天以上的附件
        log.info("start clear attachments older than 7 days");
        DateTime dateTime = DateUtil.offsetDay(new Date(), -7);
        String format = DateUtil.format(dateTime, DatePattern.NORM_DATE_PATTERN);

        List<DrafeBox> drafeBoxList = new LambdaQueryChainWrapper<>(drafeBoxMapper)
                .le(DrafeBox::getSubmitTime, format)
                .list();

        int i = 1/0;
   }
2023-11-20 15:21:50.311 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:324: Retry: count=0
2023-11-20 15:21:50.317  INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob                    : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58b6c600] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==>  Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<==    Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<==        Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <<BLOB>>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58b6c600]
2023-11-20 15:21:51.398 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:360: Checking for rethrow: count=1
2023-11-20 15:21:51.399 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:324: Retry: count=1
2023-11-20 15:21:51.399  INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob                    : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@186a72c6] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==>  Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<==    Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<==        Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <<BLOB>>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@186a72c6]
2023-11-20 15:21:52.413 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:360: Checking for rethrow: count=2
2023-11-20 15:21:52.413 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:324: Retry: count=2
2023-11-20 15:21:52.414  INFO [srv_crm_base,,,] 892 --- [1-1700464910262] c.y.c.b.j.DraftBoxJob                    : clearVeryLongDrafts:48: start clear attachments older than 7 days
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e349061] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@40904969] will not be managed by Spring
==>  Preparing: SELECT id,company_code,business_type,business_name,json_drafe,submit_time,create_by,create_time,update_by,update_time,is_delete FROM t_drafe_box WHERE is_delete=0 AND (submit_time <= ?)
==> Parameters: 2023-11-13(String)
<==    Columns: id, company_code, business_type, business_name, json_drafe, submit_time, create_by, create_time, update_by, update_time, is_delete
<==        Row: 1722184859663953922, 010000, TRANSFER_APPLICATION, 客户转移申请, <<BLOB>>, 2023-11-08 17:30:42, BWhuhang, 2023-11-08 17:30:42, BWhuhang, 2023-11-10 09:51:29, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1e349061]
2023-11-20 15:21:52.424 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:360: Checking for rethrow: count=3
2023-11-20 15:21:52.424 DEBUG [srv_crm_base,,,] 892 --- [1-1700464910262] o.s.r.s.RetryTemplate                    : doExecute:383: Retry failed last attempt: count=3

从上面的全部日志可以看到,再每一次查询之后就出现了以下日志,这就表示重试了count标识重试次数。

o.s.r.s.RetryTemplate                    : doExecute:360: Checking for rethrow: count=1
o.s.r.s.RetryTemplate                    : doExecute:324: Retry: count=1

当然我们还可以自定义很多参数,这些就不一一测试了。

5、RetryTemplate

5.1、RetryOperations

Spring Retry提供了RetryOperations接口,它提供了一组execute()方法:

public interface RetryOperations {
    <T> T execute(RetryCallback<T> retryCallback) throws Exception;

    ...
}

execute()方法的参数RetryCallback,是一个接口,可以插入需要在失败时重试的业务逻辑:

public interface RetryCallback<T> {
    T doWithRetry(RetryContext context) throws Throwable;
}

5.2、RetryTemplate配置

RetryTemplateRetryOperations的一个实现。

让我们在@Configuration类中配置一个RetryTemplate的bean:

@Configuration
public class AppConfig {
    //...
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000l);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(2);
        retryTemplate.setRetryPolicy(retryPolicy);
        
        return retryTemplate;
    }
}

这个RetryPolicy确定了何时应该重试操作。

其中SimpleRetryPolicy定义了重试的固定次数,另一方面,BackOffPolicy用于控制重试尝试之间的回退。

最后,FixedBackOffPolicy会使重试在继续之前暂停一段固定的时间。

5.3、使用RetryTemplate

要使用重试处理来运行代码,我们可以调用retryTemplate.execute()方法:

retryTemplate.execute(new RetryCallback<Void, RuntimeException>() {
    @Override
    public Void doWithRetry(RetryContext arg0) {
        myService.templateRetryService();
        ...
    }
});

我们可以使用lambda表达式代替匿名类:

retryTemplate.execute(arg0 -> {
    myService.templateRetryService();
    return null;
});

6、监听器

监听器在重试时提供另外的回调。我们可以用这些来关注跨不同重试的各个横切点。

6.1、添加回调

回调在RetryListener接口中提供:

public class DefaultListenerSupport extends RetryListenerSupport {
    @Override
    public <T, E extends Throwable> void close(RetryContext context,
      RetryCallback<T, E> callback, Throwable throwable) {
        logger.info("onClose");
        ...
        super.close(context, callback, throwable);
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context,
      RetryCallback<T, E> callback, Throwable throwable) {
        logger.info("onError"); 
        ...
        super.onError(context, callback, throwable);
    }

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context,
      RetryCallback<T, E> callback) {
        logger.info("onOpen");
        ...
        return super.open(context, callback);
    }
}

openclose的回调在整个重试之前和之后执行,而onError应用于单个RetryCallback调用。

6.2、注册监听器

接下来,我们将我们的监听器(DefaultListenerSupport)注册到我们的RetryTemplate bean:

@Configuration
public class AppConfig {
    ...

    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        ...
        retryTemplate.registerListener(new DefaultListenerSupport());
        return retryTemplate;
    }
}

7、测试结果

为了完成我们的示例,让我们验证一下结果:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = AppConfig.class,
  loader = AnnotationConfigContextLoader.class)
public class SpringRetryIntegrationTest {

    @Autowired
    private MyService myService;

    @Autowired
    private RetryTemplate retryTemplate;

    @Test(expected = RuntimeException.class)
    public void givenTemplateRetryService_whenCallWithException_thenRetry() {
        retryTemplate.execute(arg0 -> {
            myService.templateRetryService();
            return null;
        });
    }
}

从测试日志中可以看出,我们已经正确配置了RetryTemplateRetryListener

2020-01-09 20:04:10 [main] INFO  c.p.s.DefaultListenerSupport - onOpen 
2020-01-09 20:04:10 [main] INFO  c.pin.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService() 
2020-01-09 20:04:10 [main] INFO  c.p.s.DefaultListenerSupport - onError 
2020-01-09 20:04:12 [main] INFO  c.pin.springretry.MyServiceImpl - throw RuntimeException in method templateRetryService() 
2020-01-09 20:04:12 [main] INFO  c.p.s.DefaultListenerSupport - onError 
2020-01-09 20:04:12 [main] INFO  c.p.s.DefaultListenerSupport - onClose

8、结论

在本文中,我们看到了如何使用注解、RetryTemplate和回调监听器来使用Spring Retry。了解了如何使用 Spring Retry 来减少样板代码并使代码更具可读性和可维护性。通过 Spring Retry,相信你也能够消除冗余代码。

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Retry 是一个用于在失败情况下自动重试的库。它提供了一些注解和模板来简化重试逻辑的编写和配置。 要使用 Spring Retry,首先需要将其添加为项目的依赖项。在 Maven 项目中,可以通过在 pom.xml 文件中添加以下依赖项来实现: ``` <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.1</version> </dependency> ``` 然后,在需要进行重试的方法上添加 @Retryable 注解。该注解可以指定重试的次数、重试的异常类型以及可选的回退方法。例如: ```java import org.springframework.retry.annotation.Retryable; @Retryable(value = {CustomException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000)) public void retryMethod() throws CustomException { // 重试逻辑 } ``` 在上面的示例中,retryMethod() 方法将在捕获 CustomException 异常时进行最多三次重试。每次重试之间会等待 1000 毫秒。 最后,需要在 Spring 的配置类上添加 @EnableRetry 注解以启用重试功能。例如: ```java import org.springframework.context.annotation.Configuration; import org.springframework.retry.annotation.EnableRetry; @Configuration @EnableRetry public class AppConfig { // 配置类内容 } ``` 这样,Spring Retry 就会自动在发生异常时触发重试逻辑。 需要注意的是,Spring Retry 默认只会重试在方法内部抛出的异常,而不会重试外部异常(例如网络故障)。如果需要对外部异常进行重试,可以通过自定义 RetryPolicy 或使用 Spring 的 AspectJ 功能来实现。 希望这些信息能帮助到你使用 Spring Retry。如果有任何进一步的问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值