Using Spring Retry to consume REST Services

Spring-retry is one of the many side-projects of Spring: the famous dependency injection framework. This library let us automatically re-invoke a method, moreover this operation it’s trasparent to the rest of our application. In this post I will try to illustrate the main features of this API, using a simple example.

Setup

To add Spring-retry to your project, you just need to add the following dependencies to your pom.xml file:

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

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

Spring Retry Use case

Our purpose is to obtain the current Euro/Dollar exchange rate consuming a REST service. If the server responds with a HTTP code 503, we will relaunch the method unitil the server responds with a 200 code. Here is the service implementation:

@Path("/fetchRate")
public class ChangeService {

	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public Response getChange(@QueryParam("from") String from,
			@QueryParam("to") String to) {
		Random randomGenerator = new Random();
		int randomInt = randomGenerator.nextInt(2);

		if (from.equals("EUR") && to.equals("USD")) {
			if (randomInt == 1)
				return Response.status(200).entity("1.10").build();
			else
				return Response.status(503)
						.entity("Service temporarily not available").build();
		} 
		return Response.status(500).entity("Currency not available").build();

	}

}

 As you can see, in order to simulate a service which is temporarily unavailable we are using a Random generator which returns a 503 error on 50% of the invocations.

@Retryable

The “heart” of spring-retry is the @Retryable annotation. With the maxAttempts attribute we will set how many times the method should be invoked. With the @Backoff annotation we will configure an initial delay in milliseconds; this delay will increase of a 50% factor for every iteration. Let’s have a look at the full code of the REST client to understand how this annotation works.

public class RealExchangeRateCalculator implements ExchangeRateCalculator {
	
	private static final double BASE_EXCHANGE_RATE = 1.09;
	private int attempts = 0;
	private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
	
	@Retryable(maxAttempts=10,value=RuntimeException.class,backoff = @Backoff(delay = 10000,multiplier=2))
	public Double getCurrentRate() {
		
		System.out.println("Calculating - Attempt " + attempts + " at " + sdf.format(new Date()));
		attempts++;
		
		try {
			HttpResponse<JsonNode> response = Unirest.get("http://rate-exchange.herokuapp.com/fetchRate")
				.queryString("from", "EUR")
				.queryString("to","USD")
				.asJson();
			
			switch (response.getStatus()) {
			case 200:
				return response.getBody().getObject().getDouble("Rate");
			case 503:
				throw new RuntimeException("Server Response: " + response.getStatus());
			default:
				throw new IllegalStateException("Server not ready");
			}
		} catch (UnirestException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Recover
	public Double recover(RuntimeException e){
		System.out.println("Recovering - returning safe value");
		return BASE_EXCHANGE_RATE;
	}

}

In case of a RuntimeException, the getCurrentRate method will be automatically reinvoked 10 times. If during the last execution the method fails again, the method annotated with @Recover will be launched. If there’s no recover method, the exception it’s simply throwed up.

XML Configuration

All of our configuration are made via annotations. Spring-retry, just like its greater brother Spring Framework can use the XML configuration. In this case we use the aspect-oriented programming, adding an Interceptor to our beans with this configuration:

<aop:config>
    <aop:pointcut id="retryable" expression="execution(* it..*RateCalculator.getCurrentRate(..))" />
    <aop:advisor pointcut-ref="retryable" advice-ref="retryAdvice" order="-1"/>
</aop:config>

<bean id="retryAdvice" class="org.springframework.batch.retry.interceptor.RetryOperationsInterceptor"/>

The source code for this Spring application is available on Github.

Using Spring Retry with Spring Boot

Let’s see now an example about String retry using Spring Boot framework. The logic does not change comparing to the standard Spring application. We will code a Controller class which uses a Database to retrieve a list of Customer objects. Should this fail, a static list of Customer objects will be returned:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@RestController
public class CustomerController {
    @Autowired
    CustomerRepository repository;
    List<Customer> customerList = new ArrayList<Customer>();

    @PostConstruct
    public void init() {
        customerList.add(new Customer(1, "frank"));
        customerList.add(new Customer(2, "john"));
    }

    @RequestMapping("/list")
    @Retryable(value = {ServiceNotAvailableException.class}, maxAttempts = 2, backoff = @Backoff(delay = 1000))
    public List<Customer> findAll() {

        int random = new Random().nextInt(2);

        if (random == 1) {
            throw new ServiceNotAvailableException("DB Not available! using Spring-retry..");
        }
        System.out.println("Returning data from DB");
        return repository.findAll();
    }

    @Recover
    public List<Customer> getBackendResponseFallback(ServiceNotAvailableException e) {
        System.out.println("Returning cached list.");
        return customerList;
    }

}

As you can see, the logic is contained in the findAll method which uses a Random generator to simulate a failure in accessing the Database. This is allowed up to 2 attempts. If that fails more than maxAttempts, the @Recover method will be triggered, to return the customerList from an ArrayList.
We need to add @EnableRetry on the top of our SpringBoot application:

@SpringBootApplication
@EnableRetry
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}

Upon start, the application will insert some data in the H2 Database, so that the findAll method is able to return a set of records:

INSERT into CUSTOMER (id,name) VALUES (10, 'frank');
INSERT into CUSTOMER (id,name) VALUES (20, 'john');

To connect to the H2 Database, we will include the following configuration in the resources/application.properties file:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

The full source code for the application is available on GitHub: https://github.com/fmarchioni/mastertheboss/tree/master/spring/demo-retry

Do you want some more Spring Boot stuff ? check Spring Boot Tutorials !

Found the article helpful? if so please follow us on Socials