做者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/10984081.html
版權聲明:本文爲博主原創文章,轉載請附上博文連接!css
本文將談論 SpringBoot 的默認錯誤處理機制,以及如何自定義錯誤響應。html
咱們新建一個項目,先來看看 SpringBoot 的默認響應式什麼:java
首先,引入 maven 依賴:git
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
而後,寫一個請求接口:github
package com.yanfei1819.customizeerrordemo.web.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * Created by 追夢1819 on 2019-05-09. */ @Controller public class DefaultErrorController { @GetMapping("/defaultViewError") public void defaultViewError(){ System.out.println("默認頁面異常"); } @ResponseBody @GetMapping("/defaultDataError") public void defaultDataError(){ System.out.println("默認的客戶端異常"); } }
隨意訪問一個8080端口的地址,例如 http://localhost:8080/a ,以下效果:web
瀏覽器訪問,返回一個默認頁面spring
其它的客戶端訪問,返回肯定的json字符串json
以上是SpringBoot 默認的錯誤響應頁面和返回值。不過,在實際項目中,這種響應對用戶來講並不友好。一般都是開發者自定義異常頁面和返回值,使其看起來更加友好、更加溫馨。瀏覽器
在定製錯誤頁面和錯誤響應數據以前,咱們先來看看 SpringBoot 的錯誤處理機制。springboot
ErrorMvcAutoConfiguration :
容器中有如下組件:
一、DefaultErrorAttributes
二、BasicErrorController
三、ErrorPageCustomizer
四、DefaultErrorViewResolver
系統出現 4xx 或者 5xx 錯誤時,ErrorPageCustomizer 就會生效:
@Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath); }
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; private final DispatcherServletPath dispatcherServletPath; protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) { this.properties = properties; this.dispatcherServletPath = dispatcherServletPath; } // 註冊錯誤頁面響應規則 @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath .getRelativePath(this.properties.getError().getPath())); errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } }
上面的註冊錯誤頁面響應規則可以的到錯誤頁面的路徑(getPath):
@Value("${error.path:/error}") private String path = "/error"; //(web.xml註冊的錯誤頁面規則) public String getPath() { return this.path; }
此時會被 BasicErrorController 處理:
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { }
BasicErrorController 中有兩個請求:
// //產生html類型的數據;瀏覽器發送的請求來到這個方法處理 // MediaType.TEXT_HTML_VALUE ==> "text/html" @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); //去哪一個頁面做爲錯誤頁面;包含頁面地址和頁面內容 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } //產生json數據,其餘客戶端來到這個方法處理; @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<>(body, status); }
上面源碼中有兩個請求,分別是處理瀏覽器發送的請求和其它瀏覽器發送的請求的。是經過請求頭來區分的:
一、瀏覽器請求頭
二、其餘客戶端請求頭
resolveErrorView,獲取全部的異常視圖解析器 ;
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { //獲取全部的 ErrorViewResolver 獲得 ModelAndView for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; }
DefaultErrorViewResolver,默認錯誤視圖解析器,去哪一個頁面是由其解析獲得的;
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { // 視圖名,拼接在 error/ 後面 String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { // 使用模板引擎的狀況 return new ModelAndView(errorViewName, model); } // 未使用模板引擎的狀況 return resolveResource(errorViewName, model); }
其中 SERIES_VIEWS 是:
private static final Map<Series, String> SERIES_VIEWS; static { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); }
下面看看沒有使用模板引擎的狀況:
private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
以上代碼能夠總結爲:
模板引擎不可用
就在靜態資源文件夾下
找errorViewName對應的頁面 error/4xx.html
若是,靜態資源文件夾下存在,返回這個頁面
若是,靜態資源文件夾下不存在,返回null
按照 SpringBoot 的默認異常響應,分爲默認響應頁面和默認響應信息。咱們也分爲定製錯誤頁面和錯誤信息。
有模板引擎的狀況
SpringBoot 默認定位到模板引擎文件夾下面的 error/ 文件夾下。根據發生的狀態碼的錯誤尋找到響應的頁面。注意一點的是,頁面能夠"精確匹配"和"模糊匹配"。
精確匹配的意思是返回的狀態碼是什麼,就找到對應的頁面。例如,返回的狀態碼是 404,就匹配到 404.html.
模糊匹配,意思是可使用 4xx 和 5xx 做爲錯誤頁面的文件名來匹配這種類型的全部錯誤。不過,"精確匹配"優先。
沒有模板引擎
項目若是沒有使用模板引擎,則在靜態資源文件夾下面查找。
下面自定義異常頁面,並模擬異常發生。
在以上的示例基礎上,首先,自定義一個異常:
public class UserNotExistException extends RuntimeException { public UserNotExistException() { super("用戶不存在"); } }
而後,進行異常處理:
@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); // 傳入咱們本身的錯誤狀態碼 4xx 5xx,不然就不會進入定製錯誤頁面的解析流程 // Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); request.setAttribute("javax.servlet.error.status_code",500); map.put("code","user.notexist"); map.put("message","用戶出錯啦"); request.setAttribute("ext",map); //轉發到/error return "forward:/error"; } }
注意幾點,必定要定製自定義的狀態碼,不然沒有做用。
第三步,定製一個頁面:
<!doctype html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <title>Internal Server Error | 服務器錯誤</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> <!--省略css代碼--> </style> </head> <body> <h1>服務器錯誤</h1> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h1>status:[[${status}]]</h1> <h2>timestamp:[[${timestamp}]]</h2> <h2>exception:[[${exception}]]</h2> <h2>message:[[${message}]]</h2> <h2>ext:[[${ext.code}]]</h2> <h2>ext:[[${ext.message}]]</h2> </main> </body> </html>
最後,模擬一個異常:
@Controller public class CustomizeErrorController { @GetMapping("/customizeViewError") public void customizeViewError(){ System.out.println("自定義頁面異常"); throw new UserNotExistException(); } }
啓動項目,能夠觀察到如下結果:
針對瀏覽器意外的其餘客戶端錯誤響應,類似的道理,咱們先進行自定義異常處理:
@ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String,Object> handleException(Exception e){ Map<String,Object> map = new HashMap<>(); map.put("code","user.notexist"); map.put("message",e.getMessage()); return map; }
而後模擬異常的出現:
@ResponseBody @GetMapping("/customizeDataError") public void customizeDataError(){ System.out.println("自定義客戶端異常"); throw new UserNotExistException(); }
啓動項目,看到結果是:
異常處理同日志同樣,也屬於項目的「基礎設施」,它的存在,能夠擴大系統的容錯處理,增強系統的健壯性。在自定義的基礎上,優化了錯誤提示,對用戶更加友好。
因爲篇幅所限,以上的 SpringBoot 的內部錯誤處理機制也只屬於「走馬觀花」。後期將重點分析 SpringBoot 的工做機制。
最後,若是須要完整代碼,請移步至個人GitHub。
源碼:個人GitHub