How to integrate Spring Retry in Java Application

1. Introduction

In a business application, we often come across the use-cases, where we need to access a resource whose access is unreliable. If the initial access fails, then ideally we retry the failed operation (hoping it might succeed in the subsequent attempt). Also we need a fallback mechanism in case all our retries fail.


Common examples include accessing a remote web-service/RMI (network outage, server down, network glitch, deadlock)


Spring Retry library (https://github.com/spring-projects/spring-retry) provides a declarative support to retry failed operations as well as fallback mechanism in case all the attempts fail.


This article will explain step by step setup for integrate spring-retry in our spring-boot application.

2. Project

2.1 Project Structure

We will be creating a spring-boot maven project. The project layout is as follows :


└───maven-project ├───pom.xml └───src ├───main │ ├───java (java source files) │ ├───resources (properties files)

The project structure screenshot is as follows :


spring

2.2 Project High Level Diagram :

spring

Explanation :

1. The project has RemoteClientService and its dummy implementation, which mimics the real world unreliable service (It works 25% of time and throws RemoteException the remaining times)


2. The ApplicationService accesses the APIs of RemoteClientService and have the retryable context on it (ie if the access fails, then retry accessing it again as per retry configuration).


3. The LookupController exposes the rest API of our application, where we get a key as request param and return ait along with its value.


2.3 Maven dependencies:

The pom.xml file for our project is as follows :


<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hemant</groupId> <artifactId>spring-retry</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-retry</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.1.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

2.3 Project Details


The code for the same is as follows :

2.3.1 RemoteClientService

The RemoteClientService and its implementation is as follows :


packagecom.hemant.springretry.service.remote; importjava.util.Map; /** * This service represents a remote webservice/RMI * whose response is not guaranteed. * @author hemant * */ publicinterfaceRemoteClientService { Map<String, String>getValueForKey(String key); }

The dummy implementation of the RemoteClientService which mimics real world unreliable access is as follows :


packagecom.hemant.springretry.service.remote; importjava.util.HashMap; importjava.util.Map; importjava.util.Random; import org.slf4j.Logger; import org.slf4j.LoggerFactory; importorg.springframework.remoting.RemoteAccessException; importorg.springframework.stereotype.Service; /** * This service mimics a remote webservice/RMI * whose response is not guaranteed. * * @author hemant * */ @Service publicclassDummyRemoteClientServiceImplimplementsRemoteClientService { privatestaticfinal Logger LOG = LoggerFactory.getLogger(DummyRemoteClientServiceImpl.class); privatefinal Random random = new Random(); /*** This mimics the response of unreliable service. * Here we use {@code java.util.Random} so that * it returns success sometimes as well as failure sometimes * * The random number generates {0,1,2,3} with equal probablity * When random = 0, successful response is returned * Else, it throws {@code org.springframework.remoting.RemoteAccessException} * * Thus the success probablity = 25% * * @return */ @Override public Map<String, String>getValueForKey(String key) { intnum = random.nextInt(4); LOG.info("The random number is :{}", num); if(num != 0) { LOG.error("The random number is {} NOT 0. Hence throwing exception", num); thrownewRemoteAccessException("The random number " + num + " is NOT 0"); } LOG.info("The random number is 0. Hence returning the response"); Map<String, String> map = newHashMap<>(); String value = key + random.nextDouble(); LOG.info("The value for the key :{} is {}", key, value); map.put(key, value); return map; } }

2.3.2 Application Service


The application service uses the API of the RemoteClientService.Since the API is unreliable, the method calling it is annotated with @Retryable. The retry config is


1. Retry when the implementation throws RuntimeException or any its subclass

2. Retry the code 3 times upon failure

3. Next retry is made after a delay of 1000 msie 1 sec


Also it has 1 more method, ie fallback method.


This method is called automatically when all the retries are exhausted. The signature of this method is same as the method annotated with @Retryable.

1. This method is annotated with @Recover

2. The return type is same as method with @Retryable

3. The arguments here are arguments for method with @Retryable + the exception.


packagecom.hemant.springretry.service.app; importjava.util.Map; importorg.springframework.retry.annotation.Backoff; importorg.springframework.retry.annotation.Recover; importorg.springframework.retry.annotation.Retryable; /** * Application service * @author hemant * */ publicinterfaceApplicationService { /** * This method consumes the API from RemoteClientService and sends it back * We have declared this as retryable with following config : * * <ul> * <li> 1. Retry when the implementation throws RuntimeException or any its subclass * <li> 2. Retry the code 3 times upon failure * <li> 3. Next retry is made after a delay of 1000 msie 1 sec * </ul> */ @Retryable( value = { RuntimeException.class }, maxAttempts = 3, backoff = @Backoff(delay = 1000)) Map<String, String>getValueForKey(String key); /*** This method is a fallback method for * #{com.hemant.springretry.service.app.ApplicationService.getValueForKey()} * * Following are the things to note : * <ul> * <li> 1. The signature of this method is same as the method with @Retryable * as return type is same. * <li> 2. Its arguements follow the order (Throwable t, args of the method with retryAble) * </ul> * * @param e * @return */ @Recover Map<String, String>getValueForKeyFallback(RuntimeException e, String key); }

The implementation of the interface :


packagecom.hemant.springretry.service.app; importjava.util.HashMap; importjava.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Service; importcom.hemant.springretry.service.remote.RemoteClientService; /** * This is a component at application end which uses the * RemoteClientServiceAPIs * * @author hemant * */ @Service publicclassApplicationServiceImplimplementsApplicationService { privatestaticfinal Logger LOG = LoggerFactory.getLogger(ApplicationServiceImpl.class); @Autowired privateRemoteClientServiceremoteClientService; /** * This method consumes the API from RemoteClientService and sends it back */ @Override public Map<String, String>getValueForKey(String key) { returnremoteClientService.getValueForKey(key); } /** * This method is a fallback method for * #{com.hemant.springretry.service.app.ApplicationService.getValueForKey()} */ @Override public Map<String, String>getValueForKeyFallback(RuntimeException e, String key) { LOG.warn("All retries were exhausted. Hence recover method is called. Returning fallback value"); Map<String, String> map = newHashMap<>(); map.put(key, "FALL BACK VALUE"); return map; } }

2.3.3 Controller

The Rest Controller exposes the single API (/lookup), which accepts a key and then calls the applicationService to get its value (which in-turn calls the RemoteClientService).


packagecom.hemant.springretry.web; importjava.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RequestMethod; importorg.springframework.web.bind.annotation.RequestParam; importorg.springframework.web.bind.annotation.RestController; importcom.hemant.springretry.service.app.ApplicationService; /** * The controller exposing the application API * @author hemant * */ @RestController publicclassLookupController { privatestaticfinal Logger LOG = LoggerFactory.getLogger(LookupController.class); @Autowired privateApplicationServiceapplicationService; /** * Gets the key value for the given parameter * @param key * @return */ @RequestMapping(value="/lookup", method = RequestMethod.GET) public Map<String, String>getValueForKey( @RequestParam String key) { LOG.info("The KEY parameter recieved is {}", key); returnapplicationService.getValueForKey(key); } }

2.3.4 Main Application class


The main application is :


1. Annotated with annotation @SpringBootApplicationindicating that it is main class for spring boot

2. Also annotated with @EnableRetry indicating spring to enable retry config


packagecom.hemant.springretry; importorg.springframework.boot.SpringApplication; importorg.springframework.boot.autoconfigure.SpringBootApplication; importorg.springframework.retry.annotation.EnableRetry; @SpringBootApplication @EnableRetry publicclassSpringRetryApplication { publicstaticvoidmain(String[] args) { SpringApplication.run(SpringRetryApplication.class, args); } }

2.3.5 Application properties file

Spring boot gets its properties from default application.properties file in src/main/resources directory.


# IDENTITY (ContextIdApplicationContextInitializer) spring.application.name= spring-retry # Server HTTP port. server.port=8080 #logging pattern logging.pattern.console=%d{HH:mm:ss} %-5level %logger{10} - %msg%n #since we need to look closer at retry library logs logging.level.org.springframework.retry=DEBUG

3. Executing The Project

3.1 Running the application

1. Run the main application class (SpringRetryApplication.java) as Java Application in IDE (Eclipse/IntelliJ) OR

2. If you want to run it from terminal, then

a. Go to root directory, execute “mvn clean install”. This will create the jar in /target child directory.

b. Execute the jar as “java -jar target/spring-retry-0.0.1-SNAPSHOT.jar ”

3. Once the application is started, its endpoints can be called.

3.2 Execution use cases

3.2.1 Successful call without retries :


In this case, the first call to remote service is success, the logs and response are as follows :


23:05:48INFO c.h.s.w.LookupController - The KEY parameter recieved is 123 23:05:48 DEBUG o.s.r.s.RetryTemplate - Retry: count=0 23:05:48 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :0 23:05:48 INFO c.h.s.s.r.DummyRemoteClientServiceImpl- The random number is 0. Hence returning the response 23:05:48 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The value for the key :123 is 1230.28348079448241803 GET http://localhost:8080/lookup?key=123 Response : { "123": "1230.28348079448241803" }

3.2.2 Initial call fail but we got success in next attempts

In this case, the first call fails, but subsequent retry attempts fetches the response.


23:23:02INFO c.h.s.w.LookupController - The KEY parameter recieved is 123 23:23:02 DEBUG o.s.r.s.RetryTemplate - Retry: count=0 23:23:02 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :3 23:23:02 ERROR c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is 3 NOT 0. Hence throwing exception 23:23:03 DEBUG o.s.r.s.RetryTemplate - Checking forrethrow: count=1 23:23:03 DEBUG o.s.r.s.RetryTemplate - Retry: count=1 23:23:03INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :0 23:23:03 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is 0. Hence returning the response 23:23:03 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The value for the key :123 is 1230.8441210232646286 GET http://localhost:8080/lookup?key=123 Response : { "123": "1230.8441210232646286" }

3.2.3 When all retries fail:

Here the first call as well as subsequent retry attempts fail.
Hence at the end the fallback method is called (one annotated with @Recover) which sends the fallback response to client.


23:36:28 INFO o.a.c.c.C.[.[.[/] - Initializing Spring FrameworkServlet'dispatcherServlet' 23:36:28 INFO o.s.w.s.DispatcherServlet - FrameworkServlet'dispatcherServlet': initialization started 23:36:28 INFO o.s.w.s.DispatcherServlet - FrameworkServlet'dispatcherServlet': initialization completed in 30ms 23:36:28 INFO c.h.s.w.LookupController - The KEY parameter recieved is 123 23:36:28 DEBUG o.s.r.s.RetryTemplate - Retry: count=0 23:36:28 INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :1 23:36:28 ERROR c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is 1 NOT 0. Hence throwing exception 23:36:29 DEBUG o.s.r.s.RetryTemplate - Checking forrethrow: count=1 23:36:29 DEBUG o.s.r.s.RetryTemplate - Retry: count=1 23:36:29INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :1 23:36:29 ERROR c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is 1 NOT 0. Hence throwing exception 23:36:30 DEBUG o.s.r.s.RetryTemplate - Checking forrethrow: count=2 23:36:30 DEBUG o.s.r.s.RetryTemplate - Retry: count=2 23:36:30INFO c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is :3 23:36:30 ERROR c.h.s.s.r.DummyRemoteClientServiceImpl - The random number is 3 NOT 0. Hence throwing exception 23:36:30 DEBUG o.s.r.s.RetryTemplate - Checking forrethrow: count=3 23:36:30 DEBUG o.s.r.s.RetryTemplate - Retry failed last attempt: count=3 23:36:30WARN c.h.s.s.a.ApplicationServiceImpl - All retries were exhausted. Hence recover method is called. Returning fallback value GET http://localhost:8080/lookup?key=123 Response : { "123": "FALL BACK VALUE" }

4. Conclusion

Thus we have created a java application development using spring-retry which provides us a declarative way to handle retries more efficiently.

 
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img
  • img