Spring Retry

In this post, we are covering the Spring Retry feature. This feature is handy when we are integrating with external API’s and need a robust system to handle system downtime or network downtime.

 

Introduction

To make processing more robust and less prone to failure, sometimes it helps to automatically retry a failed operation in case it might succeed on a subsequent attempt. Let’s take an example of external API integration where web service call may fail because of a network failure or network glitch. Retry provides the ability to automatically re-invoke a failed operation. In this post, we will learn how to use Spring retry feature in a Spring application.

 

1. Project Setup

To enable support to the Spring Retry, add following dependencies in your pom.xml file 

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

Please check maven central for the latest artifact. Spring Retry also require AOP. For Spring Boot, add the spring-boot-starter-aop starter in the pom.xml.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

[pullquote align=”normal”]We are using Spring Boot for this post. [/pullquote]

In case you are not using Spring Boot, add following dependencies in your pom.xml.

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>5.0.8.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.9.1</version>
</dependency>

 

2. Enable Spring Retry

Add @EnableRetry annotation to the configuration class.

@EnableRetry
@SpringBootApplication
public class SpringBootApplication {
 // ...
}

For non Spring Boot applications

@Configuration
@EnableRetry
public class Application {
 // ...
}

 

2. @Retryable Annotation

As the next step to use the retry feature, we use @Retryable annotation on the method where we like to enable retry feature.

 

2.1 @Retryable

Let’s create our sample retry service to see @Retryable annotation in action.

package com.javadevjournal.service;

import com.javadevjournal.exception.CustomException;
import com.javadevjournal.exception.CustomRetryException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;

public interface RetryService {

 @Retryable(value = {
  CustomRetryException.class}, maxAttempts = 4, backoff = @Backoff(200))
  public String retry() throws CustomRetryException, CustomException;
}

@Retryable annotation provides options to customize the retry behavior. Here are the details.

  • value attribute tells Spring retry to act if the method throws CustomRetryException or CustomException.
  • maxAttempts set the maximum number of retry. If you do not specify the default value is 3.
  • backoff specify the delay in the next retry. The default value is 1 second.

 

2.2 @Recover

The @Recover annotation used to define a separate recovery method when a @Retryablemethod fails with a specified exception.

import com.javadevjournal.exception.CustomException;
import com.javadevjournal.exception.CustomRetryException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class DefaultRetryService implements RetryService {

 private static final Logger LOG = LoggerFactory.getLogger(DefaultRetryService.class);
 private static int count = 0;

 @Override
 public String retry() throws CustomRetryException {
  LOG.info("Throwing CustomRetryException in method retry");
  throw new CustomRetryException("Throw custom exception");
 }

 @Override
 @Recover
 public String recover(Throwable throwable) {
  LOG.info("Default Retry servive test");
  return "Error Class :: " + throwable.getClass().getName();
 }
}

 

2.3 Testing Application

Let’s run our application to see the output

package com.javadevjournal;

import com.javadevjournal.exception.CustomException;
import com.javadevjournal.exception.CustomRetryException;
import com.javadevjournal.service.RetryService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class SpringRetryApplicationTests {

 private static final Logger LOGGER = LoggerFactory.getLogger(SpringRetryApplicationTests.class);

 @Autowired
 RetryService retryService;

 @Test
 public void contextLoads() {}

 @Test
 public void sampleRetryService() {
  try {
   final String message = retryService.retry();
   LOGGER.info("message = " + message);
  } catch (CustomRetryException e) {
   LOGGER.error("Error while executing test {}", e.getMessage());
  }
 }
}

We have the following output on the console

2018-09-02 21:45:10.059  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : Counter current value is 1 
2018-09-02 21:45:10.059  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : CustomRetryException
2018-09-02 21:45:10.262  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : Counter current value is 2 
2018-09-02 21:45:10.262  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : CustomException
2018-09-02 21:45:10.462  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : Counter current value is 3 
2018-09-02 21:45:10.463  INFO 55106 --- [           main] c.j.service.DefaultRetryService          : Default Retry servive test
2018-09-02 21:45:10.463  INFO 55106 --- [           main] c.j.SpringRetryApplicationTests          : message = Error Class :: java.lang.RuntimeException

 

3. RetryTemplate

RetryTemplate provides alternate in a case using the retry annotation is not an option for you.

 

3.1 RetryOperations

Spring retry providesRetryOperations strategy using RetryOperations interface. This interface provides several execute() methods.

public interface RetryOperations {

 <T> T execute(RetryCallback < T > retryCallback) throws Exception;
 // other execute methods

 <T> T execute(RetryCallback < T > retryCallback, RecoveryCallback < T > recoveryCallback,
  RetryState retryState) throws Exception;
}

The RetryCallback allows insertion of business logic that needs to be retried upon failure.

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

 

3.2 RetryTemplate

The RetryTemplate provides an implementation for the RetryOperations.It is considered a good practice to create a bean from it.

@Configuration
public class ApplicationConfiguration {

 public RetryTemplate retryTemplate() {
  SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
  simpleRetryPolicy.setMaxAttempts(2);

  FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
  backOffPolicy.setBackOffPeriod(1000 L); // milliseconds

  RetryTemplate template = new RetryTemplate();
  template.setRetryPolicy(simpleRetryPolicy);
  template.setBackOffPolicy(backOffPolicy);

  return template;
 }
}

 

3.3  Testing RetryTemplate

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class SpringRetryApplicationTests {

 private static final Logger LOGGER = LoggerFactory.getLogger(SpringRetryApplicationTests.class);

 @Autowired
 RetryService retryService;

 @Autowired
 RetryTemplate retryTemplate;

 @Test
 public void contextLoads() {}


 @Test(expected = RuntimeException.class)
 public void retryTemplateTest() {
  retryTemplate.execute(args -> {
   retryService.templateRetryService();
   return null;
  });
 }
}

 

4. When to use Spring Retry

Spring retry is a powerful and robust feature, however, this is not a solution for all the problems. Let’s have a look at some use cases where a retry is not the best option.

  • Use retry only on the temporary errors. I do not recommend it to use it on permanent errors else it can cause system performance issues.
  • Spring retry is not an alternative to circuit breaker concept. It may be a good idea to mix both retry and circuit breaker feature.

 

Summary

In this post, we looked at the different features of Spring retry. We got better clarity how it can help us make our applications more robust. We learned how to use @Retryable annotations and the RetryTemplate.