Como usar Spring Boot Retry

By | 08/07/2018

Descripción

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

<dependencies>

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

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

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

   <dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjweaver</artifactId>
   </dependency>

</dependencies>

Como activar Retry en Spring Boot

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

@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 parametros del metodo que ha fallado.

@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(String.format("Retry retryTemplateExample %d", LocalDateTime.now().getSecond()));
       if (s == "error") {
           throw new RuntimeException("Error in RetryExampleService.retryExample ");
       } else {
           return "Hi " + s;
       }
   }

   @Recover
   public String retryExampleRecovery(RuntimeException t, String s) {
       LOGGER.info(String.format("Retry Recovery - %s", 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!”

@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.

@Configuration
public class ApiConfig {

   @Bean
   public RetryTemplate retryTemplate() {

       RetryTemplate retryTemplate = new RetryTemplate();

       FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
       fixedBackOffPolicy.setBackOffPeriod(1000L);
       retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

       SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
       retryPolicy.setMaxAttempts(4);
       retryTemplate.setRetryPolicy(retryPolicy);

       retryTemplate.registerListener(new ApiRetryListener());

       return retryTemplate;
   }

}

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.

public class ApiRetryListener extends RetryListenerSupport {

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

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

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

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

}

Luego para utilizar este template usas el bean que definiste llamando al método execute , org.springframework.retry.support.RetryTemplate#execute(org.springframework.retry.RetryCallback)

@Service
public class RetryTemplateExampleService {

   private static Logger LOGGER = 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
               LOGGER.info(String.format("Retry retryTemplateExample %d", LocalDateTime.now().getSecond()));
               if (s == "error") {
                   throw new RuntimeException("Error in ExampleRetryTemplateService.retryTemplateExample ");
               } else {
                   return "Hi " + s;
               }
           }
       });
       LOGGER.info(String.format("Returning %s", result));
       return result;
   }

}

Probar RetryTemplate con un test

@RunWith(SpringRunner.class)
@SpringBootTest
public class RetryTemplateExampleServiceTest {

   @Autowired
   private RetryTemplateExampleService retryTemplateExampleService;

   @Test(expected = RuntimeException.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 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.open
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 29
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 30
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 31
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 32
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.onError
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.close


// retryTemplateExampleShouldReturnCorrectValue
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.open
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Retry retryTemplateExample 32
INFO 75533 --- [main] com.gp.config.ApiRetryListener           : ApiRetryListener.close
INFO 75533 --- [main] c.g.service.RetryTemplateExampleService  : Returning Hi word

Conclusión

Hemos visto 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

Compartir esto:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *