詳解SpringBoot中的統(tǒng)一異常處理
一、概述
1、統(tǒng)一異常處理介紹
Spring
在3.2版本增加了一個注解@ControllerAdvice
,可以與@ExceptionHandler
、@InitBinder
、@ModelAttribute
等注解注解配套使用。不過跟異常處理相關的只有注解@ExceptionHandler
,從字面上看,就是 異常處理器 的意思
2、原理和目標
簡單的說,該注解可以把異常處理器應用到所有控制器,而不是單個控制器。借助該注解,我們可以實現(xiàn):在獨立的某個地方,比如單獨一個類,定義一套對各種異常的處理機制,然后在類的簽名加上注解@ControllerAdvice
,統(tǒng)一對 不同階段的
、不同異常
進行處理。這就是統(tǒng)一異常處理的原理。
對異常按階段進行分類,大體可以分成:進入Controller
前的異常 和 Service
層異常
目標就是消滅95%以上的 try catch
代碼塊,并以優(yōu)雅的 Assert
(斷言) 方式來校驗業(yè)務的異常情況,只關注業(yè)務邏輯,而不用花費大量精力寫冗余的 try catch
代碼塊。
二、Assert(斷言)
1、概述
**Assert(斷言)**是Spring 家族的 org.springframework.util.Assert
,在我們寫測試用例的時候經(jīng)常會用到,使用斷言能讓我們編碼的時候有一種非一般絲滑的感覺
Assert
的部分源碼,可以看到,Assert 其實就是幫我們把 if {…} 封裝了一下,拋出的異常是IllegalArgumentException()
public abstract class Assert { public Assert() { } public static void notNull(@Nullable Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } } }
2、Assert自定義實戰(zhàn)
2.1 自定義接口Assert
Assert斷言方法是使用接口的默認方法定義的,然后有沒有發(fā)現(xiàn)當斷言失敗后,拋出的異常不是具體的某個異常,而是交由2個newException接口方法提供。因為業(yè)務邏輯中出現(xiàn)的異?;径际菍囟ǖ膱鼍?,比如根據(jù)用戶id獲取用戶信息,查詢結果為null,此時拋出的異??赡転?code>UserNotFoundException,并且有特定的異常碼(比如7001)和異常信息"用戶不存在"。所以具體拋出什么異常,有Assert的實現(xiàn)類決定。
public interface Assert { /** * 創(chuàng)建異常 * @param args * @return */ BaseException newException(Object... args); /** * 創(chuàng)建異常 * @param t * @param args * @return */ BaseException newException(Throwable t, Object... args); /** * <p>斷言對象<code>obj</code>非空。如果對象<code>obj</code>為空,則拋出異常 * * @param obj 待判斷對象 */ default void assertNotNull(Object obj) { if (obj == null) { throw newException(obj); } } /** * <p>斷言對象<code>obj</code>非空。如果對象<code>obj</code>為空,則拋出異常 * <p>異常信息<code>message</code>支持傳遞參數(shù)方式,避免在判斷之前進行字符串拼接操作 * * @param obj 待判斷對象 * @param args message占位符對應的參數(shù)列表 */ default void assertNotNull(Object obj, Object... args) { if (obj == null) { throw newException(args); } } }
2.2 自定義異常
public interface IResponseEnum { int getCode(); String getMessage(); } /** * <p>業(yè)務異常</p> * <p>業(yè)務處理時,出現(xiàn)異常,可以拋出該異常</p> */ public class BusinessException extends BaseException { private static final long serialVersionUID = 1L; public BusinessException(IResponseEnum responseEnum, Object[] args, String message) { super(responseEnum, args, message); } public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) { super(responseEnum, args, message, cause); } }
2.3 Enum整合
代碼示例中定義了兩個枚舉實例:BAD_LICENCE_TYPE
、LICENCE_NOT_FOUND
,分別對應了BadLicenceTypeException
、LicenceNotFoundException
兩種異常。以后每增加一種異常情況,只需增加一個枚舉實例即可,再也不用每一種異常都定義一個異常類了
public interface BusinessExceptionAssert extends IResponseEnum, Assert { @Override default BaseException newException(Object... args) { String msg = MessageFormat.format(this.getMessage(), args); return new BusinessException(this, args, msg); } @Override default BaseException newException(Throwable t, Object... args) { String msg = MessageFormat.format(this.getMessage(), args); return new BusinessException(this, args, msg, t); } } @Getter @AllArgsConstructor public enum ResponseEnum implements BusinessExceptionAssert { /** * Bad licence type */ BAD_LICENCE_TYPE(7001, "Bad licence type."), /** * Licence not found */ LICENCE_NOT_FOUND(7002, "Licence not found.") ; /** * 返回碼 */ private int code; /** * 返回消息 */ private String message; }
2.4 實戰(zhàn)檢測
使用枚舉類結合(繼承)Assert,只需根據(jù)特定的異常情況定義不同的枚舉實例,如上面的BAD_LICENCE_TYPE
、LICENCE_NOT_FOUND
,就能夠針對不同情況拋出特定的異常(這里指攜帶特定的異常碼和異常消息),這樣既不用定義大量的異常類,同時還具備了斷言的良好可讀性
private void checkNotNull(Licence licence) { ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence,"測試"); } // 替代下面的方法 private void checkNotNull(Licence licence) { if (licence == null) { throw new LicenceNotFoundException(); // 或者這樣 throw new BusinessException(7001, "Bad licence type."); } }
三、統(tǒng)一異常處理器
1、異常處理器說明
1.1 handleServletException
一個http請求,在到達Controller
前,會對該請求的請求信息與目標控制器信息做一系列校驗
NoHandlerFoundException
:首先根據(jù)請求Url查找有沒有對應的控制器,若沒有則會拋該異常,也就是大家非常熟悉的404異常,但是實際上當出現(xiàn)404的時候,默認是不拋異常的,而是 forward跳轉(zhuǎn)到/error控制器,spring也提供了默認的error控制器,如果要拋出異常,需要配置
spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false
HttpRequestMethodNotSupportedException
:若匹配到了(匹配結果是一個列表,不同的是http方法不同,如:Get、Post等),則嘗試將請求的http方法與列表的控制器做匹配,若沒有對應http方法的控制器,則拋該異常;HttpMediaTypeNotSupportedException
:然后再對請求頭與控制器支持的做比較,比如content-type
請求頭,若控制器的參數(shù)簽名包含注解@RequestBody
,但是請求的content-type
請求頭的值沒有包含application/json
,那么會拋該異常(當然,不止這種情況會拋這個異常);MissingPathVariableException
:未檢測到路徑參數(shù)。比如url為:/licence/{licenceId}
,參數(shù)簽名包含@PathVariable("licenceId")
,當請求的url為/licence
,在沒有明確定義url為/licence
的情況下,會被判定為:缺少路徑參數(shù);MissingServletRequestParameterException
:缺少請求參數(shù)。比如定義了參數(shù)@RequestParam("licenceId") String licenceId
,但發(fā)起請求時,未攜帶該參數(shù),則會拋該異常;TypeMismatchException
: 參數(shù)類型匹配失敗。比如:接收參數(shù)為Long型,但傳入的值確是一個字符串,那么將會出現(xiàn)類型轉(zhuǎn)換失敗的情況,這時會拋該異常;HttpMessageNotReadableException
:與上面的HttpMediaTypeNotSupportedException
舉的例子完全相反,即請求頭攜帶了"content-type: application/json;charset=UTF-8"
,但接收參數(shù)卻沒有添加注解@RequestBody
,或者請求體攜帶的 json 串反序列化成 pojo 的過程中失敗了,也會拋該異常;HttpMessageNotWritableException
:返回的 pojo 在序列化成 json 過程失敗了,那么拋該異常;
1.2 handleBindException和handleValidException
參數(shù)校驗異常
1.3 handleBusinessException、handleBaseException
處理自定義的業(yè)務異常,只是handleBaseException
處理的是除了 BusinessException
意外的所有業(yè)務異常。就目前來看,這2個是可以合并成一個的
1.4 handleException
處理所有未知的異常,比如操作數(shù)據(jù)庫失敗的異常。
注:上面的
handleServletException
、handleException
這兩個處理器,返回的異常信息,不同環(huán)境返回的可能不一樣,以為這些異常信息都是框架自帶的異常信息,一般都是英文的,不太好直接展示給用戶看,所以統(tǒng)一返回SERVER_ERROR
代表的異常信息
2、自定義統(tǒng)一異常處理器類
將異常分成幾類,實際上只有兩大類,一類是ServletException
、ServiceException
,還記得上文提到的 按階段分類 嗎,即對應 進入Controller
前的異常 和 Service
層異常;然后 ServiceException
再分成自定義異常、未知異常。對應關系如下:
- 進入
Controller
前的異常: handleServletException、handleBindException、handleValidException - 自定義異常:handleBusinessException、handleBaseException
- 未知異常: handleException
@Slf4j @Component @ControllerAdvice @ConditionalOnWebApplication @ConditionalOnMissingBean(UnifiedExceptionHandler.class) public class UnifiedExceptionHandler { /** * 生產(chǎn)環(huán)境 */ private final static String ENV_PROD = "prod"; @Autowired private UnifiedMessageSource unifiedMessageSource; /** * 當前環(huán)境 */ @Value("${spring.profiles.active}") private String profile; /** * 獲取國際化消息 * 這里可以做處理 * @param e 異常 * @return */ public String getMessage(BaseException e) { String code = "response." + e.getResponseEnum().toString(); String message = unifiedMessageSource.getMessage(code, e.getArgs()); if (message == null || message.isEmpty()) { return e.getMessage(); } return message; } /** * 業(yè)務異常 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler(value = BusinessException.class) @ResponseBody public ErrorResponse handleBusinessException(BaseException e) { log.error(e.getMessage(), e); return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e)); } /** * 自定義異常 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler(value = BaseException.class) @ResponseBody public ErrorResponse handleBaseException(BaseException e) { log.error(e.getMessage(), e); return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e)); } /** * Controller上一層相關異常 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler({ NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, // BindException.class, // MethodArgumentNotValidException.class HttpMediaTypeNotAcceptableException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, MissingServletRequestPartException.class, AsyncRequestTimeoutException.class }) @ResponseBody public ErrorResponse handleServletException(Exception e) { log.error(e.getMessage(), e); int code = CommonResponseEnum.SERVER_ERROR.getCode(); try { ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName()); code = servletExceptionEnum.getCode(); } catch (IllegalArgumentException e1) { log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName()); } if (ENV_PROD.equals(profile)) { // 當為生產(chǎn)環(huán)境, 不適合把具體的異常信息展示給用戶, 比如404. code = CommonResponseEnum.SERVER_ERROR.getCode(); BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR); String message = getMessage(baseException); return new ErrorResponse(code, message); } return new ErrorResponse(code, e.getMessage()); } /** * 參數(shù)綁定異常 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler(value = BindException.class) @ResponseBody public ErrorResponse handleBindException(BindException e) { log.error("參數(shù)綁定校驗異常", e); return wrapperBindingResult(e.getBindingResult()); } /** * 參數(shù)校驗異常,將校驗失敗的所有異常組合成一條錯誤信息 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public ErrorResponse handleValidException(MethodArgumentNotValidException e) { log.error("參數(shù)綁定校驗異常", e); return wrapperBindingResult(e.getBindingResult()); } /** * 包裝綁定異常結果 * * @param bindingResult 綁定結果 * @return 異常結果 */ private ErrorResponse wrapperBindingResult(BindingResult bindingResult) { StringBuilder msg = new StringBuilder(); for (ObjectError error : bindingResult.getAllErrors()) { msg.append(", "); if (error instanceof FieldError) { msg.append(((FieldError) error).getField()).append(": "); } msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage()); } return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2)); } /** * 未定義異常 * * @param e 異常 * @return 異常結果 */ @ExceptionHandler(value = Exception.class) @ResponseBody public ErrorResponse handleException(Exception e) { log.error(e.getMessage(), e); if (ENV_PROD.equals(profile)) { // 當為生產(chǎn)環(huán)境, 不適合把具體的異常信息展示給用戶, 比如數(shù)據(jù)庫異常信息. int code = CommonResponseEnum.SERVER_ERROR.getCode(); BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR); String message = getMessage(baseException); return new ErrorResponse(code, message); } return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage()); } }
3、其他類型統(tǒng)一處理器
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * 沒有登錄 * @param request * @param response * @param e * @return */ @ExceptionHandler(NoLoginException.class) public Object noLoginExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][noLoginExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.NO_LOGIN); jsonResult.setMessage("用戶登錄失效或者登錄超時,請先登錄"); return jsonResult; } /** * 業(yè)務異常 * @param request * @param response * @param e * @return */ @ExceptionHandler(ServiceException.class) public Object businessExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][businessExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("業(yè)務異常,請聯(lián)系管理員"); return jsonResult; } /** * 全局異常處理 * @param request * @param response * @param e * @return */ @ExceptionHandler(Exception.class) public Object exceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e) { log.error("[GlobalExceptionHandler][exceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("系統(tǒng)錯誤,請聯(lián)系管理員"); return jsonResult; } }
4、統(tǒng)一返回結果
code
、message
是所有返回結果中必有的字段,而當需要返回數(shù)據(jù)時,則需要另一個字段 data
來表示。所以首先定義一個 BaseResponse
來作為所有返回結果的基類
然后定義一個通用返回結果類CommonResponse
,繼承 BaseResponse
,而且多了字段 data
;為了區(qū)分成功和失敗返回結果,于是再定義一個 ErrorResponse
最后還有一種常見的返回結果,即返回的數(shù)據(jù)帶有分頁信息,因為這種接口比較常見,所以有必要單獨定義一個返回結果類 QueryDataResponse
,該類繼承自 CommonResponse
,只是把 data
字段的類型限制為 QueryDdata
,QueryDdata
中定義了分頁信息相應的字段,即totalCount
、pageNo
、 pageSize
、records
。其中比較常用的只有 CommonResponse
和 QueryDataResponse
,但是名字又賊鬼死長,何不定義2個名字超簡單的類來替代呢?于是 R
和 QR
誕生了,以后返回結果的時候只需這樣寫:new R<>(data)
、new QR<>(queryData)
因為這一套統(tǒng)一異常處理可以說是通用的,所有可以設計成一個
common
包,以后每一個新項目/模塊只需引入該包即可
到此這篇關于詳解SpringBoot中的統(tǒng)一異常處理的文章就介紹到這了,更多相關SpringBoot統(tǒng)一異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java實現(xiàn)轉(zhuǎn)跳不同系統(tǒng)使用枚舉加switch的方式示例
今天小編就為大家分享一篇關于Java實現(xiàn)轉(zhuǎn)跳不同系統(tǒng)使用枚舉加switch的方式示例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12Java?Cookie與Session實現(xiàn)會話跟蹤詳解
session的工作原理和cookie非常類似,在cookie中存放一個sessionID,真實的數(shù)據(jù)存放在服務器端,客戶端每次發(fā)送請求的時候帶上sessionID,服務端根據(jù)sessionID進行數(shù)據(jù)的響應2022-11-11