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 on Java Application Development will guide you through the step-by-step setup for integrating spring-retry into your 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:
2.2 Project High Level Diagram:
Explanation:
1. The project has RemoteClientService and its dummy implementation, which mimics the real world unreliable service (It works 25% of the time and throws RemoteException the remaining times)
2. The ApplicationService accesses the APIs of RemoteClientService and has 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:
4.0.0
com.hemant
spring-retry
0.0.1-SNAPSHOT
jar
spring-retry
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.1.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.retry
spring-retry
org.springframework
spring-aspects
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
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 {
MapgetValueForKey(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 MapgetValueForKey(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 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 the 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 :
*
*
* - 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
*
*/
@Retryable(
value = { RuntimeException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
Map
getValueForKey(String key);
/*** This method is a fallback method for
* #{com.hemant.springretry.service.app.ApplicationService.getValueForKey()}
*
* Following are the things to note :
*
* - 1. The signature of this method is same as the method with @Retryable
* as return type is same.
*
- 2. Its arguements follow the order (Throwable t, args of the method with retryAble)
*
*
* @param e
* @return
*/
@Recover
MapgetValueForKeyFallback(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 MapgetValueForKey(String key) {
returnremoteClientService.getValueForKey(key);
}
/**
* This method is a fallback method for
* #{com.hemant.springretry.service.app.ApplicationService.getValueForKey()}
*/
@Override
public MapgetValueForKeyFallback(RuntimeException e, String key) {
LOG.warn("All retries were exhausted. Hence recover method is called. Returning fallback value");
Map 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 application service 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 MapgetValueForKey(
@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 the 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 the 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 using spring-retry which provides us a declarative way to handle retries more efficiently.