SpringBoot——web開發之錯誤處理機制
一、SpringBoot提供的預設錯誤處理
1、在瀏覽器端訪問時,出現錯誤時響應一個錯誤頁面:
2、在其他客戶端訪問時,響應json資料:
3、錯誤處理機制的原理,參照錯誤自動配置類——ErrorMvcAutoConfiguration,在錯誤自動配置類中,配置了以下元件:
①ErrorPageCustomizer:定製錯誤的響應規則
@Value("${error.path:/error}")
private String path = "/error";
從主配置檔案中獲取error.path的值,如果沒有則預設"/error",即一旦系統出現4xx或5xx之類的錯誤時就會發送/error請求,或者我們自定義的error.path的值的請求,該請求會有BasicErrorController類處理
②BasicErrorController:處理預設的/error請求,但是該類提供了兩種/error請求的處理方式,一種產生"text/html"響應(瀏覽器端請求),一種產生JSON格式資料
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { ... @RequestMapping(produces = "text/html")//處理瀏覽器端傳送的請求 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); } @RequestMapping @ResponseBody//處理其他客戶端傳送的請求 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); } ... }
那SpringBoot怎麼區分請求是來自瀏覽器還是其他客戶端呢?是根據請求頭來判斷的,來自瀏覽器的請求的請求頭Accept攜帶有瀏覽器請求的資訊:
其他客戶端中Accept請求頭的資訊:沒有要求優先接收html資料
這兩個方法會根據錯誤的狀態碼判斷去哪個錯誤頁面或者響應什麼JSON資料,響應頁面的解析:遍歷所有的錯誤檢視解析器(ErrorViewResolver),如果得到異常檢視則返回,否則返回null,這裡的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;
}
③DefaultErrorViewResolver:4xx為客戶端錯誤,5xx為服務端錯誤
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
...
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);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), 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) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
...
}
從這段程式碼中可以看出:在解析錯誤檢視的時候會呼叫resolve方法,將狀態碼拼接到error/後面作為檢視名稱返回,也就是說SpringBoot預設會在error資料夾下找狀態碼對應的錯誤頁面,同時如果模板引擎可用則返回到模板引擎指定的資料夾下的errorViewName檢視,否則會執行下面這段程式碼:從所有的靜態資原始檔夾下找對應的檢視(.html),找到則返回,找不到返回null
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;
}
④DefaultErrorAttributes:幫我們在頁面共享資訊,檢視中會有哪些資料是在該類中封裝的
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;
}
出錯時,SpringBoot為我們放入的資料有:這些資料我們都可以在頁面中獲取到
timestamp(時間戳)、status(狀態碼)、error(錯誤資訊)
<h1>status:[[${status}]]</h1>
<h1>timestamp:[[${timestamp}]]</h1>
整體步驟:系統出現4xx或者5xx之類的錯誤時,ErrorPageCustomizer就會生效從而定製錯誤的響應規則,就會來到/error
請求,該請求會被BasicErrorController處理,處理的結果由DefaultErrorViewResolver返回
二、如何定製錯誤處理
1、如何定製錯誤的頁面
①有模板引擎時:在模板引擎靜態資原始檔夾(templates)下新建一個error資料夾,並提供與錯誤碼對應的錯誤頁面即可(error/狀態碼.html)
也可以提供一個4xx.html來響應以4開頭的狀態碼頁面,此時若有精確匹配的則優先使用精確匹配的:
②沒有模板引擎:可以放在任何一個靜態資原始檔夾下,因為檢視解析時會一個個的解析,解析到則返回,解析不到則返回null,注意必須是靜態資原始檔夾下的error/狀態碼.html,若所有的靜態資原始檔夾下都沒有error/狀態碼.html則會響應SpringBoot為我們提供的預設錯誤頁面,也即是下面的程式碼
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
2、如何定製錯誤的JSON資料
①編寫一個自定義異常
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {
super("使用者不存在!");
}
}
②編寫一個異常處理器
a、異常處理方法使用@ResponseBody
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)//處理的異常
public Map handlerException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
//map.put("exception",e);
return map;
}
}
異常的處理方法中通過map放置異常資訊,這個map中的key和value就是我們能夠在出現異常時定製的資訊
結果:通過postMan請求和通過瀏覽器請求都會是如下結果,並沒有自適應效果(瀏覽器訪問應該響應頁面),這是因為在異常攔截器中使用了@ResponseBody
{
"code":"user.notExist",
"message":"使用者不存在!"
}
b、不使用@ResponseBody,而將請求轉發至/error,達到自適應效果
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//處理的異常
public String handlerException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
//map.put("exception",e);
return "forward:/error";
}
}
SpringBoot提供了處理/error請求的Controller,自適應效果會在該Controller中實現,但此時響應錯誤頁面並不是我們自定義的錯誤頁面,而是SpringBoot提供的錯誤頁面,且狀態碼為200:
這是因為我們在異常處理方法中沒有設定狀態碼,修改一下處理方法:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//處理的異常
public String handlerException(Exception e, HttpServletRequest request){
request.setAttribute("javax.servlet.error.status_code",400);
Map<String,Object> map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
return "forward:/error";
}
}
錯誤狀態碼的key值是BasicErrorController在解析錯誤時需要從請求中獲取的:
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = this.getStatus(request);
return new ResponseEntity(body, status);
}
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
此時再在瀏覽器中訪問時:
但是在postMan中請求時會發現並沒有將我們自定義的定製資訊(code、message等)帶回到響應中來,
③如果我們想使用自適應同時又想定製錯誤資訊,有一下兩種方式:出現錯誤以後,會來到/error請求,會被BasicErrorController處理,響應出去可以獲取的資料是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)規定的方法):
a、在容器中新增ErrorController的實現類或者繼承ErrorController的子類AbstractErrorController
b、頁面上能獲取到的資料,或者是json返回的資料都是通過errorAttributes.getErrorAttributes()得到的,SpringBoot容器中通過DefaultErrorAttributes.getErrorAttributes()進行資料處理,因此我們可以自定義ErrorAttributes,重寫該類的getErrorAttributes方法,在從父類獲取的map中塞入定製的資訊即可:
@Component//給容器中加入自定義的錯誤屬性類ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest,includeStackTrace);
map.put("company","bdm");
return map;
}
}
但此時已然只能放置一些公共的定製資訊,不能放入一些具體的資訊,比如在出現某個異常時的提示資訊,此時需要我們改動一下異常處理器和上面的ErrorAttributes,將異常處理器中的map放入請求物件中攜帶到ErrorAttributes中:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(UserNotExistException.class)//處理的異常
public String handlerException(Exception e, HttpServletRequest request){
request.setAttribute("javax.servlet.error.status_code",400);
Map<String,Object> map = new HashMap<>();
map.put("code","user.notExist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
return "forward:/error";
}
}
@Component//給容器中加入自定義的錯誤屬性類ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest,includeStackTrace);
map.put("company","bdm");
Map<String,Object> ext = (Map<String,Object>)webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
這樣我們就可以在頁面和響應的json中獲取到定製的錯誤資訊了: