Spring Boot 全域性異常處理(上)

背景

對接的專案多了,奇奇怪怪的問題就都出現了,比如有一個最讓人煩心的問題

異常

偶爾會碰到框架丟擲的預設的異常,比如 Laraval,比如 Spring Boot,每個框架丟擲的異常格式是不一致的,有 Json 或 XML 格式的資料,更甚至有 HTML 頁面,最為關鍵的是響應的資料結構和介面約定的資料結構不一致,所以這時候我們在對響應內容進行解析的時候反而會給我們自己的程式碼帶來需要處理的異常。

基於此,為了對自己的介面負責,我們需要進行全域性的異常處理,目的是防止出現約定之外的資料結構。

Spring Boot 預設的異常處理機制

預設情況下,Spring Boot 會返回兩種型別的異常,一種是

HTML

,還有一種是 Json 格式的資料,這主要取決於請求頭中的

Accept

引數,比如瀏覽器發出的請求,請求頭中會附帶

Accept:text/html

,所以此時 Spring Boot 會返回一個錯誤頁面,稱為

Whitelabel Error Page

,而當我們使用 Postman 請求時,返回的則是 Json 型別的資料。

原理其實也很簡單,Spring Boot 預設提供了程式出錯的結果對映路徑

/error

。而這個

/error

請求會由

BasicErrorController

來處理,其內部其實就是透過判斷請求頭的 Accept 中的內容來進行區分處理邏輯的(判斷是否包含 text/html),從而來決定返回頁面檢視還是 JSON 訊息內容。

相關 BasicErrorController 中程式碼如下:

Spring Boot 全域性異常處理(上)

BasicErrorController 處理邏輯

自定義錯誤頁面

自定義錯誤頁面的好處有好多,比如 404 錯誤頁面,我們完全可以自定義 404 的 HTML 頁面,上面可以放置圖片等,這樣體驗就更友好一點。

自定義的錯誤頁面有兩種,一種是

靜態頁面

,一種是使用

模板引擎

動態生成,後者的優勢是可以在頁面上顯示自定義的內容。

靜態頁面的方式,html 檔案的路徑為:

resources/public/error/xxx.html

如果要替換 404 錯誤頁面,則在此路徑下放置 404。html 檔案,同理,如果要替換 500 錯誤頁面,則在此路徑下放置 500。html 檔案即可

模板引擎渲染的動態頁面的方式,html 檔案的路徑為:

resources/templates/error/xxx.html

檔案命名同上

注意:動態頁面的優先順序是要高於靜態頁面的

比如你同時配置了靜態頁面和動態頁面,那麼最終生效的,會是動態頁面。

附上檔案結構圖:

Spring Boot 全域性異常處理(上)

檔案結構

自定義錯誤資訊

上面介紹了最簡單的錯誤處理,最主要的針對返回的 HTML,但是我們往往也要處理 Json 型別的返回內容,目的是讓資料結構和我們的介面返回的資料結構一致。

Step 1 自定義 servlet 容器

需要注意的是,Spring Boot 2.x 和 Sprig Boot 1.x 是不一樣的。

此處 Demo 我們僅處理了 404 和 500 這兩種異常。

@Configurationpublic class ContainerConfig { /** 下面是 springboot 2。x 系列的寫法 */ @Bean public WebServerFactoryCustomizer webServerFactoryCustomizer(){ return factory -> { factory。addErrorPages(new ErrorPage(HttpStatus。INTERNAL_SERVER_ERROR, “/error/500”)); factory。addErrorPages(new ErrorPage(HttpStatus。NOT_FOUND, “/error/404”)); }; } /** 下面是 springboot 1。x 系列的寫法 */ /*@Bean public EmbeddedServletContainerCustomizer containerCustomizer(){ return new EmbeddedServletContainerCustomizer(){ @Override public void customize(ConfigurableEmbeddedServletContainer container) { container。addErrorPages(new ErrorPage(HttpStatus。INTERNAL_SERVER_ERROR, “/error/500”)); container。addErrorPages(new ErrorPage(HttpStatus。NOT_FOUND, “/error/404”)); } }; }*/}

Step 2 自定義對應的請求處理類

@Controllerpublic class MyBasicErrorController extends BasicErrorController { public MyBasicErrorController() { super(new DefaultErrorAttributes(), new ErrorProperties()); } /** * @Description: 定義500的ModelAndView * @Param: [request, response] * @return: org。springframework。web。servlet。ModelAndView * @Author: Jet。Chen * @Date: 2019-07-17 21:56 */ @RequestMapping(produces = “text/html”,value = “/500”) public ModelAndView errorHtml500(HttpServletRequest request, HttpServletResponse response) { response。setStatus(getStatus(request)。value()); Map model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType。TEXT_HTML)); model。put(“msg”,“自定義錯誤資訊”); return new ModelAndView(“error/500”, model); } /** * @Description: 定義500 和 404 的錯誤JSON資訊 * @Param: [request] * @return: org。springframework。http。ResponseEntity * STCRResposeData 為全域性統一的介面資料結構 * @Author: Jet。Chen * @Date: 2019-07-17 23:13 */ @RequestMapping(value = {“/500”, “/404”}) @ResponseBody public ResponseEntity error500(HttpServletRequest request) { Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType。TEXT_HTML)); HttpStatus status = getStatus(request); Object messageTemp; // stcrResposeData 為返回的資料 STCRResposeData stcrResposeData = STCRResposeData。initError( String。format(“%d%d”, 1, status。value()), (messageTemp = body。get(“error”)) == null ? null : messageTemp。toString(), new HashMap() {{ put(“error”, body。get(“error”)); put(“message”, body。get(“message”)); }}); return new ResponseEntity<>(stcrResposeData, status); } /** * @Description: 定義404的ModelAndView * @Param: [request, response] * @return: org。springframework。web。servlet。ModelAndView * @Author: Jet。Chen * @Date: 2019-07-17 23:13 */ @RequestMapping(produces = “text/html”,value = “/404”) public ModelAndView errorHtml400(HttpServletRequest request, HttpServletResponse response) { response。setStatus(getStatus(request)。value()); Map model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType。TEXT_HTML)); model。put(“msg”,“自定義錯誤資訊”); return new ModelAndView(“error/404”, model); }}

小結

全域性異常的處理是非常有必要的,但是此文到此的處理方式其實才完成了一半,這些會在下篇文章中介紹,主要是因為此處處理的是全域性的錯誤,是在過濾器之外的,但是我們希望處理的粒度更細一點。

比如在控制器層的全域性處理方式:

@ControllerAdvice

,從抽象概念上可以理解成它是處理那些在 Controller 方法中丟擲的異常。