作者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/10984081.html
版權宣告:本文為博主原創文章,轉載請附上博文連結!
引言
本文將談論 SpringBoot 的預設錯誤處理機制,以及如何自定義錯誤響應。
版本資訊
- JDK:1.8
- SpringBoot :2.1.4.RELEASE
- maven:3.3.9
- Thymelaf:2.1.4.RELEASE
- IDEA:2019.1.1
預設錯誤響應
我們新建一個專案,先來看看 SpringBoot 的預設響應式什麼:
首先,引入 maven 依賴:
<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>
然後,寫一個請求介面:
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 ,如下效果:
瀏覽器訪問,返回一個預設頁面
其它的客戶端訪問,返回確定的json字串
以上是SpringBoot 預設的錯誤響應頁面和返回值。不過,在實際專案中,這種響應對使用者來說並不友好。通常都是開發者自定義異常頁面和返回值,使其看起來更加友好、更加舒適。
預設的錯誤處理機制
在定製錯誤頁面和錯誤響應資料之前,我們先來看看 SpringBoot 的錯誤處理機制。
ErrorMvcAutoConfiguration :
容器中有以下元件:
1、DefaultErrorAttributes
2、BasicErrorController
3、ErrorPageCustomizer
4、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);
}
上面原始碼中有兩個請求,分別是處理瀏覽器傳送的請求和其它瀏覽器傳送的請求的。是通過請求頭來區分的:
1、瀏覽器請求頭
2、其他客戶端請求頭
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();
}
}
啟動專案,可以觀察到以下結果:
定製響應的json
針對瀏覽器意外的其他客戶端錯誤響應,相似的道理,我們先進行自定義異常處理:
@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。