1. 程式人生 > >SpringBoot中的全域性異常處理

SpringBoot中的全域性異常處理

[toc] ## 本篇要點 - 介紹SpringBoot預設的異常處理機制。 - 如何定義錯誤頁面。 - 如何自定義異常資料。 - 如何自定義檢視解析。 - 介紹@ControllerAdvice註解處理異常。 ## 一、SpringBoot預設的異常處理機制 預設情況下,SpringBoot為以下兩種情況提供了不同的響應方式: 1. Browser Clients瀏覽器客戶端:通常情況下請求頭中的Accept會包含text/html,如果未定義/error的請求處理,就會出現如下html頁面:Whitelabel Error Page,關於error頁面的定製,接下來會詳細介紹。 ![](https://img2020.cnblogs.com/blog/1771072/202010/1771072-20201031164325800-1870436828.png) 2. Machine Clients機器客戶端:Ajax請求,返回ResponseEntity實體json字串資訊。 ```json { "timestamp": "2020-10-30T15:01:17.353+00:00", "status": 500, "error": "Internal Server Error", "trace": "java.lang.ArithmeticException: / by zero...", "message": "/ by zero", "path": "/" } ``` SpringBoot預設提供了程式出錯的結果對映路徑/error,這個請求的處理邏輯在BasicErrorController中處理,處理邏輯如下: ```java // 判斷mediaType型別是否為text/html @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 建立ModelAndView物件,返回頁面 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } Map body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } ``` ## 二、錯誤頁面的定製 相信Whitelabel Error Pag頁面我們經常會遇到,這樣體驗不是很好,在SpringBoot中可以嘗試定製錯誤頁面,定製方式主要分靜態和動態兩種: 1. **靜態異常頁面** 在`classpath:/public/error`或`classpath:/static/error`路徑下定義相關頁面:檔名應為**確切的狀態程式碼**,如404.html,或**系列掩碼**,如4xx.html。 舉個例子,如果你想匹配404或5開頭的所有狀態程式碼到靜態的html檔案,你的檔案目錄應該是下面這個樣子: ```java src/ +- main/ +- java/ | + +- resources/ +- public/ +- error/ | +- 404.html | +- 5xx.html +- ``` 如果500.html和5xx.html同時生效,那麼優先展示500.html頁面。 2. **使用模板的動態頁面** 放置在`classpath:/templates/error`路徑下:這裡使用thymeleaf模板舉例: ```java src/ +- main/ +- java/ | + +- resources/ +- templates/ +- error/ | +- 5xx.html +- ``` 頁面如下: ```html
5xx

5xx

``` > 如果靜態頁面和動態頁面同時存在且都能匹配,SpringBoot對於錯誤頁面的優先展示規則如下: > > 如果發生了500錯誤: > > 動態500 -> 靜態500 -> 動態5xx -> 靜態5xx ## 三、自定義異常資料 預設的資料主要是以下幾個,這些資料定義在`org.springframework.boot.web.servlet.error.DefaultErrorAttributes`中,資料的定義在`getErrorAttributes`方法中。 ![](https://img2020.cnblogs.com/blog/1771072/202010/1771072-20201031164342377-422763374.png) DefaultErrorAttributes類在`org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration`自動配置類中定義: ```java @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } ``` **如果我們沒有提供ErrorAttributes的例項,SpringBoot預設提供一個DefaultErrorAttributes例項。** 因此,我們就該知道如何去自定義異常資料屬性: 1. 實現ErrorAttributes介面。 2. 繼承DefaultErrorAttributes,本身已經定義對異常資料的處理,繼承更具效率。 定義方式如下: ```java /** * 自定義異常資料 * @author Summerday */ @Slf4j @Component public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map map = super.getErrorAttributes(webRequest, options); if(map.get("status").equals(500)){ log.warn("伺服器內部異常"); } return map; } } ``` ## 四、自定義異常檢視 自定義檢視的載入邏輯存在於BasicErrorController類的errorHtml方法中,用於返回一個ModelAndView物件,這個方法中,首先通過getErrorAttributes獲取到異常資料,然後呼叫resolveErrorView去建立一個ModelAndView物件,只有建立失敗的時候,使用者才會看到預設的錯誤提示頁面。 ```java @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map model = Collections .unmodifiableMap( //ErrorAttributes # getErrorAttributes getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // E ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } ``` DefaultErrorViewResolver類的resolveErrorView方法: ```java @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) { // 以異常狀態碼作為檢視名去locations路徑中去查詢頁面 ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); // 如果找不到,以4xx,5xx series去查詢 if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } ``` 那麼如何自定義呢?和自定義異常資料相同,如果我們定義了一個ErrorViewResolver的例項,預設的配置就會失效。 ```java /** * 自定義異常檢視解析 * @author Summerday */ @Component public class MyErrorViewResolver extends DefaultErrorViewResolver { public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) { super(applicationContext, resourceProperties); } @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) { return new ModelAndView("/hyh/resolve",model); } } ``` 此時,SpringBoot將會去/hyh目錄下尋找resolve.html頁面。 ## 五、@ControllerAdvice註解處理異常 前後端分離的年代,後端往往需要向前端返回統一格式的json資訊,以下為封裝的AjaxResult物件: ```java public class AjaxResult extends HashMap { //狀態碼 public static final String CODE_TAG = "code"; //返回內容 public static final String MSG_TAG = "msg"; //資料物件 public static final String DATA_TAG = "data"; private static final long serialVersionUID = 1L; public AjaxResult() { } public AjaxResult(int code, String msg) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); } public AjaxResult(int code, String msg, Object data) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); if (data != null) { super.put(DATA_TAG, data); } } public static AjaxResult ok() { return AjaxResult.ok("操作成功"); } public static AjaxResult ok(Object data) { return AjaxResult.ok("操作成功", data); } public static AjaxResult ok(String msg) { return AjaxResult.ok(msg, null); } public static AjaxResult ok(String msg, Object data) { return new AjaxResult(HttpStatus.OK.value(), msg, data); } public static AjaxResult error() { return AjaxResult.error("操作失敗"); } public static AjaxResult error(String msg) { return AjaxResult.error(msg, null); } public static AjaxResult error(String msg, Object data) { return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data); } public static AjaxResult error(int code, String msg) { return new AjaxResult(code, msg, null); } } ``` 根據業務的需求不同,我們往往也需要自定義異常類,便於維護: ```java /** * 自定義異常 * * @author Summerday */ public class CustomException extends RuntimeException { private static final long serialVersionUID = 1L; private Integer code; private final String message; public CustomException(String message) { this.message = message; } public CustomException(String message, Integer code) { this.message = message; this.code = code; } public CustomException(String message, Throwable e) { super(message, e); this.message = message; } @Override public String getMessage() { return message; } public Integer getCode() { return code; } } ``` @ControllerAdvice和@RestControllerAdvice這倆註解的功能之一,就是做到Controller層面的異常處理,而兩者的區別,與@Controller和@RestController差不多。 @ExceptionHandler指定需要處理的異常類,針對自定義異常,如果是ajax請求,返回json資訊,如果是普通web請求,返回ModelAndView物件。 ```java /** * 全域性異常處理器 * @author Summerday */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(CustomException.class) public Object handle(HttpServletRequest request, CustomException e) { AjaxResult info = AjaxResult.error(e.getMessage()); log.error(e.getMessage()); // 判斷是否為ajax請求 if (isAjaxRequest(request)) { return info; } ModelAndView mv = new ModelAndView(); mv.setViewName("custom"); // templates/custom.html mv.addAllObjects(info); mv.addObject("url", request.getRequestURL()); return mv; } private boolean isAjaxRequest(HttpServletRequest request) { return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); } } ``` 在Controller層,人為定義丟擲異常: ```java @RestController public class TestController { @GetMapping("/ajax") public AjaxResult ajax() { double alpha = 0.9; if (Math.random() < alpha) { throw new CustomException("自定義異常!"); } return AjaxResult.ok(); } } ``` 最後,通過`/templates/custom.html`定義的動態模板頁面展示資料: ```html
自定義介面

``` ## 原始碼下載 本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn ## 參考閱讀 - [Spring Boot乾貨系列:(十三)Spring Boot全域性異常處理整理](http://tengj.top/2018/05/16/springboot13/#%E5%89%8D%E8%A8%80) - [Springboot:Error-handling](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-error-handling) - [江南一點雨: Spring Boot 中關於自定義異常處理的套路!](http://www.javaboy.org/2019/0417/springboot-excepti