1. 概述

Spring Retry 提供了自动重新调用失败操作的能力。这对于像暂时的网络故障这样的临时错误非常有帮助。

在这个教程中,我们将探讨使用 Spring Retry 的各种方式:注解、RetryTemplate 和回调。

2. Maven 依赖

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

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

同时,我们也需要在项目中添加 Spring AOP:

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

有关 spring-retryspring-aspects 最新版本,请查看 Maven 中心仓库。

3. 启用 Spring Retry

要在应用程序中启用 Spring Retry,我们需要在 @Configuration 类上添加 @EnableRetry 注解:

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

4. 使用 Spring Retry

4.1. 使用带有恢复的 @Retryable

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

@Service
public interface MyService { 

    @Retryable 
    void retryService(String sql); 
}

由于这里没有指定任何异常,对于所有抛出的异常都将尝试重试。如果达到最大尝试次数后仍然出现异常,将抛出 ExhaustedRetryException

按照 @Retryable 的默认行为,重试可能最多尝试三次,每次重试之间间隔一秒。

4.2. @Retryable@Recover

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

@Service
public interface MyService { 

    @Retryable(retryFor = SQLException.class)
    void retryServiceWithRecovery(String sql) throws SQLException; 

    @Recover
    void recover(SQLException e, String sql); 
}

当抛出 SQLException 时,将尝试重试。@Recover 注解定义了一个单独的恢复方法,当 @Retryable 方法因特定异常失败时调用。

因此,如果 retryServiceWithRecovery 方法在三次尝试后仍不断抛出 SQLException,则会调用 recover() 方法。

恢复处理器应具有类型为 Throwable(可选)的第一个参数和相同的返回类型。以下参数按失败方法的参数列表顺序从失败方法中填充。

4.3. 自定义 @Retryable 行为

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

@Service
public interface MyService {

    @Retryable(retryFor = SQLException.class, maxAttempts = 2, backoff = @Backoff(delay = 100))
    void retryServiceWithCustomization(String sql) throws SQLException;
}

将最多尝试两次,并且每次间隔100毫秒。

4.4. 使用 Spring 属性

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

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

首先,我们在名为 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(retryFor = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}",
               backoff = @Backoff(delayExpression = "${retry.maxDelay}")) 
    void retryServiceWithExternalConfiguration(String sql) throws SQLException; 
}

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

5. RetryTemplate

5.1. RetryOperations

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

public interface RetryOperations {

    <T> T execute(RetryCallback<T> retryCallback) throws Exception;

    ...
}

RetryCallbackexecute() 方法的参数,它允许在操作失败时插入需要重试的业务逻辑:

public interface RetryCallback<T> {

    T doWithRetry(RetryContext context) throws Throwable;
}

5.2. RetryTemplate 配置

RetryTemplateRetryOperations 的实现。

让我们在 @Configuration 类中配置一个 RetryTemplate 对象:

@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 对象:

@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  o.b.s.DefaultListenerSupport - onOpen 
2020-01-09 20:04:10 [main] INFO  o.baeldung.springretry.MyServiceImpl
- throw RuntimeException in method templateRetryService() 
2020-01-09 20:04:10 [main] INFO  o.b.s.DefaultListenerSupport - onError 
2020-01-09 20:04:12 [main] INFO  o.baeldung.springretry.MyServiceImpl
- throw RuntimeException in method templateRetryService() 
2020-01-09 20:04:12 [main] INFO  o.b.s.DefaultListenerSupport - onError 
2020-01-09 20:04:12 [main] INFO  o.b.s.DefaultListenerSupport - onClose

8. 总结

在这篇文章中,我们了解了如何使用注解、RetryTemplate 和回调监听器来使用 Spring Retry。

示例代码可在 GitHub 上 查看。