解決spring @ControllerAdvice處理異常無法正確匹配自定義異常
首先說結(jié)論,使用@ControllerAdvice配合@ExceptionHandler處理全局controller的異常時(shí),如果想要正確匹配自己的自定義異常,需要在controller的方法上拋出相應(yīng)的自定義異常,或者自定義異常繼承RuntimeException類。
問題描述:
1、在使用@ControllerAdvice配合@ExceptionHandler處理全局異常時(shí),自定義了一個(gè)AppException(extends Exception),由于有些全局的參數(shù)需要統(tǒng)一驗(yàn)證,所以在所有controller的方法上加一層AOP校驗(yàn),如果參數(shù)校驗(yàn)沒通過也拋出AppException
2、在@ControllerAdvice標(biāo)記的類上,主要有兩個(gè)@ExceptionHandler,分別匹配AppException.class和Throwable.class。
3、在測(cè)試時(shí),由于全局AOP的參數(shù)校驗(yàn)沒通過,拋出了AppException,但是發(fā)現(xiàn)這個(gè)AppException被Throwable.class匹配到了,而不是我們想要的AppException.class匹配上。
分析過程:
一階段
開始由于一直測(cè)試的兩個(gè)不同的請(qǐng)求(一個(gè)通過swagger,一個(gè)通過游覽器地址輸入,兩個(gè)請(qǐng)求比較相似,我以為是同一個(gè)請(qǐng)求),一個(gè)方法上拋出了AppException,一個(gè)沒有,然后發(fā)現(xiàn)這個(gè)問題時(shí)現(xiàn)時(shí)不現(xiàn),因?yàn)闊o法穩(wěn)定復(fù)現(xiàn)問題,我猜測(cè)可能是AppException出了問題,所以我修改了AppException,將其父類改為了RuntimeException,然后發(fā)現(xiàn)問題解決了
二階段
問題解決后,我又思考了下為啥會(huì)出現(xiàn)這種情況,根據(jù)java的異常體系來說,無論是繼承Exception還是RuntimeException,都不應(yīng)該會(huì)匹配到Throwable.class上去。
我再次跟蹤了異常的執(zhí)行過程,粗略的過了一遍,發(fā)現(xiàn)在下面這個(gè)位置出現(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。
這時(shí)候到了@ControllerAdvice標(biāo)記的類時(shí)就會(huì)出問題了,因?yàn)槔^承AppException是和RuntimeException是平級(jí),所以如果走runtimeException這個(gè)判斷條件拋出去的異常注定就不會(huì)被AppException匹配上。
這時(shí)候再仔細(xì)對(duì)比下異常類型,可以發(fā)現(xiàn)正確的那個(gè)異常類型時(shí)AppException,而錯(cuò)誤的那個(gè)異常類型時(shí)java.lang.reflect.UndeclaredThrowableException,內(nèi)部包著AppException。
JDK的java doc是這么解釋UndeclaredThrowableException的:如果代理實(shí)例的調(diào)用處理程序的 invoke 方法拋出一個(gè)經(jīng)過檢查的異常(不可分配給 RuntimeException 或 Error 的 Throwable),且該異常不可分配給該方法的throws子局聲明的任何異常類,則由代理實(shí)例上的方法調(diào)用拋出此異常。
因?yàn)锳ppException繼承于Exception,所以代理拋出的異常就是包著AppException的UndeclaredThrowableException,在@ControllerAdvice匹配的時(shí)候自然就匹配不上了。
而當(dāng)AppException繼承于RuntimeException時(shí),拋出的異常依舊是AppException,所以能夠被匹配上。
結(jié)論:所以解決方法有兩種:AppException繼承RuntimeException或者Controller的方法拋出AppException異常。
Spring的@ExceptionHandler和@ControllerAdvice統(tǒng)一處理異常
之前敲代碼的時(shí)候,避免不了各種try…catch,如果業(yè)務(wù)復(fù)雜一點(diǎn),就會(huì)發(fā)現(xiàn)全都是try…catch
try{ .......... }catch(Exception1 e){ .......... }catch(Exception2 e){ ........... }catch(Exception3 e){ ........... }
這樣其實(shí)代碼既不簡(jiǎn)潔好看 ,我們敲著也煩, 一般我們可能想到用攔截器去處理, 但是既然現(xiàn)在Spring這么火,AOP大家也不陌生, 那么Spring一定為我們想好了這個(gè)解決辦法.果然:
@ExceptionHandler
源碼
//該注解作用對(duì)象為方法 @Target({ElementType.METHOD}) //在運(yùn)行時(shí)有效 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ExceptionHandler { //value()可以指定異常類 Class<? extends Throwable>[] value() default {}; }
@ControllerAdvice
源碼
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented //bean對(duì)象交給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 {}; }
從名字上可以看出大體意思是控制器增強(qiáng)
所以結(jié)合上面我們可以知道,使用@ExceptionHandler,可以處理異常, 但是僅限于當(dāng)前Controller中處理異常,
@ControllerAdvice可以配置basePackage下的所有controller. 所以結(jié)合兩者使用,就可以處理全局的異常了.
一、代碼
這里需要聲明的是,這個(gè)統(tǒng)一異常處理類,也是基于ControllerAdvice,也就是控制層切面的,如果是過濾器拋出的異常,不會(huì)被捕獲!!!
在@ControllerAdvice注解下的類,里面的方法用@ExceptionHandler注解修飾的方法,會(huì)將對(duì)應(yīng)的異常交給對(duì)應(yīng)的方法處理。
@ExceptionHandler({IOException.class}) public Result handleException(IOExceptione) { log.error("[handleException] ", e); return ResultUtil.failureDefaultError(); }
比如這個(gè),就是捕獲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 { /** * 請(qǐng)求參數(shù)錯(cuò)誤 */ private final static String BASE_PARAM_ERR_CODE = "BASE-PARAM-01"; private final static String BASE_PARAM_ERR_MSG = "參數(shù)校驗(yàn)不通過"; /** * 無效的請(qǐng)求 */ private final static String BASE_BAD_REQUEST_ERR_CODE = "BASE-PARAM-02"; private final static String BASE_BAD_REQUEST_ERR_MSG = "無效的請(qǐng)求"; /** * 頂級(jí)的異常處理 * * @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ù)校驗(yàn)不通過:" + 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("【錯(cuò)誤碼】:{},【錯(cuò)誤碼內(nèi)部描述】:{},【錯(cuò)誤碼外部描述】:{}", errorCode, innerErrMsg, outerErrMsg); return ResultUtil.failure(errorCode, outerErrMsg); } /** * 缺少servlet請(qǐng)求參數(shù)拋出的異常 * * @param e * @return */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({MissingServletRequestParameterException.class}) public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { log.warn("[handleMissingServletRequestParameterException] 參數(shù)錯(cuò)誤: " + e.getParameterName()); return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG); } /** * 請(qǐng)求參數(shù)不能正確讀取解析時(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); } /** * 請(qǐng)求參數(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ù)驗(yàn)證失敗:" + 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); } /** * 方法請(qǐng)求參數(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); } /** * 請(qǐng)求參數(shù)綁定到controller請(qǐng)求參數(shù)時(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 校驗(yàn)參數(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ù)驗(yàn)證失?。? + message); return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG); } /** * javax.validation 下校驗(yàn)參數(shù)時(shí)拋出的異常 * * @param e * @return */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({ValidationException.class}) public Result handleValidationException(ValidationException e) { log.warn("[handleValidationException] 參數(shù)驗(yàn)證失?。?, e); return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG); } /** * 不支持該請(qǐng)求方法時(shí)拋出的異常 * * @param e * @return */ @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) @ExceptionHandler({HttpRequestMethodNotSupportedException.class}) public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.warn("[handleHttpRequestMethodNotSupportedException] 不支持當(dāng)前請(qǐng)求方法: ", e); return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG); } /** * 不支持當(dāng)前媒體類型拋出的異常 * * @param e * @return */ @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) @ExceptionHandler({HttpMediaTypeNotSupportedException.class}) public Result handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) { log.warn("[handleHttpMediaTypeNotSupportedException] 不支持當(dāng)前媒體類型: ", e); return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG); } }
至于返回值,就可以理解為controller層方法的返回值,可以返回@ResponseBody,或者頁面。我這里是一個(gè)@ResponseBody的Result<>,前后端分離。
我們也可以自己根據(jù)需求,捕獲更多的異常類型。
包括我們自定義的異常類型。比如:
package com.zgd.shop.core.exception; import lombok.Data; /** * BizServiceException * 業(yè)務(wù)拋出的異常 * @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; } }
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java超詳細(xì)講解設(shè)計(jì)模式之一的單例模式
單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式2022-03-03Java實(shí)現(xiàn)Word/Pdf/TXT轉(zhuǎn)html的示例
這篇文章主要介紹了Java實(shí)現(xiàn)Word/Pdf/TXT轉(zhuǎn)html的示例,幫助大家方便的進(jìn)行文件格式轉(zhuǎn)換,完成需求,感興趣的朋友可以了解下2020-11-11springboot項(xiàng)目使用nohup將日志指定輸出文件過大問題及解決辦法
在Spring Boot項(xiàng)目中,使用nohup命令重定向日志輸出到文件可能會(huì)使日志文件過大,文章介紹了兩種解決方法:一是創(chuàng)建腳本直接清除日志文件,二是創(chuàng)建腳本保留部分日志內(nèi)容,并將這些腳本加入定時(shí)任務(wù)中,這可以有效控制日志文件的大小,避免占用過多磁盤空間2024-10-10Java如何使用流去除集合中某個(gè)字段為空的對(duì)象
這篇文章主要給大家介紹了關(guān)于Java如何使用流去除集合中某個(gè)字段為空的對(duì)象,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-08-08spring-boot-starter-web更換默認(rèn)Tomcat容器的方法
Spring Boot支持容器的自動(dòng)配置,默認(rèn)是Tomcat,當(dāng)然我們也是可以進(jìn)行修改的。下面小編給大家?guī)砹藄pring-boot-starter-web更換默認(rèn)Tomcat容器的方法,感興趣的朋友跟隨小編一起看看吧2019-04-04Java實(shí)現(xiàn)的微信公眾號(hào)獲取微信用戶信息示例
這篇文章主要介紹了Java實(shí)現(xiàn)的微信公眾號(hào)獲取微信用戶信息,結(jié)合實(shí)例形式分析了Java微信公眾號(hào)獲取微信用戶信息相關(guān)原理、步驟與操作注意事項(xiàng),需要的朋友可以參考下2019-10-10Java線程池隊(duì)列PriorityBlockingQueue和SynchronousQueue詳解
這篇文章主要為大家介紹了Java線程池隊(duì)列PriorityBlockingQueue和SynchronousQueue詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12