Exception handling operation under SpringCloud feign service fuse

  • 2021-10-13 07:36:28
  • OfStack

When I was working on the project today, Encountered a problem, If I call the interface of a service, But this service hangs up, and the result of this interface is required by the business. What should I do? The answer is through hystrix, but there is another point. The service is not hung up for no reason (excluding problems such as server power failure), that is to say, it may be timeout, or, wrong, argument, etc., so how can I cross hystrix and throw an exception successfully?

Point 1: First summarize the exception handling methods under 1:

1): By writing the @ ExceptionHandler method in controller

Write exception handler methods directly in controller


 @RequestMapping("/test")
 public ModelAndView test()
 {
  throw new TmallBaseException();
 }
 @ExceptionHandler(TmallBaseException.class)
 public ModelAndView handleBaseException()
 {
  return new ModelAndView("error");
 }

However, this method can only be used in this controller. If other controller also throws this exception, it will not be executed

2): Global exception handling:


@ControllerAdvice
public class AdminExceptionHandler
{
 @ExceptionHandler(TmallBaseException.class)
 public ModelAndView hAndView(Exception exception)
 {
  //logic
  return null;
 }
}

aop proxy in essence, as the name says, global exception handling, can handle exceptions thrown by any method

3) By implementing the HandlerExceptionResolver interface of SpringMVC


public static class Tt implements HandlerExceptionResolver
 { 
  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
   Exception ex)
  {
   //logic
   return null;
  }  
 }
 

Then add it in the mvc configuration


@Configuration
public class MyConfiguration extends WebMvcConfigurerAdapter {  
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    // Initialize the exception handler chain 
        exceptionResolvers.add(new Tt());
    } 
}

Next is Fegin. If you want to customize exceptions, you need to know one interface: ErrorDecoder

First, let's look at if decode is carried out after rmi call is finished


Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);
 
    // Code omission 
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
        response.toBuilder().request(request).build();
      }
      if (Response.class == metadata.returnType()) {
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
                response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build();
      }
      // It can be found from here that , If the status code is no longer 200-300, Or 404 At the time of , Means that the abnormal response will resolve the internal exception 
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          return decode(response);
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        return decode(response);
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

The default parsing method is:


 public static class Default implements ErrorDecoder { 
    private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder(); 
    @Override
    public Exception decode(String methodKey, Response response) {
        // Get the error status code , Generate fegin Custom exception
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        // If the retry fails multiple times , The corresponding exception
        return new RetryableException(exception.getMessage(), exception, retryAfter);
      }
    // Otherwise, the default exception
      return exception;
    }

We can find that we have done two things, first get the status code, second throw the exception again, extra judgment whether there are many failures and still error exceptions, and did not encapsulate too many exceptions, so we can encapsulate our custom exceptions

However, note that this piece does not involve hystrix, which means that handling exceptions will still trigger the fuse mechanism. The specific avoidance method will be discussed finally

First we write an BaseException for extension: omit getter/setter


public class TmallBaseException extends RuntimeException
{ 
 /**
  * 
  * @author joker
  * @date  Creation time: 2018 Year 8 Month 18 Day   Afternoon 4:46:54
  */
 private static final long serialVersionUID = -5076254306303975358L;
 //  Unauthenticated 
 public static final int UNAUTHENTICATED_EXCEPTION = 0;
 //  Unauthorized 
 public static final int FORBIDDEN_EXCEPTION = 1;
 //  Timeout 
 public static final int TIMEOUT_EXCEPTION = 2;
 //  Business logic exception 
 public static final int BIZ_EXCEPTION = 3;
 //  Unknown exception -> System anomaly 
 public static final int UNKNOWN_EXCEPTION = 4;
 //  Exception code 
 private int code;
 
 //  Exception information 
 private String message;
 
 public TmallBaseException(int code, String message)
 {
  super(message);
  this.code = code;
  this.message = message;
 }
 
 public TmallBaseException(String message, Throwable cause)
 {
  super(message, cause);
  this.message = message;
 }
 
 public TmallBaseException(int code, String message, Throwable cause)
 {
  super(message, cause);
  this.code = code;
  this.message = message;
 }
}

OK, after we define the base class, we can test it once: service interface controller:


// Show stores that a merchant cooperates with 
 @RequestMapping(value="/store")
 public ResultDTO<Collection<BrandDTO>>findStoreOperatedBrands(@RequestParam("storeId")Long storeId)
 {
         To test , Throw the exception directly first 
  throw new TmallBaseException(TmallBaseException.BIZ_EXCEPTION, "ceshi"); 
    }

Interface:


@RequestMapping(value="/auth/brand/store",method=RequestMethod.POST,produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
ResultDTO<List<BrandDTO>>findStoreOperatedBrands(@RequestParam("storeId")Long storeId);

The rest will not be posted first, and then when we initiated the rest call, we found that the exception was not handled by the exception handler after throwing the exception, because we passed fegin, and I configured the fallback class of feign, and the methods in this class will be automatically called when throwing the exception.

There are two solutions:

1. Remove hystrix directly, obviously its not a good idea

2. Encapsulate the Layer 1 exception class, as follows

The AbstractCommand # handleFallback function is a function for handling exceptions. From the method suffix name, it can be known that when exception is HystrixBadRequestException, it is thrown directly and will not trigger fallback, which means that it will not trigger demotion


final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
            @Override
            public Observable<R> call(Throwable t) {
                circuitBreaker.markNonSuccess();
                Exception e = getExceptionFromThrowable(t);
                executionResult = executionResult.setExecutionException(e);
                if (e instanceof RejectedExecutionException) {
                    return handleThreadPoolRejectionViaFallback(e);
                } else if (t instanceof HystrixTimeoutException) {
                    return handleTimeoutViaFallback();
                } else if (t instanceof HystrixBadRequestException) {
                    return handleBadRequestByEmittingError(e);
                } else {
                    /*
                     * Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException.
                     */
                    if (e instanceof HystrixBadRequestException) {
                        eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
                        return Observable.error(e);
                    }
 
                    return handleFailureViaFallback(e);
                }
            }
        };

In this case, all the 1 cuts are clear, and you can modify the inheritance structure of the class:


@ControllerAdvice
public class AdminExceptionHandler
{
 @ExceptionHandler(TmallBaseException.class)
 public ModelAndView hAndView(Exception exception)
 {
  //logic
  return null;
 }
}
0

As for how to get the exception from the server and then convert it, it is through ErrorHandler mentioned above:


@ControllerAdvice
public class AdminExceptionHandler
{
 @ExceptionHandler(TmallBaseException.class)
 public ModelAndView hAndView(Exception exception)
 {
  //logic
  return null;
 }
}
1

Finally, the global exception handling under microservices is ok. Of course, this ErrorDdecoder and BaseException are recommended to be placed under common module, and all other modules will use it.


Related articles: