If you have a need to implement robust retry logic in your code, a proven way would be to use the spring retry library. My objective here is not to show how to use the spring retry project itself, but in demonstrating different ways that it can be integrated into your codebase.
Consider a service to invoke an external system:
Assume that this call can fail and you want the call to be retried thrice with a 2 second delay each time the call fails, so to simulate this behavior I have defined a mock service using Mockito this way, note that this is being returned as a mocked Spring bean:
And this is the test for the retry logic:
We are ensuring that the service is called 3 times to account for the first two failed calls and the third call which succeeds.
If we were to directly incorporate spring-retry at the point of calling this service, then the code would have looked like this:
This is not ideal however, a better way would be where the callers don't have have to be explicitly aware of the fact that there is a retry logic in place.
Given this, the following are the approaches to incorporate Spring-retry logic.
This aspect intercepts the remote service call and delegates the call to the retryTemplate. A full working test is here.
The full working test is here.
and a full test which makes use of this declarative retry logic, also available here:
The @EnableRetry annotation activates the processing of @Retryable annotated methods and internally uses logic along the lines of approach 2 without the end user needing to be explicit about it.
I hope this gives you a slightly better taste for how to incorporate Spring-retry in your project. All the code that I have demonstrated here is also available in my github project here: https://github.com/bijukunjummen/test-spring-retry
Consider a service to invoke an external system:
package retry.service;
public interface RemoteCallService {
String call() throws Exception;
}
Assume that this call can fail and you want the call to be retried thrice with a 2 second delay each time the call fails, so to simulate this behavior I have defined a mock service using Mockito this way, note that this is being returned as a mocked Spring bean:
@BeanSo essentially this mocked service fails 2 times and succeeds with the third call.
public RemoteCallService remoteCallService() throws Exception {
RemoteCallService remoteService = mock(RemoteCallService.class);
when(remoteService.call())
.thenThrow(new RuntimeException("Remote Exception 1"))
.thenThrow(new RuntimeException("Remote Exception 2"))
.thenReturn("Completed");
return remoteService;
}
And this is the test for the retry logic:
public class SpringRetryTests {
@Autowired
private RemoteCallService remoteCallService;
@Test
public void testRetry() throws Exception {
String message = this.remoteCallService.call();
verify(remoteCallService, times(3)).call();
assertThat(message, is("Completed"));
}
}
We are ensuring that the service is called 3 times to account for the first two failed calls and the third call which succeeds.
If we were to directly incorporate spring-retry at the point of calling this service, then the code would have looked like this:
@Test
public void testRetry() throws Exception {
String message = this.retryTemplate.execute(context -> this.remoteCallService.call());
verify(remoteCallService, times(3)).call();
assertThat(message, is("Completed"));
}
This is not ideal however, a better way would be where the callers don't have have to be explicitly aware of the fact that there is a retry logic in place.
Given this, the following are the approaches to incorporate Spring-retry logic.
Approach 1: Custom Aspect to incorporate Spring-retry
This approach should be fairly intuitive as the retry logic can be considered a cross cutting concern and a good way to implement a cross cutting concern is using Aspects. An aspect which incorporates the Spring-retry would look something along these lines:package retry.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.support.RetryTemplate;
@Aspect
public class RetryAspect {
private static Logger logger = LoggerFactory.getLogger(RetryAspect.class);
@Autowired
private RetryTemplate retryTemplate;
@Pointcut("execution(* retry.service..*(..))")
public void serviceMethods() {
//
}
@Around("serviceMethods()")
public Object aroundServiceMethods(ProceedingJoinPoint joinPoint) {
try {
return retryTemplate.execute(retryContext -> joinPoint.proceed());
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
This aspect intercepts the remote service call and delegates the call to the retryTemplate. A full working test is here.
Approach 2: Using Spring-retry provided advice
Out of the box Spring-retry project provides an advice which takes care of ensuring that targeted services can be retried. The AOP configuration to weave the advice around the service requires dealing with raw xml as opposed to the previous approach where the aspect can be woven using Spring Java configuration. The xml configuration looks like this:<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:config>
<aop:pointcut id="transactional"
expression="execution(* retry.service..*(..))" />
<aop:advisor pointcut-ref="transactional"
advice-ref="retryAdvice" order="-1"/>
</aop:config>
</beans>
The full working test is here.
Approach 3: Declarative retry logic
This is the recommended approach, you will see that the code is far more concise than with the previous two approaches. With this approach, the only thing that needs to be done is to declaratively indicate which methods need to be retried:package retry.service;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
public interface RemoteCallService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
String call() throws Exception;
}
and a full test which makes use of this declarative retry logic, also available here:
package retry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import retry.service.RemoteCallService;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringRetryDeclarativeTests {
@Autowired
private RemoteCallService remoteCallService;
@Test
public void testRetry() throws Exception {
String message = this.remoteCallService.call();
verify(remoteCallService, times(3)).call();
assertThat(message, is("Completed"));
}
@Configuration
@EnableRetry
public static class SpringConfig {
@Bean
public RemoteCallService remoteCallService() throws Exception {
RemoteCallService remoteService = mock(RemoteCallService.class);
when(remoteService.call())
.thenThrow(new RuntimeException("Remote Exception 1"))
.thenThrow(new RuntimeException("Remote Exception 2"))
.thenReturn("Completed");
return remoteService;
}
}
}
The @EnableRetry annotation activates the processing of @Retryable annotated methods and internally uses logic along the lines of approach 2 without the end user needing to be explicit about it.
I hope this gives you a slightly better taste for how to incorporate Spring-retry in your project. All the code that I have demonstrated here is also available in my github project here: https://github.com/bijukunjummen/test-spring-retry