1.SpringBoot默認的錯誤處理機制
1.1默認效果
1.瀏覽器,會返回一個默認的錯誤頁面
2.如果是其他客戶端,默認響應一個JSON數據
1.2 原理
可以參照ErrorMvcAutoConfiguration,錯誤處理的自動配置,這個給容器中添加了以下組件。
1.DefaultErrorAttributes
//幫我們共享信息
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (this.includeException != null) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
/** @deprecated */
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
2.BasicErrorController:處理默認/error請求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController
在其方法中又通過兩種方式返回不同的數據(這就是爲啥通過瀏覽器請求返回html,通過其他客戶端返回json)
//產生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, getErrorAttributeOptions(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);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
查看瀏覽器的請求頭:
查看客戶端的請求頭:
可以看到這兩個對應BasicErrorController處理的兩種方式。
獲得所有的ErrorViewResolver,而這個ErrorViewResolver就是下面的DefaultErrorViewResolver組件,去哪個頁面就是由它來解析的的,通過DefaultErrorViewResolver組件得知
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
3.ErrorPageCustomizer
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
//查看getpath方法,看到path
@Value("${error.path:/error}")
private String path = "/error";
4.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) {
//默認的SpringBoot可以去找一個頁面: error/404
String errorViewName = "error/" + viewName;
//模板引擎可以解析這個頁面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情況下返回errorViewName指定的視圖地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/404.html
return resolveResource(errorViewName, model);
}
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;
}
步驟:一旦系統出現4xx或者5xx之類的錯誤,ErrorPageCustomizer就會生效(定製錯誤的響應規則),就會來到/error請求,這個請求就會被BasicErrorController處理。
響應頁面:去哪個頁面是由DefaultErrorViewResolver解析得到的。
2.如何定製錯誤響應
2.1 如何定製錯誤頁面(瀏覽器)
1.在由模板引擎的情況下:error/狀態碼。將錯誤頁面命名爲 錯誤狀態碼.html放在模板引擎文件夾裏面的error文件夾下,發生此狀態碼的錯誤就會來到對應的頁面。
我們可以使用4xx和5xx作爲錯誤頁面的文件名來匹配這種類型的所有錯誤,精確優先(優先尋找精確的狀態碼.html)。如下所示:
頁面能獲取的信息(這個信息可以從上面DefaultErrorAttributes源碼中看到):
timestamp:時間戳
status:狀態碼
error:錯誤提示
exception:異常對象
message:異常消息
errors:JSR303數據校驗的錯誤都在這裏
可以看到html頁面取屬性:
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h1>status:[[${status}]]</h1>
<h1>timestamp:[[${timestamp}]]</h1>
<h1>message:[[${message}]]</h1>
<h1>exception:[[${exception}]]</h1>
<!--配置文件配置-->
</main>
2.沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資源文件夾下找。
3.以上都沒有錯誤頁面,就是默認來到SpringBoot的默認錯誤提示頁面。
2.2 如何定製錯誤的JSON數據
自己先編寫一個異常:
public class UserNotExistException extends RuntimeException{
public UserNotExistException() {
super("用戶不存在");
}
}
1.自定義異常處理和返回JSON數據,這個沒有自適應的效果,也就是沒有上面說的時間戳等信息
@ControllerAdvice
public class MyExceptionHandler {
//第一種寫法,瀏覽器和客戶端返回都是JSON數據
@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;
}
}
這個原理的就是:我們在看BasicErrorController這個源碼的時候(用來處理/error請求),可以看到添加這個組件的時候,就可以看到這個上面有這個註解:
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
意思是沒有這個ErrorController類的話,這個組件才生效。現在我們自定義了MyExceptionHandler用來處理UserNotExistException異常。
2.轉發到/error請求
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
//傳入我們自己的錯誤狀態碼 4xx 5xx,否則就不會進入定製錯誤頁面的解析流程
/**
* Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
*/
Map<String,Object> map=new HashMap<>();
request.setAttribute("javax.servlet.error.status_code",400);
map.put("code","user.notexist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
//轉發到/error請求
return "forward:/error";
}
3.將定製的數據攜帶出去
出現錯誤以後,會來到/error請求,會被BasicErrorController處理,響應出去可以獲取的數據是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)規定的方法)。
1.完全來編寫ErrorController的實現類(或者是編寫AbstractErrorController的子類),放在容器中。
2.頁面上用的數據,或者是JSON返回能用的數據都是通過errorAttributes.getErrorAttributes得到。容器中DefaultErrorAttributes.getErrorAttributes();默認進行數據處理的。
自定義ErrorAttributes:
@Component
//給容器中加入我們自定義ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回的map就是頁面和JSON獲取的所有字段
//WebRequest繼承了RequestAttributes
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String,Object> map=super.getErrorAttributes(webRequest, options);
//我們異常處理器攜帶的數據
Map<String,Object> ext= (Map<String, Object>) webRequest.getAttribute("ext",0);
map.put("ext",ext);
Throwable error=getError(webRequest);
if(error!=null)
{
map.put("exception",error.getClass().getName());
}
return map;
}
}
最終的效果:響應是自適應的,可以通過定製ErrorAttributes改變需要返回的內容: