解決spring @ControllerAdvice處理異常無法正確匹配自定義異常
首先說結論,使用@ControllerAdvice配合@ExceptionHandler處理全局controller的異常時,如果想要正確匹配自己的自定義異常,需要在controller的方法上拋出相應的自定義異常,或者自定義異常繼承RuntimeException類。
問題描述:
1、在使用@ControllerAdvice配合@ExceptionHandler處理全局異常時,自定義了一個AppException(extends Exception),由于有些全局的參數(shù)需要統(tǒng)一驗證,所以在所有controller的方法上加一層AOP校驗,如果參數(shù)校驗沒通過也拋出AppException
2、在@ControllerAdvice標記的類上,主要有兩個@ExceptionHandler,分別匹配AppException.class和Throwable.class。
3、在測試時,由于全局AOP的參數(shù)校驗沒通過,拋出了AppException,但是發(fā)現(xiàn)這個AppException被Throwable.class匹配到了,而不是我們想要的AppException.class匹配上。
分析過程:
一階段
開始由于一直測試的兩個不同的請求(一個通過swagger,一個通過游覽器地址輸入,兩個請求比較相似,我以為是同一個請求),一個方法上拋出了AppException,一個沒有,然后發(fā)現(xiàn)這個問題時現(xiàn)時不現(xiàn),因為無法穩(wěn)定復現(xiàn)問題,我猜測可能是AppException出了問題,所以我修改了AppException,將其父類改為了RuntimeException,然后發(fā)現(xiàn)問題解決了
二階段
問題解決后,我又思考了下為啥會出現(xiàn)這種情況,根據(jù)java的異常體系來說,無論是繼承Exception還是RuntimeException,都不應該會匹配到Throwable.class上去。
我再次跟蹤了異常的執(zhí)行過程,粗略的過了一遍,發(fā)現(xiàn)在下面這個位置出現(xiàn)了差別:
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匹配上。
這時候再仔細對比下異常類型,可以發(fā)現(xiàn)正確的那個異常類型時AppException,而錯誤的那個異常類型時java.lang.reflect.UndeclaredThrowableException,內部包著AppException。
JDK的java doc是這么解釋UndeclaredThrowableException的:如果代理實例的調用處理程序的 invoke 方法拋出一個經(jīng)過檢查的異常(不可分配給 RuntimeException 或 Error 的 Throwable),且該異常不可分配給該方法的throws子局聲明的任何異常類,則由代理實例上的方法調用拋出此異常。
因為AppException繼承于Exception,所以代理拋出的異常就是包著AppException的UndeclaredThrowableException,在@ControllerAdvice匹配的時候自然就匹配不上了。
而當AppException繼承于RuntimeException時,拋出的異常依舊是AppException,所以能夠被匹配上。
結論:所以解決方法有兩種:AppException繼承RuntimeException或者Controller的方法拋出AppException異常。
Spring的@ExceptionHandler和@ControllerAdvice統(tǒng)一處理異常
之前敲代碼的時候,避免不了各種try…catch,如果業(yè)務復雜一點,就會發(fā)現(xiàn)全都是try…catch
try{
..........
}catch(Exception1 e){
..........
}catch(Exception2 e){
...........
}catch(Exception3 e){
...........
}
這樣其實代碼既不簡潔好看 ,我們敲著也煩, 一般我們可能想到用攔截器去處理, 但是既然現(xiàn)在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. 所以結合兩者使用,就可以處理全局的異常了.
一、代碼
這里需要聲明的是,這個統(tǒng)一異常處理類,也是基于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 {
/**
* 請求參數(shù)錯誤
*/
private final static String BASE_PARAM_ERR_CODE = "BASE-PARAM-01";
private final static String BASE_PARAM_ERR_MSG = "參數(shù)校驗不通過";
/**
* 無效的請求
*/
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 = "參數(shù)校驗不通過:" + 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請求參數(shù)拋出的異常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({MissingServletRequestParameterException.class})
public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
log.warn("[handleMissingServletRequestParameterException] 參數(shù)錯誤: " + e.getParameterName());
return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
}
/**
* 請求參數(shù)不能正確讀取解析時,拋出的異常,比如傳入和接受的參數(shù)類型不一致
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler({HttpMessageNotReadableException.class})
public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("[handleHttpMessageNotReadableException] 參數(shù)解析失?。?, e);
return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
}
/**
* 請求參數(shù)無效拋出的異常
*
* @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] 參數(shù)驗證失敗:" + 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);
}
/**
* 方法請求參數(shù)類型不匹配異常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({MethodArgumentTypeMismatchException.class})
public Result handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.warn("[handleMethodArgumentTypeMismatchException] 方法參數(shù)類型不匹配異常: ", e);
return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
}
/**
* 請求參數(shù)綁定到controller請求參數(shù)時的異常
*
* @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] 參數(shù)綁定失?。? + message);
return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
}
/**
* javax.validation:validation-api 校驗參數(shù)拋出的異常
*
* @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] 參數(shù)驗證失敗:" + message);
return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
}
/**
* javax.validation 下校驗參數(shù)時拋出的異常
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ValidationException.class})
public Result handleValidationException(ValidationException e) {
log.warn("[handleValidationException] 參數(shù)驗證失?。?, 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<>,前后端分離。
我們也可以自己根據(jù)需求,捕獲更多的異常類型。
包括我們自定義的異常類型。比如:
package com.zgd.shop.core.exception;
import lombok.Data;
/**
* BizServiceException
* 業(yè)務拋出的異常
* @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;
}
}
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Java實現(xiàn)Word/Pdf/TXT轉html的示例
這篇文章主要介紹了Java實現(xiàn)Word/Pdf/TXT轉html的示例,幫助大家方便的進行文件格式轉換,完成需求,感興趣的朋友可以了解下2020-11-11
springboot項目使用nohup將日志指定輸出文件過大問題及解決辦法
在Spring Boot項目中,使用nohup命令重定向日志輸出到文件可能會使日志文件過大,文章介紹了兩種解決方法:一是創(chuàng)建腳本直接清除日志文件,二是創(chuàng)建腳本保留部分日志內容,并將這些腳本加入定時任務中,這可以有效控制日志文件的大小,避免占用過多磁盤空間2024-10-10
spring-boot-starter-web更換默認Tomcat容器的方法
Spring Boot支持容器的自動配置,默認是Tomcat,當然我們也是可以進行修改的。下面小編給大家?guī)砹藄pring-boot-starter-web更換默認Tomcat容器的方法,感興趣的朋友跟隨小編一起看看吧2019-04-04
Java線程池隊列PriorityBlockingQueue和SynchronousQueue詳解
這篇文章主要為大家介紹了Java線程池隊列PriorityBlockingQueue和SynchronousQueue詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12

