Guide to Spring Retry

Credits

1. Overview

Spring Retry provides an ability to automatically re-invoke a failed operation. This is helpful where the errors may be transient (like a momentary network glitch).

In this tutorial, we’ll see the various ways to use Spring Retry: annotations, RetryTemplate, and callbacks.

Read more 

2. Maven Dependencies 

Let’s begin by adding the spring-retry dependency into our pom.xml file:

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

We also need to add Spring AOP in our project:

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

Have a look at Maven Central for the latest versions of the spring-retry and spring-aspects dependencies.

3. Enabling Spring Retry

To enable Spring Retry in an application, we need to add the @EnableRetry annotation to our @Configuration class:

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

4. Using Spring Retry

4.1. @Retryable Without Recovery

We can use the @Retryable annotation to add retry functionality to methods:

@Service
public interface MyService { 

    @Retryable 
    void retryService(String sql); 
}

Since we have not specified any exceptions here, retry will be attempted for all the exceptions. Also, once the max attempts are reached and there is still an exception, ExhaustedRetryException will be thrown.

Per @Retryable‘s default behavior, the retry may happen up to three times, with a delay of one second between retries.

4.2. @Retryable and @Recover

Let’s now add a recovery method using the @Recover annotation:

freestar
@Service
public interface MyService { 

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

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

Here, the retry is attempted when an SQLException is thrown. The @Recover annotation defines a separate recovery method when a @Retryable method fails with a specified exception.

Consequently, if the retryServiceWithRecovery method keeps throwing an SqlException after three attempts, the recover() method will be called.

The recovery handler should have the first parameter of type Throwable (optional) and the same return type.The following arguments are populated from the argument list of the failed method in the same order.

4.3. Customizing @Retryable’s Behavior

In order to customize a retry’s behavior, we can use the parameters maxAttempts and backoff:

@Service
public interface MyService {

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

There will be up to two attempts and a delay of 100 milliseconds.

4.4. Using Spring Properties

We can also use properties in the @Retryable annotation.

To demonstrate this, we’ll see how to externalize the values of delay and max attempts into a properties file.

First, let’s define the properties in a file called retryConfig.properties:

retry.maxAttempts=2
retry.maxDelay=100Copy

We then instruct our @Configuration class to load this file:

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

Finally, we can inject the values of retry.maxAttempts and retry.maxDelay in our @Retryable definition:

@Service 
public interface MyService {

    @Retryable(retryFor = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}",
               backoff = @Backoff(delayExpression = "${retry.maxDelay}")) 
    void retryServiceWithExternalConfiguration(String sql) throws SQLException; 
}

Please note that we are now using maxAttemptsExpression and delayExpression instead of maxAttempts and delay.

5. RetryTemplate

5.1. RetryOperations

Spring Retry provides RetryOperations interface, which supplies a set of execute() methods:

public interface RetryOperations {

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

    ...
}

The RetryCallback, which is a parameter of the execute(), is an interface that allows the insertion of business logic that needs to be retried upon failure:

public interface RetryCallback<T> {

    T doWithRetry(RetryContext context) throws Throwable;
}

5.2. RetryTemplate Configuration 

The RetryTemplate is an implementation of the RetryOperations.

Let’s configure a RetryTemplate bean in our @Configuration class:

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

The RetryPolicy determines when an operation should be retried.

SimpleRetryPolicy is used to retry a fixed number of times. On the other hand, the BackOffPolicy is used to control backoff between retry attempts.

Finally, a FixedBackOffPolicy pauses for a fixed period of time before continuing.

5.3. Using the RetryTemplate

To run code with retry handling, we can call the retryTemplate.execute() method:

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

Instead of an anonymous class, we can use a lambda expression:

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

6. Listeners

Listeners provide additional callbacks upon retries. And we can use these for various cross-cutting concerns across different retries.

6.1. Adding Callbacks

The callbacks are provided in a RetryListener interface:

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);
    }
}

The open and close callbacks come before and after the entire retry, while onError applies to the individual RetryCallback calls.

6.2. Registering the Listener

Next, we register our listener (DefaultListenerSupport) to our RetryTemplate bean:

@Configuration
public class AppConfig {
    ...

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

7. Testing the Results

To finish our example, let’s verify the results:

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

As we can see from the test logs, we have properly configured the RetryTemplate and the RetryListener:

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. Conclusion

In this article, we saw how to use Spring Retry using annotations, the RetryTemplate, and callbacks listeners.

The source code for the examples is available over on GitHub.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments