五、SpringBoot錯誤處理機制

1、錯誤處理機制

1.1、Spring Boot默認的錯誤處理機制

如果是瀏覽器,則返回一個默認的錯誤頁面:

默認的錯誤頁面.png

如果是其他測試工具,如Postman,則返回一個json數(shù)據(jù):

其他測試工具默認錯誤json信息.png

原理:

? 可以參照ErrorMvcAutoConfiguration,錯誤處理的自動配置類。該自動配置類給容器中添加了以下幾個組件:

1)、ErrorPageCustomizer:錯誤頁面定制器

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

來看看ErrorPageCustomizer類定義:

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

    private final ServerProperties properties;

    protected ErrorPageCustomizer(ServerProperties properties) {
        this.properties = properties;
    }

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
                                            + this.properties.getError().getPath());
        errorPageRegistry.addErrorPages(errorPage);
    }
    //other code...
}

注冊error頁面,而頁面的請求路徑由getPath方法返回。

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

public String getPath() {
    return this.path;
}

所以當系統(tǒng)出現(xiàn)錯誤以后,會來到error請求進行處理。

2)、BasicErrorController:錯誤控制器

創(chuàng)建一個Error控制器:

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

看看Error控制器定義:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    
    //瀏覽器首先返回text/html
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        //getErrorAttributes根據(jù)錯誤信息來封裝一些model數(shù)據(jù),用于頁面顯示
        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 ? new ModelAndView("error", model) : modelAndView);
    }

    //其他測試工具默認返回json數(shù)據(jù)
    @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<Map<String, Object>>(body, status);
    }
    
    //other code...
}

? 從@RequestMapping屬性上看得出,如果在配置文件中配置了server.error.path值,則使用指定的值作為錯誤請求;如果未配置,則查看是否配置了error.path;如果還是沒有,則該控制器默認處理/error請求。

? 該控制器處理錯誤請求,返回兩種類型,分別是text/html和JSON數(shù)據(jù),具體可以參看以下兩圖:

下圖是瀏覽器訪問時請求頭中的accpet:

瀏覽器訪問時請求頭中的accpet.png

下圖是測試工具訪問時請求頭中的accpet:

測試工具訪問時請求頭中的accpet.png

? 所以當使用瀏覽器訪問時出現(xiàn)錯誤的時候,會進入BasicErrorController控制器中的errorHtml方法,而如果是測試工具訪問時出現(xiàn)錯誤的時候,就進入error方法。

? 在響應頁面的errorHtml方法中,會調用了父類的resolveErrorView方法:

protected ModelAndView resolveErrorView(HttpServletRequest request,
            HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    //獲取所有的視圖解析器來處理這個錯誤信息,而這個errorViewResolvers對象其實就是DefaultErrorViewResolver對象的集合
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}

所以從上述源碼中看得出,在響應頁面的時候,會在父類的resolveErrorView方法中獲取所有的ErrorViewResolver對象(DefaultErrorViewResolver對象),一起來解析這個錯誤信息。

3)、DefaultErrorViewResolver:默認錯誤視圖處理器

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

來看DefaultErrorViewResolver類定義:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
    private static final Map<Series, String> SERIES_VIEWS;

    static {
        Map<Series, String> views = new HashMap<Series, String>();
        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) {
        //先以錯誤狀態(tài)碼作為錯誤頁面名
        ModelAndView modelAndView = resolve(String.valueOf(status), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            //如果無法處理,則使用4xx或者5xx作為錯誤頁面名
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //錯誤頁面:error/400,或者error/404,或者error/500...
        String errorViewName = "error/" + viewName;
        //模版引擎可以解析到這個頁面地址就用模版引擎來解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
                .getProvider(errorViewName, this.applicationContext);
        if (provider != null) {
            //模版引擎能夠解析到頁面的情況下返回到errorViewName指定的視圖
            return new ModelAndView(errorViewName, model);
        }
        //模版引擎不能夠解析到頁面的情況下,就在靜態(tài)資源文件夾下查找errorViewName對應的頁面
        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);
                //從靜態(tài)資源文件中查找errorViewName對應的頁面
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    //如果存在,則直接返回
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }
}

步驟:

? 第1步:假設訪問出現(xiàn)了404報錯,則狀態(tài)碼status=404,首先根據(jù)狀態(tài)碼status生成一個視圖error/status;

? 第2步:然后使用模版引擎去解析這個視圖error/status,就是去查找classpath類路徑下的templates模版文件夾下的error文件夾下是否有status.html這個頁面;

? 第3步:如果模版引擎能夠解析到這個視圖,則將該視圖和model數(shù)據(jù)封裝成ModelAndView返回并結束;否則進入第4步;

? 第4步:假設解析不到error/status視圖,則依次從靜態(tài)資源文件中查找error/status.html,如果存在,則進行封裝返回并結束;否則進入第5步;

? 第5步:在模版引擎解析不到error/status視圖,靜態(tài)文件夾下都沒有error/status.html的情況下,使用error/4xx作為視圖名,即此時status=4xx,重新返回第1步進行查找;

? 第6步:如果最后還是未找到,則使用Spring Boot默認錯誤頁面。

4)、DefaultErrorAttributes

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

看看DefaultErrorAttributes類定義:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
        implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
                                                  boolean includeStackTrace) {
        //用來生成頁面model數(shù)據(jù)
        Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, requestAttributes);
        addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
        addPath(errorAttributes, requestAttributes);
        return errorAttributes;
    }
    
    //other code...
}

? 在Error控制器處理錯誤的時候會調用DefaultErrorAttributesgetErrorAttributes方法來生成model數(shù)據(jù),用于頁面顯示或者json數(shù)據(jù)的返回。

? model數(shù)據(jù):

? timestamp:時間戳

? status:狀態(tài)碼

? error:錯誤提示

? exception:異常對象

? message:錯誤消息

? errors:jsr303數(shù)據(jù)校驗錯誤內容

? path:錯誤請求路徑

5)、defaultErrorView:Spring Boot默認視圖View

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;
}

? 在BasicErrorController返回前,如果在模版文件夾下或者靜態(tài)資源文件夾下都無法找到對應的錯誤頁面,則默認使用error作為視圖名,而在ErrorMvcAutoConfiguration類中創(chuàng)建了名為error的視圖Bean,用來作為Spring Boot默認的錯誤頁面。

1.2、定制錯誤響應

1.2.1、定制錯誤頁面

? 1)、在有模版引擎的情況下,將錯誤頁面命名為狀態(tài)碼.html,并放在模版文件夾下的error文件夾下,發(fā)生此狀態(tài)碼的錯誤就會來到對應的頁面。可以使用4xx和5xx作為錯誤頁面的文件名來匹配這種類型的所有錯誤。精確錯誤頁面優(yōu)先,當沒有精確錯誤的頁面,才去找4xx或者5xx錯誤頁面。

? 2)、如果沒有模版引擎的情況下,就會去靜態(tài)資源文件夾下查找錯誤頁面。

? 3)、上兩者都沒有錯誤頁面,則默認使用Spring Boot的錯誤提示頁面,即使用error作為視圖名。

1.2.2 定制錯誤Json數(shù)據(jù)

1)、自定義異常處理類并返回json數(shù)據(jù)

@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value= NotExistException.class)
    public Map<String, Object> handler(Exception e){
        Map<String, Object> map = new HashMap<>();
        map.put("message", e.getMessage());

        return map;
    }
}

缺點:瀏覽器和測試工具都返回json數(shù)據(jù),沒有自適應的功能。

2)、轉發(fā)到/error請求進行自適應響應處理

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(NotExistException.class)
    public String handler(Exception e, HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
//      Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        //需要傳入自己的錯誤狀態(tài)碼 4xx或者5xx(重要,否則是狀態(tài)碼為200,會找不到對應的錯誤頁面)
        request.setAttribute("javax.servlet.error.status_code", 400);

        map.put("email", "412425870@qq.com");
        map.put("error", "發(fā)生錯誤啦!");
        request.setAttribute("map", map);

        //轉發(fā)到/error請求
        return "forward:/error";
    }
}

缺點:雖然能夠自適應,但是無法將自定義的錯誤信息傳給頁面或者json數(shù)據(jù)。

3)、將自定義的數(shù)據(jù)傳遞給頁面或者json數(shù)據(jù)

自定義ErrorAttributes類,來覆蓋ErrorMvcAutoConfiguration自動配置類中創(chuàng)建的默認DefaultErrorAttributes的Bean。

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);

        //在原來的錯誤信息上添加自定義的錯誤信息
        map.put("author", "caychen");

        //獲取異常處理類中設置的錯誤信息
        Map<String, Object> other = (Map<String, Object>) requestAttributes.getAttribute("map", RequestAttributes.SCOPE_REQUEST);
        map.put("other", other);
        return map;
    }
}

這樣就可以在模版頁面上獲取對應的錯誤信息了。

<h1>status: [[${status}]]</h1>
<h2>timestamp: [[${timestamp}]]</h2>
<h2>error: [[${error}]]</h2>
<h2>exception: [[${exception}]]</h2>
<h2>author: [[${author}]]</h2>
<h2 th:if="${other.error}" th:text="${other.error}"></h2>
<h2 th:if="${other.email}" th:text="${other.email}"></h2>
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當在唯一索引所對應的列上鍵入重復值時,會觸發(fā)此異常。 O...
    我想起個好名字閱讀 6,017評論 0 9
  • width: 65%;border: 1px solid #ddd;outline: 1300px solid #...
    邵勝奧閱讀 5,194評論 0 1
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標準。 注意:講述HT...
    kismetajun閱讀 28,868評論 1 45
  • iOS開發(fā)系列--網(wǎng)絡開發(fā) 概覽 大部分應用程序都或多或少會牽扯到網(wǎng)絡開發(fā),例如說新浪微博、微信等,這些應用本身可...
    lichengjin閱讀 4,058評論 2 7
  • 我不止一次的,想那個誤入桃花源的武陵人為什么最后會回到他的現(xiàn)實世界?是因為心中的牽掛,還是什么想讓更多人過上向往生...
    DS蔓蔓閱讀 304評論 0 1

友情鏈接更多精彩內容