解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

网友投稿 229 2023-01-07

解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

首先说结论,使用@ControllerAdvice配合@ExceptionHandler处理全局controller的异常时,如果想要正确匹配自己的自定义异常,需要在controller的方法上抛出相应的自定义异常,或者自定义异常继承RuntimeException类。

问题描述:

1、在使用@ControllerAdvice配合@ExceptionHandler处理全局异常时,自定义了一个AppException(extends Exception),由于有些全局的参数需要统一验证,所以在所有controller的方法上加一层AOP校验,如果参数校验没通过也抛出AppException

2、在@ControllerAdvice标记的类上,主要有两个@ExceptionHandler,分别匹配AppException.class和Throwable.class。

3、在测试时,由于全局AOP的参数校验没通过,抛出了AppException,但是发现这个AppException被Throwable.class匹配到了,而不是我们想要的AppException.class匹配上。

分析过程:

一阶段

开始由于一直测试的两个不同的请求(一个通过swagger,一个通过游览器地址输入,两个请求比较相似,我以为是同一个请求),一个方法上抛出了AppException,一个没有,然后发现这个问题时现时不现,因为无法稳定复现问题,我猜测可能是AppException出了问题,所以我修改了AppException,将其父类改为了RuntimeException,然后发现问题解决了

二阶段

问题解决后,我又思考了下为啥会出现这种情况,根据java的异常体系来说,无论是继承Exception还是RuntimeException,都不应该会匹配到Throwable.class上去。

我再次跟踪了异常的执行过程,粗略的过了一遍,发现在下面这个位置出现了差别:

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);

}

}

成功的走的是Exception,失败的走的是RuntimeException。

这时候到了@ControllerAdvice标记的类时就会出问题了,因为继承AppException是和RuntimeException是平级,所以如果走runtimeException这个判断条件抛出去的异常注定就不会被AppException匹配上。

这时候再仔细对比下异常类型,可以发现正确的那个异常类型时AppException,而错误的那个异常类型时java.lang.reflect.UndeclaredThrowableException,内部包着AppException。

JDK的java doc是这么解释UndeclaredThrowableException的:如果代理实例的调用处理程序的 invoke 方法抛出一个经过检查的异常(不可分配给 RuntimeException 或 Error 的 Throwable),且该异常不可分配给该方法的throws子局声明的任何异常类,则由代理实例上的方法调用抛出此异常。

因为AppException继承于Exception,所以代理抛出的异常就是包着AppException的UndeclaredThrowableException,在@ControllerAdvice匹配的时候自然就匹配不上了。

而当AppException继承于RuntimeException时,抛出的异常依旧是AppException,所以能够被匹配上。

结论:所以解决方法有两种:AppException继承RuntimeException或者Controller的方法抛出AppException异常。

Spring的@ExceptionHandler和@ControllerAdvice统一处理异常

之前敲代码的时候,避免不了各种try…catch,如果业务复杂一点,就会发现全都是try…catch

try{

..........

}catch(Exception1 e){

..........

}catch(Exception2 e){

...........

}catch(Exception3 e){

...........

}

这样其实代码既不简洁好看 ,我们敲着也烦, 一般我们可能想到用拦截器去处理, 但是既然现在Spring这么火,AOP大家也不陌生, 那么Spring一定为我们想好了这个解决办法.果然:

@ExceptionHandler

源码

//该注解作用对象为方法

@Target({ElementType.METHOD})

//在运行时有效

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface ExceptionHandler {

//value()可以指定异常类

Class extends Throwable>[] value() default {};

}

@ControllerAdvice

源码

@Target({ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Documented

//bean对象交给spring管理生成

@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 {};

}

从名字上可以看出大体意思是控制器增强

所以结合上面我们可以知道,使用@ExceptionHandler,可以处理异常, 但是仅限于当前Controller中处理异常,

@ControllerAdvice可以配置basePackage下的所有controller. 所以结合两者使用,就可以处理全局的异常了.

一、代码

这里需要声明的是,这个统一异常处理类,也是基于ControllerAdvice,也就是控制层切面的,如果是过滤器抛出的异常,不会被捕获!!!

在@ControllerAdvice注解下的类,里面的方法用@ExceptionHandler注解修饰的方法,会将对应的异常交给对应的方法处理。

@ExceptionHandler({IOException.class})

public Result handleException(IOExceptione) {

log.error("[handleException] ", e);

return ResultUtil.failureDefaultError();

}

比如这个,就是捕获IO异常并处理。

废话不多说,代码:

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

* 全局的异常处理

*

* @author zgd

* @date 2019/7/19 11:01

*/

@ControllerAdvice

@ResponseBody

@Slf4j

public class GlobalExceptionHandle {

/**

* 请求参数错误

*/

private final static String BASE_PARAM_ERR_CODE = "BASE-PARAM-01";

private final static String BASE_PARAM_ERR_MSG = "参数校验不通过";

/**

* 无效的请求

*/

private final static String BASE_BAD_REQUEST_ERR_CODE = "BASE-PARAM-02";

private final static String BASE_BAD_REQUEST_ERR_MSG = "无效的请求";

/**

* 顶级的异常处理

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.OK)

@ExceptionHandler({Exception.class})

public Result handleException(Exception e) {

log.error("[handleException] ", e);

return ResultUtil.failureDefaultError();

}

/**

* 自定义的异常处理

*

* @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 = "参数校验不通过:" + 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("【错误码】:{},【错误码内部描述】:{},【错误码外部描述】:{}", errorCode, innerErrMsg, outerErrMsg);

return ResultUtil.failure(errorCode, outerErrMsg);

}

/**

* 缺少servlet请求参数抛出的异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.BAD_REQUEST)

@ExceptionHandler({MissingServletRequestParameterException.class})

public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {

log.warn("[handleMissingServletRequestParameterException] 参数错误: " + e.getParameterName());

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* 请求参数不能正确读取解析时,抛出的异常,比如传入和接受的参数类型不一致

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.OK)

@ExceptionHandler({HttpMessageNotReadableException.class})

public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {

log.warn("[handleHttpMessageNotReadableException] 参数解析失败:", e);

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* 请求参数无效抛出的异常

*

* @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] 参数验证失败:" + 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() : "空";

String code = error != null ? error.getDefaultMessage() : "空";

return String.format("%s:%s", field, code);

}

/**

* 方法请求参数类型不匹配异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.BAD_REQUEST)

@ExceptionHandler({MethodArgumentTypeMismatchException.class})

public Result handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {

log.warn("[handleMethodArgumentTypeMismatchException] 方法参数类型不匹配异常: ", e);

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* 请求参数绑定到controller请求参数时的异常

*

* @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] 参数绑定失败:" + message);

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* javax.validation:validation-api 校验参数抛出的异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.BAD_REQUEST)

@ExceptionHandler({ConstraintViolationException.class})

public Result handleServiceException(ConstraintViolationException e) {

Set> violations = e.getConstraintViolations();

ConstraintViolation> violation = violations.iterator().next();

String message = violation.getMessage();

log.warn("[handleServiceException] 参数验证失败:" + message);

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* javax.validation 下校验参数时抛出的异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.BAD_REQUEST)

@ExceptionHandler({ValidationException.class})

public Result handleValidationException(ValidationException e) {

log.warn("[handleValidationException] 参数验证失败:", e);

return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);

}

/**

* 不支持该请求方法时抛出的异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)

@ExceptionHandler({HttpRequestMethodNotSupportedException.class})

public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {

log.warn("[handleHttpRequestMethodNotSupportedException] 不支持当前请求方法: ", e);

return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);

}

/**

* 不支持当前媒体类型抛出的异常

*

* @param e

* @return

*/

@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)

@ExceptionHandler({HttpMediaTypeNotSupportedException.class})

public Result handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {

log.warn("[handleHttpMediaTypeNotSupportedException] 不支持当前媒体类型: ", e);

return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);

}

}

至于返回值,就可以理解为controller层方法的返回值,可以返回@ResponseBody,或者页面。我这里是一个@ResponseBody的Result<>,前后端分离。

我们也可以自己根据需求,捕获更多的异常类型。

包括我们自定义的异常类型。比如:

package com.zgd.shop.core.exception;

import lombok.Data;

/**

* BizServiceException

* 业务抛出的异常

* @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;

}

}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:浅谈java socket的正确关闭姿势
下一篇:api网站接口源码(API接口网站源码)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~