spring-bootspring-cloudresttemplatefabric8-kubernetes-client

Spring RestTemplate is not finding recovery method


I have this org.springframework.web.client.RestTemplate code used in old Spring boot microservices configured to work with Eureka client/Server. I'm trying to make it work with org.springframework.cloud:spring-cloud-kubernetes-client-discovery into Kubernetes.

In this code I get incorrect code execution:

try {
      ResponseEntity<AccountGroup> response =
          connector.getAccountGroup(accountGroup.getValue());
      if (response != null && HttpStatus.NOT_FOUND.equals(response.getStatusCode())) {
        response = connector.exchange(............);
      }
      if (response == null || !response.getStatusCode().is2xxSuccessful()) {
        // call this methid to create a group 
        LOGGER.error("Error while creating group ", accountGroup.getValue());
      }
      return repository.save(accountGroup);
    } catch (DataIntegrityViolationException e) {
      throw new BadRequestException();
    }



// body of method getAccountGroup(....)

private RestTemplate clientRestTemplate;

  @Retryable(maxAttemptsExpression = "3",
      value = ResourceAccessException.class,
      backoff = @Backoff(
          delayExpression = "1000",
          maxDelayExpression = "10000",
          multiplierExpression = "1")
      )
  public <T> ResponseEntity<T> exchange(...........) {
    return clientRestTemplate.exchange(url, method, requestEntity, responseType, uriVariables);
  }

  @Recover
  public <T> ResponseEntity<T> recoverExchange(Exception exception, String service, String url, HttpMethod method,
      HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws Exception {
    ..................
    throw exception;
  }

Into logs I see:

o.s.web.client.RestTemplate: Response 404 NOT_FOUND. 

This is correct response from the target service. Now the code "// call this method to create a group " should be called but it's not. I get error:

org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method

Do you know how this issue can be solved?

Edit: Code example: https://github.com/rcbandit111/error_handler_poc/blob/main/parent/src/main/java/com/parent/web/rest/controller/RetryableRestTemplate.java#L31


Solution

  • There are several issues in your provided GitHub project. I'll start with the less obvious problem: Spring tries to find matching recover methods by comparing signatures. You're using parameterized return types with different generic types T being declared for each individual method:

    1: @Retryable

    public <T> ResponseEntity<T> exchange(String service, String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
    

    2: @Recover

    public <T> ResponseEntity<T> recoverExchange(Exception exception, String service, String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
    

    If you move the type declaration <T> to class level, the T will be equal for both methods and Spring should find the matching recover method. The complete RetryableRestTemplate then looks like this:

    package com.parent.web.rest.controller;
    
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.ResponseEntity;
    import org.springframework.retry.annotation.Backoff;
    import org.springframework.retry.annotation.Recover;
    import org.springframework.retry.annotation.Retryable;
    import org.springframework.web.client.ResourceAccessException;
    import org.springframework.web.client.RestTemplate;
    
    public class RetryableRestTemplate<T> {
    
        private RestTemplate clientRestTemplate;
        private DiscoveryClient discoveryClient;
    
        public RetryableRestTemplate(
          RestTemplate clientRestTemplate,
          DiscoveryClient discoveryClient) {
            this.clientRestTemplate = clientRestTemplate;
            this.discoveryClient = discoveryClient;
        }
    
        @Retryable(maxAttemptsExpression = "3",
          retryFor = ResourceAccessException.class,
          backoff = @Backoff(
            delayExpression = "1000",
            maxDelayExpression = "10000",
            multiplierExpression = "1")
        )
        public ResponseEntity<T> exchange(
          String service, String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) {
            return clientRestTemplate.exchange(url, method, requestEntity, responseType, uriVariables);
        }
    
        @Recover
        public ResponseEntity<T> recoverExchange(
          Exception exception, String service, String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws Exception {
            //  some code here
            throw exception;
        }
    }
    

    Regarding the other issues I can only assume that you provided a minimal example but removed too much to make the example project work. I made the following changes before I could reproduce your question:

    1. add @EnableRetry(proxyTargetClass = true) on the Application class

    2. add implementation 'org.springframework:spring-aspects:6.1.5' to the build.gradle of the parent project

    3. changed the url in RestClient#invoke to return retryableRestTemplate.exchange(service, "http://www.test.com", method, null, null);