一、SpringBoot錯誤處理機制
1.1 默認錯誤處理
瀏覽器訪問出現錯誤時,會返回一個默認的錯誤頁面。
其他客戶端訪問出現錯誤,默認響應一個json數據。
{
"timestamp":"2020-05-07T03:04:44.737+0000",
"status":404,
"error":"Not Found",
"message":"No message available",
"path":"/crud/aax"
}
如何區分是瀏覽器訪問
還是客戶端訪問
?
原因在於瀏覽器和其他客戶端的請求頭的accept屬性對html頁面的請求優先級不同。
1.2 錯誤處理原理
一但系統出現4xx或者5xx之類的錯誤,ErrorPageCustomizer組件生效,會執行/error
請求。被BasicErrorController處理,此時就會根據請求頭的不同來判定返回是返回響應頁面還是響應數據。
- 響應頁面:去哪個頁面是由DefaultErrorViewResolver解析得到的。
錯誤處理的自動配置原理可以參照ErrorMvcAutoConfiguration
類,該類添加了下列組件:
1、DefaultErrorAttributes組件
作用:幫我們在頁面共享信息、
源碼:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());//時間戳
addStatus(errorAttributes, webRequest);//狀態碼
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
...
}
2、BasicErrorController組件
作用:處理默認/error
請求
源碼:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
//產生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) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
//ErrorAttributes的實現是DefaultErrorAttributes
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
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;
}
3、ErrorPageCustomizer組件
作用:定製錯誤的相應規則
系統出現錯誤以後,來到error
請求進行處理。
源碼:
@Value("${error.path:/error}")
private String path = "/error";
4、DefaultErrorViewResolver組件
作用:
源碼:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
...
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");//所有4開頭的狀態碼
views.put(Series.SERVER_ERROR, "5xx");//所有5開頭的狀態碼
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@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) {
//默認SpringBoot可以去找到一個頁面 error/404
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析這個頁面地址就用模板引擎
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情況下,返回到errorView指定的視圖地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,調用resolveResource方法
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//該方法會在靜態資源文件夾下找errorViewName對應的頁面 error/404.html
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;
}
...
}
二、定製錯誤頁面(實現)
2.1 服務端:定製錯誤的頁面
第一種情況:使用了模板引擎
使用了模板引擎的情況下,會去找error/狀態碼
。我們只需要將錯誤頁面命名爲錯誤狀態碼.html
,放在模板引擎文件夾下的error文件夾下,發生此狀態碼的錯誤就會來到對應的頁面。
可以使用4xx和5xx作爲錯誤頁面的文件名來匹配這種類型的所有錯誤,原則是精確優先。
頁面能獲取的信息:
- timestamp:時間戳
- status:狀態碼
- error:錯誤提示
- exception:異常對象
- message:異常消息
- errors:JSR303數據校驗的錯誤都在這裏
可以通過下面的方式在頁面獲取這些信息。
<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>
第二種情況:沒有使用模板引擎
沒有模板引擎(模板引擎找不到這個錯誤頁面),會在靜態資源文件夾下找。
如果模板引擎文件夾下
和靜態資源文件夾下
都沒有錯誤頁面,就默認來到SpringBoot的錯誤提示頁面。
2.2 客戶端:定製錯誤的json數據
寫法一:自定義異常處理返回定製json數據。
- 缺點:沒有自適應效果。
/**
* 異常處理器
*/
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}
寫法二:轉發到/error
進行自適應響應效果處理
- 需要手動設置狀態碼,否則不會進入定製錯誤頁面的解析流程。
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//傳入我們自己的狀態碼 4xx 5xx
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message",e.getMessage());
//轉發到/error
return "forward:/error";
}
寫法三:將我們的定製數據攜帶出去
- 出現錯誤以後,會來到
/error
請求,會被BasicErrorController
處理,響應出去的可以獲取的數據是由getErrorAttributes
得到的。 - 而getErrorAttributes是由
AbstractErrorController(ErrorController)
規定的方法。
(1)完全來編寫一個ErrorController的實現類(或編寫AbstractErrorController的子類),放在容器中。
(2)頁面上能用的數據,或者是json返回能用的數據都是通過errorAttributes.getErrorAttributes
得到的。(第一種較爲複雜,後文採用這種寫法)
容器中DefaultErrorAttributes.getErrorAttributes()
是默認進行數據處理的。
自定義ErrorAttributes
//爲容器加入自己定義的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//獲取錯誤的屬性
//返回的map就是頁面和json能獲取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company","gql");//添加公司標識
//異常處理器攜帶的數據
Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
在異常處理器中設置map
的值。
/**
* 異常處理器
*/
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//傳入我們自己的狀態碼 4xx 5xx
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
//轉發到/error
return "forward:/error";
}
}
最終,響應是自適應的,可以通過定製ErrorAttributes改變需要返回的內容。