Resolving spring @ ControllerAdvice Handling Exception Does Not Match Custom Exception Correctly

  • 2021-09-16 07:06:40
  • OfStack

First of all, when using @ ControllerAdvice and @ ExceptionHandler to handle the exception of global controller, if you want to match your own custom exception correctly, you need to throw the corresponding custom exception on the method of controller, or the custom exception inherits the RuntimeException class.

Problem description:

1. When using @ ControllerAdvice and @ ExceptionHandler to handle global exceptions, a user-defined AppException (extends Exception) is created. Because some global parameters need to be verified by Unified 1, one layer of AOP verification is added to all controller methods, and AppException is thrown if the parameter verification fails

2. On the @ ControllerAdvice tagged class, there are mainly two @ ExceptionHandler, which match AppException. class and Throwable. class respectively.

3. During the test, AppException was thrown because the parameter check of global AOP failed, but it was found that this AppException was matched by Throwable. class instead of the AppException. class we wanted.

Analysis process:

Phase 1

At first, I tested two different requests directly (one through swagger, one through the visitor address, the two requests are similar, I thought they were the same request). One method throws AppException, one does not, and then finds that this problem does not appear now. Because the problem cannot be reproduced stably, I guess it may be that there is something wrong with AppException, so I modified AppException and changed its parent class to RuntimeException, and then found that the problem was solved

Phase 2

After the problem was solved, I thought about why this happened. According to the exception system of java, neither Exception nor RuntimeException should be matched to Throwable. class.

I tracked the execution of the exception again, roughly once, and found that there was a difference in the following position:


catch (InvocationTargetException ex) {
            // Unwrap for HandlerExceptionResolvers ...
            Throwable targetException = ex.getTargetException();
            if (targetException instanceof RuntimeException) {
                throw (RuntimeException) targetException;
            }
            else if (targetException instanceof Error) {
                throw (Error) targetException;
            }
            else if (targetException instanceof Exception) {
                throw (Exception) targetException;
            }
            else {
                String text = getInvocationErrorMessage("Failed to invoke handler method", args);
                throw new IllegalStateException(text, targetException);
            }
        }

The successful one is Exception, and the failed one is RuntimeException.

At this time, when it comes to the class marked by @ ControllerAdvice, there will be a problem, because inheriting AppException is on the same level as RuntimeException, so the exception thrown by taking runtimeException is doomed not to be matched by AppException.

At this time, by carefully comparing the exception types, we can find that the correct exception type is AppException, while the wrong exception type is java. lang. reflect. UndeclaredThrowableException, which is wrapped in AppException.

UndeclaredThrowableException is interpreted by java doc of JDK as follows: If the invoke method of the call handler of the proxy instance throws a checked exception (not assignable to RuntimeException or Throwable of Error) and the exception is not assignable to any exception class declared by the throws suboffice of the method, the exception is thrown by a method call on the proxy instance.

Because AppException inherits from Exception, the exception thrown by the proxy is UndeclaredThrowableException wrapped with AppException, which will naturally not match when @ ControllerAdvice matches.

When AppException inherits from RuntimeException, the exception thrown is still AppException, so it can be matched.

Conclusion: So there are two solutions: AppException inherits RuntimeException or Controller throws AppException exception.

Spring's @ ExceptionHandler and @ ControllerAdvice System 1 Handling Exceptions

When you type the code before, you can't avoid all kinds of try … catch. If the business is 1 point complicated, you will find that all of them are try … catch


try{
    ..........
}catch(Exception1 e){
    ..........
}catch(Exception2 e){
    ...........
}catch(Exception3 e){
    ...........
}

In fact, this code is not simple and good-looking, and we are annoyed when knocking. 1 We may think of using interceptors to deal with it, but since Spring is so hot and AOP is no stranger to everyone, Spring1 is determined that we have thought of this solution. Sure enough:

@ExceptionHandler

Source code


// The annotation works on a method 
@Target({ElementType.METHOD})
// Valid at runtime 
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
 //value() You can specify an exception class 
    Class<? extends Throwable>[] value() default {};
}

@ControllerAdvice

Source code


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//bean Object delivery spring Manage generation 
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<?>[] assignableTypes() default {};
    Class<? extends Annotation>[] annotations() default {};
}

It can be seen from the name that the general meaning is controller enhancement

So in combination with the above, you can handle exceptions with @ ExceptionHandler, but only in the current Controller,

@ ControllerAdvice can configure all controller under basePackage, so using both together can handle global exceptions.

1. Code

What needs to be declared here is that this exception handling class is also based on ControllerAdvice, that is, the control layer section. If it is an exception thrown by the filter, it will not be caught! ! !

Classes under the @ ControllerAdvice annotation whose methods are decorated with the @ ExceptionHandler annotation will hand over the corresponding exception to the corresponding method for handling.


@ExceptionHandler({IOException.class})
public Result handleException(IOExceptione) {
    log.error("[handleException] ", e);
    return ResultUtil.failureDefaultError();
  }

For example, this is to catch IO exceptions and handle them.

Needless to say, the code:


package com.zgd.shop.core.exception;
import com.zgd.shop.core.error.ErrorCache;
import com.zgd.shop.core.result.Result;
import com.zgd.shop.core.result.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.Set;
/**
 * GlobalExceptionHandle
 *  Global exception handling 
 *
 * @author zgd
 * @date 2019/7/19 11:01
 */
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandle {
  /**
   *  Wrong request parameter 
   */
  private final static String BASE_PARAM_ERR_CODE = "BASE-PARAM-01";
  private final static String BASE_PARAM_ERR_MSG = " Parameter verification failed ";
  /**
   *  Invalid request 
   */
  private final static String BASE_BAD_REQUEST_ERR_CODE = "BASE-PARAM-02";
  private final static String BASE_BAD_REQUEST_ERR_MSG = " Invalid request ";
  /**
   *  Top-level exception handling 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({Exception.class})
  public Result handleException(Exception e) {
    log.error("[handleException] ", e);
    return ResultUtil.failureDefaultError();
  }
  /**
   *  Custom exception handling 
   *
   * @param ex
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({BizServiceException.class})
  public Result serviceExceptionHandler(BizServiceException ex) {
    String errorCode = ex.getErrCode();
    String msg = ex.getErrMsg() == null ? "" : ex.getErrMsg();
    String innerErrMsg;
    String outerErrMsg;
    if (BASE_PARAM_ERR_CODE.equalsIgnoreCase(errorCode)) {
      innerErrMsg = " Parameter verification failed: " + msg;
      outerErrMsg = BASE_PARAM_ERR_MSG;
    } else if (ex.isInnerError()) {
      innerErrMsg = ErrorCache.getInternalMsg(errorCode);
      outerErrMsg = ErrorCache.getMsg(errorCode);
      if (StringUtils.isNotBlank(msg)) {
        innerErrMsg = innerErrMsg + " , " + msg;
        outerErrMsg = outerErrMsg + " , " + msg;
      }
    } else {
      innerErrMsg = msg;
      outerErrMsg = msg;
    }
    log.info(" "Error code": {} "Error Code Internal Description": {} "Error Code External Description": {}", errorCode, innerErrMsg, outerErrMsg);
    return ResultUtil.failure(errorCode, outerErrMsg);
  }
  /**
   *  Lack servlet Exception thrown by request parameter 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MissingServletRequestParameterException.class})
  public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
    log.warn("[handleMissingServletRequestParameterException]  Parameter error : " + e.getParameterName());
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   *  Exceptions thrown when the request parameter cannot be read and parsed correctly, such as the incoming and accepted parameter types are not 1 To 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({HttpMessageNotReadableException.class})
  public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
    log.warn("[handleHttpMessageNotReadableException]  Parsing of parameters failed: ", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   *  Exception thrown by invalid request parameter 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MethodArgumentNotValidException.class})
  public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    BindingResult result = e.getBindingResult();
    String message = getBindResultMessage(result);
    log.warn("[handleMethodArgumentNotValidException]  Parameter validation failed: " + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  private String getBindResultMessage(BindingResult result) {
    FieldError error = result.getFieldError();
    String field = error != null ? error.getField() : " Empty ";
    String code = error != null ? error.getDefaultMessage() : " Empty ";
    return String.format("%s:%s", field, code);
  }
  /**
   *  Method request parameter type mismatch exception 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MethodArgumentTypeMismatchException.class})
  public Result handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
    log.warn("[handleMethodArgumentTypeMismatchException]  Method parameter type mismatch exception : ", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   *  Request parameters are bound to the controller Exception when requesting parameters 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({BindException.class})
  public Result handleHttpMessageNotReadableException(BindException e) {
    BindingResult result = e.getBindingResult();
    String message = getBindResultMessage(result);
    log.warn("[handleHttpMessageNotReadableException]  Parameter binding failed: " + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * javax.validation:validation-api  Exception thrown by verification parameter 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({ConstraintViolationException.class})
  public Result handleServiceException(ConstraintViolationException e) {
    Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
    ConstraintViolation<?> violation = violations.iterator().next();
    String message = violation.getMessage();
    log.warn("[handleServiceException]  Parameter validation failed: " + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * javax.validation  Exception thrown when parameterizing 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({ValidationException.class})
  public Result handleValidationException(ValidationException e) {
    log.warn("[handleValidationException]  Parameter validation failed: ", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   *  Exception thrown when the request method is not supported 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
  @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
  public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
    log.warn("[handleHttpRequestMethodNotSupportedException]  The current request method is not supported : ", e);
    return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);
  }
  /**
   *  Exceptions thrown by the current media type are not supported 
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
  @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
  public Result handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
    log.warn("[handleHttpMediaTypeNotSupportedException]  Current media type not supported : ", e);
    return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);
  }
}

As for the return value, it can be understood as the return value of the controller layer method, which can return @ ResponseBody, or the page. I'm here for an Result @ ResponseBody < > The front and rear ends are separated.

We can also catch more exception types according to our needs.

Including our custom exception types. For example:


package com.zgd.shop.core.exception;
import lombok.Data;
/**
 * BizServiceException
 *  Exceptions thrown by business 
 * @author zgd
 * @date 2019/7/19 11:04
 */
@Data
public class BizServiceException extends RuntimeException{
  private String errCode;
  private String errMsg;
  private boolean isInnerError;
  public BizServiceException(){
    this.isInnerError=false;
  }
  public BizServiceException(String errCode){
    this.errCode =errCode;
    this.isInnerError = false;
  }
  public BizServiceException(String errCode,boolean isInnerError){
    this.errCode =errCode;
    this.isInnerError = isInnerError;
  }
  public BizServiceException(String errCode,String errMsg){
    this.errCode =errCode;
    this.errMsg = errMsg;
    this.isInnerError = false;
  }
  public BizServiceException(String errCode,String errMsg,boolean isInnerError){
    this.errCode =errCode;
    this.errMsg = errMsg;
    this.isInnerError = isInnerError;
  }
}

Related articles: