Spring Retry Example

     


TL;DR

When using Spring Retry you need to follow certain rules.

If you call the @Retryable method from another method inside the same class it does not work since Spring Retry uses Spring AOP.

@Recover method should follow the rules about the method signature: Same return type as @Retryable and same arguments plus optional Throwable as the first argument.


Source code for this example can be found on gitlab.


Spring has a spring-retry project which makes it super easy to add retry mechanism to your applications.

You can find the details of the spring-retry project here.

When you are using spring-retry there are some details you need to be careful about. So let’s go over all of these with an example.

Gradle file

Let’s start with creating a build.gradle file with the following content:

 1buildscript {
 2    ext {
 3        springBootVersion = '1.5.8.RELEASE'
 4    }
 5    repositories {
 6        mavenCentral()
 7    }
 8    dependencies {
 9        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
10    }
11}
12
13apply plugin: 'java'
14apply plugin: 'idea'
15apply plugin: 'org.springframework.boot'
16
17group = 'solutions.prodigi'
18version = '0.0.1-SNAPSHOT'
19sourceCompatibility = 1.8
20
21repositories {
22    mavenCentral()
23}
24
25
26dependencies {
27    compile('org.springframework.retry:spring-retry')
28    compile('org.springframework.boot:spring-boot-starter')
29    runtime('org.springframework:spring-aspects')
30    testCompile('org.springframework.boot:spring-boot-starter-test')
31}

This is a very minimal build.gradle file only containing necessary dependencies.

Spring configuration

Let’s now create a spring configuration class.

 1package solutions.prodigi.springretryexample;
 2
 3import org.springframework.boot.SpringBootConfiguration;
 4import org.springframework.context.annotation.ComponentScan;
 5import org.springframework.retry.annotation.EnableRetry;
 6
 7@SpringBootConfiguration
 8@ComponentScan
 9@EnableRetry
10public class SpringRetryExampleConfiguration {
11}

The configuration class makes component scanning possible and also adds retry support. It also uses @SpringBootConfiguration annotation so that the test classes can automatically find it.

@Retryable in the same class

Now let’s create a service and try to add retry mechanism to it.

 1package solutions.prodigi.springretryexample;
 2
 3import org.slf4j.Logger;
 4import org.slf4j.LoggerFactory;
 5import org.springframework.retry.annotation.Retryable;
 6import org.springframework.stereotype.Service;
 7
 8@Service
 9public class GreetingsService {
10    private static final Logger LOGGER = LoggerFactory.getLogger(GreetingsService.class);
11    private boolean fail = true;
12
13    public String greetMe(String name) {
14        LOGGER.info("Greet me is called with name: {}", name);
15        return String.format("%s %s", this.getGreetingMessage(), name);
16    }
17
18    @Retryable
19    private String getGreetingMessage() {
20        LOGGER.info("getGreetingMessage is called.");
21        if (fail) {
22            fail = false;
23            LOGGER.error("Failing");
24            throw new RuntimeException("Failed doing the important stuff");
25        }
26        return "Hello";
27    }
28}

This is our first try to add retry. getGreetingMessage method throws an exception when it is called for the first time. If you call the same method again it has to return “Hello”.

Since by default @Retryable retries 3 times, greetMe should return a string value instead of throwing an exception generated by getGreetingMessage.

Let’s write a test and verify this behaviour.

 1package solutions.prodigi.springretryexample;
 2
 3import org.junit.Test;
 4import org.junit.runner.RunWith;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.boot.test.context.SpringBootTest;
 7import org.springframework.test.context.junit4.SpringRunner;
 8
 9import static org.hamcrest.CoreMatchers.is;
10import static org.junit.Assert.assertThat;
11
12@RunWith(SpringRunner.class)
13@SpringBootTest
14public class GreetingsServiceTest {
15
16    @Autowired
17    private GreetingsService greetingsService;
18
19    @Test
20    public void canGreetMe() throws Exception {
21        assertThat(greetingsService.greetMe("baran"), is("Hello baran"));
22    }
23
24}

When you run the test you should see the following lines in the console:

2017-11-10 10:23:57.055  INFO 3522 --- [           main] s.p.springretryexample.GreetingsService  : Greet me is called with name: baran
2017-11-10 10:23:57.058  INFO 3522 --- [           main] s.p.springretryexample.GreetingsService  : getGreetingMessage is called.
2017-11-10 10:23:57.058 ERROR 3522 --- [           main] s.p.springretryexample.GreetingsService  : Failing

java.lang.RuntimeException: Failed doing the important stuff

As you can see our test has failed since getGreetingMessage is only called once.

But why didn’t it work?

Spring retry is using Spring AOP. This means that the method that you annotate with @Retryable should be in a spring managed bean and must be called from another spring managed bean.

If you call the method from within the same class you are short circuiting the proxy generated by spring.

Let’s fix it. Let’s move the retryable method to a different service.

 1package solutions.prodigi.springretryexample;
 2
 3import org.slf4j.Logger;
 4import org.slf4j.LoggerFactory;
 5import org.springframework.retry.annotation.Retryable;
 6import org.springframework.stereotype.Service;
 7
 8@Service
 9public class GreetingMessageService {
10    private static final Logger LOGGER = LoggerFactory.getLogger(GreetingMessageService.class);
11
12    private boolean fail = true;
13
14    @Retryable
15    public String getGreetingMessage() {
16        LOGGER.info("getGreetingMessage is called.");
17        if (fail) {
18            fail = false;
19            LOGGER.error("Failing");
20            throw new RuntimeException("Failed doing the important stuff");
21        }
22        return "Hello";
23    }
24}

And let’s refactor GreetingsService.

 1package solutions.prodigi.springretryexample;
 2
 3import org.slf4j.Logger;
 4import org.slf4j.LoggerFactory;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.stereotype.Service;
 7
 8@Service
 9public class GreetingsService {
10
11    private static final Logger LOGGER = LoggerFactory.getLogger(GreetingsService.class);
12
13    private final GreetingMessageService greetingMessageService;
14
15    @Autowired
16    public GreetingsService(GreetingMessageService greetingMessageService) {
17        this.greetingMessageService = greetingMessageService;
18    }
19
20    public String greetMe(String name) {
21        LOGGER.info("Greet me is called with name: {}", name);
22        return String.format("%s %s", greetingMessageService.getGreetingMessage(), name);
23    }
24}

If you know run GreetingsServiceTest you should see the following output:

2017-11-10 10:57:36.684  INFO 3990 --- [           main] s.p.springretryexample.GreetingsService  : Greet me is called with name: baran
2017-11-10 10:57:36.703  INFO 3990 --- [           main] s.p.s.GreetingMessageService             : getGreetingMessage is called.
2017-11-10 10:57:36.703 ERROR 3990 --- [           main] s.p.s.GreetingMessageService             : Failing
2017-11-10 10:57:37.707  INFO 3990 --- [           main] s.p.s.GreetingMessageService             : getGreetingMessage is called.

Output shows that retry is working and test is now passing.

@Recover

Spring retry has @Recover annotation. Methods annotated with @Recover is called if all of the previous tries fail.

You should be careful when using @Recover. Methods annotated with @Recover should follow some rules.

Let’s refactor GreetingMessageService and add a recover method.

 1package solutions.prodigi.springretryexample;
 2
 3import org.slf4j.Logger;
 4import org.slf4j.LoggerFactory;
 5import org.springframework.retry.annotation.Recover;
 6import org.springframework.retry.annotation.Retryable;
 7import org.springframework.stereotype.Service;
 8
 9@Service
10public class GreetingMessageService {
11    private static final Logger LOGGER = LoggerFactory.getLogger(GreetingMessageService.class);
12
13    @Retryable
14    public String getGreetingMessage() {
15        LOGGER.error("Trying to get a greeting message");
16        throw new RuntimeException("Failed doing the important stuff");
17    }
18
19    @Recover
20    public String recover(Throwable t) {
21        LOGGER.error("Recovering from {}", t.getMessage());
22        return "Hello";
23    }
24}

If you run the test again you should see the following output and test is still passing.

2017-11-10 11:09:10.090  INFO 4134 --- [           main] s.p.springretryexample.GreetingsService  : Greet me is called with name: baran
2017-11-10 11:09:10.109  INFO 4134 --- [           main] s.p.s.GreetingMessageService             : Trying to get a greeting message
2017-11-10 11:09:11.113  INFO 4134 --- [           main] s.p.s.GreetingMessageService             : Trying to get a greeting message
2017-11-10 11:09:12.116  INFO 4134 --- [           main] s.p.s.GreetingMessageService             : Trying to get a greeting message
2017-11-10 11:09:12.117 ERROR 4134 --- [           main] s.p.s.GreetingMessageService             : Recovering from Failed doing the important stuff

As you can see the getGreetingMessage method is called 3 times and all of them failed. Finally recover method is called and returned the expected string.