Como usar Spring Boot Retry

Spring Boot Retry ofrece una forma simple para reintentar alguna operación que ha fallado. Esto es útil sobre todo cuando se tratan errores temporales o transitorios como el acceso a un recurso externo.

Spring Boot Retry puede configurarse de forma declarativa mediante anotaciones o definiendo una config general.
Vamos a ver aquí cómo utilizar la funcionalidad Retry dentro de SpringBoot.

Dependencias necesarias para Spring Boot Retry

Las dependencias que necesitas son las siguientes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>

   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter</artifactId>
   </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>


</dependencies>

Como activar Retry en Spring Boot

Para activar Spring Retry debes anotar tu aplicación @EnableRetry

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableRetry
public class Application {

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

}

Como indicar que un método es ‘reintentable’

Para indicar que un método es reintentable usas esta anotación @Retryable de este modo. Puedes además establecer la máxima cantidad de intentos maxAttempts, y la demora entre intentos backoff entre otras opciones.

Para recuperarse de un método que, a pesar de los reintentos, sigue fallando puedes anotar un método de recuperación @Recover que será llamado por Spring al agotar todos los intentos. Este método debe devolver el mismo tipo de dato que el método original. Observa que puedes recibir como primer parámetro la excepción y subsecuentemente el resto de parámetros del método que ha fallado.

Hemos definido una excepción propia, ante la cual deseamos que se realicen los reintentos.

1
2
3
4
5
public class ApiRetryException extends RuntimeException {
    public ApiRetryException(String message) {
        super(message);
    }
}

En este ejemplo si recibimos en nuestro método el parámetro “error”, provocamos intencionalmente un error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class RetryExampleService {

    private static Logger LOGGER = LoggerFactory.getLogger(RetryExampleService.class);

    @Retryable(value = {RuntimeException.class}, maxAttempts = 4, backoff = @Backoff(1000))
    public String retryExample(String s) {
        LOGGER.info("Retry retryTemplateExample {}", LocalDateTime.now().getSecond());
        if (s == "error") {
            throw new ApiRetryException("Error in RetryExampleService.retryExample ");
        } else {
            return "Hi " + s;
        }
    }

    @Recover
    public String retryExampleRecovery(RuntimeException t, String s) {
        LOGGER.info("Retry Recovery - {}", t.getMessage());
        return "Retry Recovery OK!";
    }
}

Probar Retry con un test

Vamos a probar si nuestro método puede recuperarse con @Recovery. Nuestro método de recuperación para este ejemplo simplemente devuelve un String “Retry Recovery OK!”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RunWith(SpringRunner.class)
@SpringBootTest
public class RetryExampleServiceTest {

   @Autowired
   private RetryExampleService retryExampleService;

   @Test
   public void retryExampleWithRecoveryTest() throws Exception {
       String result = retryExampleService.retryExample("error");
       Assert.assertEquals("Retry Recovery OK!", result);
   }

}

La salida de este test RetryExampleServiceTest

INFO 74514 --- [main] com.gp.service.RetryExampleService       : Retry retryTemplateExample 52
INFO 74514 --- [main] com.gp.service.RetryExampleService       : Retry retryTemplateExample 53
INFO 74514 --- [main] com.gp.service.RetryExampleService       : Retry retryTemplateExample 54
INFO 74514 --- [main] com.gp.service.RetryExampleService       : Retry retryTemplateExample 55
INFO 74514 --- [main] com.gp.service.RetryExampleService       : Retry Recovery - Error in RetryExampleService.retryExample 

Con definir un template para Retry usando RetryTemplate

Además de usar las anotaciones @Retryable y @Recover puedes crear un template en tu configuración para usarlo de manera general.

Para definir un template para los Retry en tu config defines un bean usando org.springframework.retry.support.RetryTemplate. Básicamente tienes dos atributos importantes: cantidad de intentos y el tiempo entre reintentos.

Si deseas puedes también definir un listener a partir de la clase RetryListenerSupport y registrarlo en el template.

Aquí definimos un reintento con un máximo de 4 intentos y 1000 milisegundos entre cada uno.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
public class RetryConfig {

    @Bean
    public RetryTemplate retryTemplate() {

        return RetryTemplate.builder()
                .maxAttempts(4)
                .fixedBackoff(1000L)
                .retryOn(ApiRetryException.class)
                .withListener(new ApiRetryListener())
                .build();
    }

}

Para el listener extiendes de RetryListenerSupport en donde tienes definido tres métodos:

  • open cuando se inicia,
  • onError al momento de producirse el error y reintentar y
  • close cuando se agotaron todos los intentos o cuando finaliza porque ya no ocurrio ningún error.

En el listener tienes un RetryContext del cual puedes obtener información o pasar datos entre tu método y el listener.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ApiRetryListener extends RetryListenerSupport {

    private static Logger LOGGER = LoggerFactory.getLogger(ApiRetryListener.class);

    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        LOGGER.info("ApiRetryListener.open");
        return super.open(context, callback);
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        LOGGER.info("ApiRetryListener.onError");
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        LOGGER.info("ApiRetryListener.close");
        LOGGER.info("ApiRetryListener.onError isExhausted {}", isExhausted(context));
    }


    private boolean isExhausted(RetryContext context) {
        return context.hasAttribute(RetryContext.EXHAUSTED);
    }

    private boolean isClosed(RetryContext context) {
        return context.hasAttribute(RetryContext.CLOSED);
    }

    private boolean isRecovered(RetryContext context) {
        return context.hasAttribute(RetryContext.RECOVERED);
    }

}

Luego para utilizar este template usas el bean que definiste llamando al método retryTemplate.execute(..)

Este método recibe una interfaz RetryCallback en la que debes implementar doWithRetry y alli ejecutar tu código.

1
2
3
4
5
6
    retryTemplate.execute(new RetryCallback<String, RuntimeException>() {
        @Override
        public String doWithRetry(RetryContext retryContext) {
            // do process
        }
    });

Veamos esto con nuestro ejemplo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class RetryTemplateExampleService {

    private static Logger log = LoggerFactory.getLogger(RetryTemplateExampleService.class);
    private final RetryTemplate retryTemplate;

    @Autowired
    public RetryTemplateExampleService(RetryTemplate retryTemplate) {
        this.retryTemplate = retryTemplate;
    }

    public String retryTemplateExample(String s) {
        String result;
        result = retryTemplate.execute(new RetryCallback<String, RuntimeException>() {
            @Override
            public String doWithRetry(RetryContext retryContext) {
                // do something in this service
                log.info(String.format("Retry retryTemplateExample %d", LocalDateTime.now().getSecond()));
                if (s.equals("error")) {
                    throw new ApiRetryException("Error in process");
                } else {
                    return "Hi " + s;
                }
            }
        });
        log.info("Returning {}", result);
        return result;
    }
}

Probar RetryTemplate con un test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringRunner.class)
@SpringBootTest
public class RetryTemplateExampleServiceTest {

    @Autowired
    private RetryTemplateExampleService retryTemplateExampleService;

    @Test(expected = ApiRetryException.class)
    public void retryTemplateExampleShouldThrowRuntime() throws Exception {
        retryTemplateExampleService.retryTemplateExample("error");
    }

    @Test
    public void retryTemplateExampleShouldReturnCorrectValue() throws Exception {
        String s = retryTemplateExampleService.retryTemplateExample("word");
        Assert.assertEquals("Hi word", s);
    }

}

La salida de este test


// retryTemplateExampleShouldThrowRuntime
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.open
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 1
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 2
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 4
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 5
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.close
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError isExhausted true

// retryTemplateExampleShouldReturnCorrectValue
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.open
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 5
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.close
INFO 30304 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError isExhausted false
INFO 30304 --- [main] c.g.service.RetryTemplateExampleService  : Returning Hi word

Conclusión

Hemos revisado cómo usar Spring Retry para reintentar alguna operación en nuestro código si ha fallado y cómo crear un método de recuperación si lo necesitaramos. Vimos que podemos definir esto simplemente con una anotación o con un template a usar.

Puedes ver este código completo