解决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,⽽错误的那个异常类型时
flect.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) {
<("[handleException] ", e);
return ResultUtil.failureDefaultError();
}
⽐如这个,就是捕获IO异常并处理。
废话不多说,代码:
xception;
rror.ErrorCache;
esult.Result;
esult.ResultUtil;
slf4j.Slf4j;
import org.apachemons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.verter.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.hod.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) {
<("[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 = InternalMsg(errorCode);
outerErrMsg = Msg(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);
spring framework版本}
/**
* 请求参数⽆效抛出的异常
*
* @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 = FieldError();
String field = error != null ? Field() : "空";
String code = error != null ? DefaultMessage() : "空";
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<ConstraintViolation<?>> violations = e.getConstraintViolations();
ConstraintViolation<?> violation = violations.iterator().next();
String message = Message();
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<>,前后端分离。
我们也可以⾃⼰根据需求,捕获更多的异常类型。
包括我们⾃定义的异常类型。⽐如:
xception;
import lombok.Data;
/**
* BizServiceException
* 业务抛出的异常
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论