java框架之SpringBoot(7)-異常處理

前言

在 SpringBoot 項目中,默認狀況下,使用瀏覽器訪問一個不存在的地址會返回以下錯誤頁面:html

而當客戶端未非瀏覽器時,錯誤信息則會以 json 數據返回,以下:java

會出現如上效果的緣由是 SpringBoot 針對錯誤消息作了自動配置,對應自動配置類爲 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 。web

自定義錯誤頁

查看錯誤自動配置類會發如今該類中註冊了以下組件:spring

ErrorPageCustomizer

@Bean
public ErrorPageCustomizer errorPageCustomizer() {
    return new ErrorPageCustomizer(this.serverProperties);
}

查看該組件類:json

 1     private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
 2 
 3         private final ServerProperties properties;
 4 
 5         protected ErrorPageCustomizer(ServerProperties properties) {
 6             this.properties = properties;
 7         }
 8 
 9         @Override
10         public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
11             ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
12                     + this.properties.getError().getPath());
13             errorPageRegistry.addErrorPages(errorPage);
14         }
15 
16         @Override
17         public int getOrder() {
18             return 0;
19         }
20 
21     }
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration.ErrorPageCustomizer

在第 10 行的 registerErrorPages 方法中,註冊了一個錯誤頁,錯誤頁路徑爲 this.properties.getError().getPath() ,該值爲瀏覽器

@Value("${error.path:/error}")
private String path = "/error";

即,一旦出現了 4xx 或 5xx 錯誤,該組件就會生效,可用其定製系統發生錯誤時的轉發路徑,默認狀況下當前請求會轉發到  /error 路徑。springboot

BasicErrorController

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
            this.errorViewResolvers);
}

查看該組件類:app

 1 package org.springframework.boot.autoconfigure.web;
 2 
 3 import java.util.Collections;
 4 import java.util.List;
 5 import java.util.Map;
 6 
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace;
11 import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
12 import org.springframework.http.HttpStatus;
13 import org.springframework.http.MediaType;
14 import org.springframework.http.ResponseEntity;
15 import org.springframework.stereotype.Controller;
16 import org.springframework.util.Assert;
17 import org.springframework.web.bind.annotation.RequestMapping;
18 import org.springframework.web.bind.annotation.ResponseBody;
19 import org.springframework.web.servlet.ModelAndView;
20 
21 @Controller
22 @RequestMapping("${server.error.path:${error.path:/error}}")
23 public class BasicErrorController extends AbstractErrorController {
24 
25     private final ErrorProperties errorProperties;
26 
27     public BasicErrorController(ErrorAttributes errorAttributes,
28             ErrorProperties errorProperties) {
29         this(errorAttributes, errorProperties,
30                 Collections.<ErrorViewResolver>emptyList());
31     }
32 
33     public BasicErrorController(ErrorAttributes errorAttributes,
34             ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
35         super(errorAttributes, errorViewResolvers);
36         Assert.notNull(errorProperties, "ErrorProperties must not be null");
37         this.errorProperties = errorProperties;
38     }
39 
40     @Override
41     public String getErrorPath() {
42         return this.errorProperties.getPath();
43     }
44 
45     @RequestMapping(produces = "text/html")
46     public ModelAndView errorHtml(HttpServletRequest request,
47             HttpServletResponse response) {
48         HttpStatus status = getStatus(request);
49         Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
50                 request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
51         response.setStatus(status.value());
52         ModelAndView modelAndView = resolveErrorView(request, response, status, model);
53         return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
54     }
55 
56     @RequestMapping
57     @ResponseBody
58     public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
59         Map<String, Object> body = getErrorAttributes(request,
60                 isIncludeStackTrace(request, MediaType.ALL));
61         HttpStatus status = getStatus(request);
62         return new ResponseEntity<Map<String, Object>>(body, status);
63     }
64 
65     protected boolean isIncludeStackTrace(HttpServletRequest request,
66             MediaType produces) {
67         IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
68         if (include == IncludeStacktrace.ALWAYS) {
69             return true;
70         }
71         if (include == IncludeStacktrace.ON_TRACE_PARAM) {
72             return getTraceParameter(request);
73         }
74         return false;
75     }
76 
77     protected ErrorProperties getErrorProperties() {
78         return this.errorProperties;
79     }
80 
81 }
org.springframework.boot.autoconfigure.web.BasicErrorController

能夠看到該組件其實是一個控制器,用來處理路徑爲配置中定義的 "${server.error.path:${error.path:/error}}" 請求,若是 server.error 和 error.path 都沒有配置,則默認處理路徑爲 /error 的請求。ide

控制器中有兩個響應方法,分別爲第 46 行的 errorHtml 方法和第 58 行的 error 方法,它們都是用來處理路徑爲 /error 的請求,但 errorHtml 方法返回的錯誤消息是一個 html 頁面,而 error 方法是返回的錯誤消息是一個 json 數據。經過 @RequestMapping 註解中的 produces 屬性來區分客戶端須要的錯誤消息類型,即根據客戶端的 accept 請求頭區分。具體以哪一個頁面做爲錯誤頁則可看到在第 52 行的 resolveErrorView 方法:測試

 1 protected ModelAndView resolveErrorView(HttpServletRequest request,
 2         HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
 3     for (ErrorViewResolver resolver : this.errorViewResolvers) {
 4         ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
 5         if (modelAndView != null) {
 6             return modelAndView;
 7         }
 8     }
 9     return null;
10 }
org.springframework.boot.autoconfigure.web.AbstractErrorController#resolveErrorView

能夠看到,該方法時遍歷容器中全部的錯誤視圖解析器,若是解析器解析當前請求返回的 modelAndView 不爲空,則以該 modelAndView 做爲錯誤頁的響應。即:以哪一個頁面做爲錯誤頁是由錯誤視圖解析器的 resolveErrorView 方法的返回值決定。

DefaultErrorViewResolver

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext,
            this.resourceProperties);
}

這是默認配置的錯誤視圖解析器,查看它的 resolveErrorView 方法:

 1 @Override
 2 public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
 3         Map<String, Object> model) {
 4     // 傳入字符串形式的狀態碼
 5     ModelAndView modelAndView = resolve(String.valueOf(status), model);
 6     if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
 7         modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
 8     }
 9     return modelAndView;
10 }
11 
12 private ModelAndView resolve(String viewName, Map<String, Object> model) {
13     String errorViewName = "error/" + viewName;  // 如:error/404
14     TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
15             .getProvider(errorViewName, this.applicationContext);
16     if (provider != null) { // 若是模板引擎解析器可解析則返回模板視圖
17         return new ModelAndView(errorViewName, model);
18     }
19     // 模板引擎不可解析時
20     return resolveResource(errorViewName, model);
21 }
22 
23 private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
24     for (String location : this.resourceProperties.getStaticLocations()) { // 遍歷靜態資源文件夾
25         try {
26             Resource resource = this.applicationContext.getResource(location); // 獲取靜態資源
27             resource = resource.createRelative(viewName + ".html");  // 如:error/404.html
28             if (resource.exists()) { // 判斷對應資源是否存在
29                 return new ModelAndView(new HtmlResourceView(resource), model);  // 若是存在則返回對應 html 視圖
30             }
31         }
32         catch (Exception ex) {
33         }
34     }
35     return null;
36 }
org.springframework.boot.autoconfigure.web.DefaultErrorViewResolver#resolveErrorView

經過上述代碼能夠看到,當請求出現錯誤時,錯誤視圖解析器會在模板路徑及靜態文件夾路徑下尋找以該錯誤對應狀態碼命名的 html 頁面做爲錯誤響應視圖。好比錯誤代碼爲 404,那麼默認狀況下將會尋找在 templates 和 static 等靜態資源文件夾下的 error/404.html 頁面做爲響應頁。咱們還能夠經過使用 4xx.html 和 5xx.html 做爲模板頁或靜態頁分別來匹配以 4 開頭和 5 開頭的錯誤讓其做爲該錯誤的響應頁。從 org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml 方法的返回值能夠看到,若是在模板文件夾和靜態文件夾下都沒有找到對應的錯誤頁,那麼將會返回 new ModelAndView("error", model) 對象,而這個 error 視圖在錯誤自動配置類中中已經配置好了:

 1 @Configuration
 2 @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
 3 @Conditional(ErrorTemplateMissingCondition.class)
 4 protected static class WhitelabelErrorViewConfiguration {
 5 
 6     private final SpelView defaultErrorView = new SpelView(
 7             "<html><body><h1>Whitelabel Error Page</h1>"
 8                     + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
 9                     + "<div id='created'>${timestamp}</div>"
10                     + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
11                     + "<div>${message}</div></body></html>");
12 
13     @Bean(name = "error")
14     @ConditionalOnMissingBean(name = "error")
15     public View defaultErrorView() {
16         return this.defaultErrorView;
17     }
18 
19     @Bean
20     @ConditionalOnMissingBean(BeanNameViewResolver.class)
21     public BeanNameViewResolver beanNameViewResolver() {
22         BeanNameViewResolver resolver = new BeanNameViewResolver();
23         resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
24         return resolver;
25     }
26 }
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration

DefaultErrorAttributes

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
}

如上咱們只說明瞭錯誤頁的顯示規則,那錯誤頁的消息又是從何而來呢?回頭看到 org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml 方法:

 1 @RequestMapping(produces = "text/html")
 2 public ModelAndView errorHtml(HttpServletRequest request,
 3         HttpServletResponse response) {
 4     HttpStatus status = getStatus(request);
 5     Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
 6             request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
 7     response.setStatus(status.value());
 8     ModelAndView modelAndView = resolveErrorView(request, response, status, model);
 9     return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
10 }
org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml

能夠看到返回的 model 的數據爲 getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)) 方法的返回值,查看該方法:

1 protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
2         boolean includeStackTrace) {
3     RequestAttributes requestAttributes = new ServletRequestAttributes(request);
4     return this.errorAttributes.getErrorAttributes(requestAttributes,
5             includeStackTrace);
6 }
org.springframework.boot.autoconfigure.web.AbstractErrorController#getErrorAttributes

繼續查看 this.errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace) 方法:

 1 @Override
 2 public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
 3         boolean includeStackTrace) {
 4     Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
 5     errorAttributes.put("timestamp", new Date());
 6     addStatus(errorAttributes, requestAttributes);
 7     addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
 8     addPath(errorAttributes, requestAttributes);
 9     return errorAttributes;
10 }
11 
12 private void addStatus(Map<String, Object> errorAttributes,
13         RequestAttributes requestAttributes) {
14     Integer status = getAttribute(requestAttributes,
15             "javax.servlet.error.status_code");
16     if (status == null) {
17         errorAttributes.put("status", 999);
18         errorAttributes.put("error", "None");
19         return;
20     }
21     errorAttributes.put("status", status);
22     try {
23         errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
24     }
25     catch (Exception ex) {
26         errorAttributes.put("error", "Http Status " + status);
27     }
28 }
29 
30 private void addErrorDetails(Map<String, Object> errorAttributes,
31         RequestAttributes requestAttributes, boolean includeStackTrace) {
32     Throwable error = getError(requestAttributes);
33     if (error != null) {
34         while (error instanceof ServletException && error.getCause() != null) {
35             error = ((ServletException) error).getCause();
36         }
37         errorAttributes.put("exception", error.getClass().getName());
38         addErrorMessage(errorAttributes, error);
39         if (includeStackTrace) {
40             addStackTrace(errorAttributes, error);
41         }
42     }
43     Object message = getAttribute(requestAttributes, "javax.servlet.error.message");
44     if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null)
45             && !(error instanceof BindingResult)) {
46         errorAttributes.put("message",
47                 StringUtils.isEmpty(message) ? "No message available" : message);
48     }
49 }
50 
51 private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
52     BindingResult result = extractBindingResult(error);
53     if (result == null) {
54         errorAttributes.put("message", error.getMessage());
55         return;
56     }
57     if (result.getErrorCount() > 0) {
58         errorAttributes.put("errors", result.getAllErrors());
59         errorAttributes.put("message",
60                 "Validation failed for object='" + result.getObjectName()
61                         + "'. Error count: " + result.getErrorCount());
62     }
63     else {
64         errorAttributes.put("message", "No errors");
65     }
66 }
67 
68 private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
69     StringWriter stackTrace = new StringWriter();
70     error.printStackTrace(new PrintWriter(stackTrace));
71     stackTrace.flush();
72     errorAttributes.put("trace", stackTrace.toString());
73 }
74 
75 private void addPath(Map<String, Object> errorAttributes,
76         RequestAttributes requestAttributes) {
77     String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
78     if (path != null) {
79         errorAttributes.put("path", path);
80     }
org.springframework.boot.autoconfigure.web.DefaultErrorAttributes#getErrorAttributes

經過上述代碼咱們能夠知道在錯誤頁中咱們可使用以下錯誤信息:

timestamp:時間戳
status:狀態碼
error:錯誤提示
exception:異常對象
message:異常信息
errors:JSR303 數據校驗錯誤信息

自定義錯誤信息

上述已經描述了咱們如何使用自定義的錯誤頁,可是使用的錯誤信息還依舊是 SpringBoot 默認配置的,若是咱們想要本身定製錯誤信息,則可經過以下方式。

方便下面測試先編寫以下異常類及控制器:

package com.springboot.webdev2.ex;

public class MyException extends RuntimeException {
    public MyException() {
        super("運行期間出異常了");
    }
}
com.springboot.webdev2.ex.MyException
package com.springboot.webdev2.controller;

import com.springboot.webdev2.ex.MyException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TestController {

    @RequestMapping("test")
    public void test1(){
        throw new MyException();
    }
}
com.springboot.webdev2.controller.TestController

方式一:自定義異常處理器

 1 package com.springboot.webdev2.component;
 2 
 3 import com.springboot.webdev2.ex.MyException;
 4 import org.springframework.web.bind.annotation.ControllerAdvice;
 5 import org.springframework.web.bind.annotation.ExceptionHandler;
 6 import org.springframework.web.bind.annotation.ResponseBody;
 7 
 8 import java.util.HashMap;
 9 import java.util.Map;
10 
11 @ControllerAdvice
12 public class MyExceptionHandler {
13 
14     @ResponseBody
15     @ExceptionHandler(MyException.class)
16     public Map<String,Object> handleException(Exception e){
17         Map<String, Object> map = new HashMap<>();
18         map.put("code", "myCode");
19         map.put("msg", "自定義的異常");
20         return map;
21     }
22 }

com.springboot.webdev2.component.MyExceptionHandler

該方式是 SpringMVC 提供的異常處理方式,缺點:使用該方式失去了 SpringBoot 自己的根據客戶端的不一樣自適應響應數據類型的功能。

方式二:轉發到錯誤處理路徑

咱們已經知道默認狀況下出現異常 SpringBoot 會將請求轉發到 /error ,那麼若是咱們經過異常處理器手動轉發到該路徑,並可手動將咱們須要的錯誤信息放入請求域,咱們就能夠解決方式一的缺點而且能夠在錯誤頁使用咱們本身的錯誤信息了。

package com.springboot.webdev2.component;

import com.springboot.webdev2.ex.MyException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(MyException.class)
    public String handleException(Exception e, HttpServletRequest request){
        // SpringBoot 默認使用的狀態碼就是請求域中的 javax.servlet.error.status_code
        request.setAttribute("javax.servlet.error.status_code", 400);
        Map<String, Object> map = new HashMap<>();
        map.put("code", "myCode");
        map.put("msg", "自定義的異常");
        request.setAttribute("ext", map);
        return "forward:/error";
    }
}
com.springboot.webdev2.component.MyExceptionHandler
<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>[[${status}]]</h1>
    <h2>[[${message}]]</h2>
    <!--請求域中取錯誤信息-->
    <h2>[[${ext.msg}]]</h2>
</body>
</html>

templates/error/4xx.html

能夠發現,該方式依舊有一個缺點:放入請求域中的數據未被序列化,因此只可在轉發到的模板頁中取到,而在客戶端是非瀏覽器時是拿不到自定義的錯誤信息的。

方式三:自定義錯誤處理控制器

出現異常時 SpringBoot 會將請求轉發到 /error ,而處理該請求的控制器爲 BaseErrorController  ,查看該控制器註冊信息咱們也能夠知道,當咱們本身定義一個 org.springframework.boot.autoconfigure.web.ErrorController 組件註冊到容器中時,那麼默認的 BasicErrorController 就不生效了,因此咱們能夠在自定義的錯誤處理控制器中根據咱們的須要取到咱們合適的信息返回。該方式比較複雜,明顯不合適,瞭解便可,略過。

方式四:自定義ErrorAttributes

經過查看 org.springframework.boot.autoconfigure.web.BasicErrorController 咱們已經知道,無論是響應 html 仍是 json 錯誤信息,它們的錯誤信息都是經過 this.errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace) 方法取到,而 this.errorAttributes 對應的組件實際上在錯誤自動配置類中已經註冊,即 DefaultErrorAttributes ,因此咱們能夠自定義一個的 org.springframework.boot.autoconfigure.web.ErrorAttributes 組件註冊到容器中,重寫它的 getErrorAttributes 方法,經過手動取得自定義的錯誤信息返回便可。

package com.springboot.webdev2.component;

import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;

import java.util.Map;

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
        errorAttributes.put("ext", requestAttributes.getAttribute("ext", RequestAttributes.SCOPE_REQUEST));
        return errorAttributes;
    }
}
com.springboot.webdev2.component.MyErrorAttributes
package com.springboot.webdev2.component;

import com.springboot.webdev2.ex.MyException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(MyException.class)
    public String handleException(Exception e, HttpServletRequest request){
        // SpringBoot 默認使用的狀態碼就是請求域中的 javax.servlet.error.status_code
        request.setAttribute("javax.servlet.error.status_code", 400);
        Map<String, Object> map = new HashMap<>();
        map.put("code", "myCode");
        map.put("msg", "自定義的異常");
        request.setAttribute("ext", map);
        return "forward:/error";
    }
}
com.springboot.webdev2.component.MyExceptionHandler
<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>[[${status}]]</h1>
    <h2>[[${message}]]</h2>
    <!--請求域中取錯誤信息-->
    <h2>[[${ext.msg}]]</h2>
</body>
</html>

templates/error/4xx.html
相關文章
相關標籤/搜索