【Spring Boot】8.錯誤處理

簡介

錯誤處理機制提及來是每一個網站架構開發的核心部分,不少時候咱們並無去關注他們,其實錯誤在咱們平常訪問過程當中時長出現,對錯誤機制進行了解也是開發一個好的網站所必備的技能之一。html

默認錯誤反饋

spring boot默認會根據不一樣的請求客戶端,返回不一樣的結果: 一、狀況一:返回一個默認的錯誤頁面java

當咱們使用web訪問出錯的時候,會跳到這樣的錯誤頁面,其信息以下所示:web

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Dec 17 14:50:33 CST 2018
There was an unexpected error (type=Bad Request, status=400).
Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "aaa"

二、 狀況二:返回json信息spring

當咱們使用其餘的客戶端,例如postman模擬請求的時候,返回的信息則是json數據格式:json

{
    "timestamp": "2018-12-17T06:59:00.851+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/somepage"
}

這裏要模擬一個頁面不存在的錯誤錯誤,最好先把登陸過濾器關掉,不然請求任何不存在的頁面都會給你過濾到登陸界面,不會出現錯誤信息。瀏覽器

SpringBoot錯誤處理過程

咱們開發網站過程當中,顯然不會使用這些默認方式,而是要本身去定製反饋結果的。但咱們首先仍是先去了解SpringBoot的默認錯誤處理過程,瞭解一下原理。springboot

參考自動配置類ErrorMvcAutoConfiguration。咱們看看該自動配置類爲容器中添加了以下組件:架構

  • DefaultErrorAttributes 記錄錯誤相關的信息並將其共享;
  • BasicErrorController

查看BasicErrorController源碼app

@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 = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        // 去哪一個頁面做爲錯誤頁面:包含頁面的地址和內容
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    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);
    }
}

能夠知道,該組件用於默認處理/error請求,其中須要留意:ide

  • ErrorPageCustomizer 系統出現錯誤的時候來到error請求進行處理,相似於error.xml裏配置的錯誤頁面規則。
  • DefaultErrorViewResolver

錯誤處理的流程

一旦系統出現4XX或者5XX之類的錯誤,ErrorPageCustomizer就會生效(定製錯誤的響應規則),使請求來到/error。這時候,BasicErrorController控制器會處理這個請求。

觀看上述代碼咱們能夠發現,之因此出現兩種錯誤結果,無非就是對error進行處理的controller會根據不一樣的請求頭:

  • Accept: text/html
  • Accept:"*/*"

這二者進行不一樣的反饋,前者返回錯誤頁面信息,後者返回json數據。咱們來看看錯誤響應頁面的視圖解析器:

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
        Iterator var5 = this.errorViewResolvers.iterator();

        ModelAndView modelAndView;
        do {
            if (!var5.hasNext()) {
                return null;
            }

            ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
            modelAndView = resolver.resolveErrorView(request, status, model);
        } while(modelAndView == null);

        return modelAndView;
    }

這段代碼拿到了異常視圖解析器(ErrorViewResolvers)類型來進行處理,當前咱們註冊的是DefaultErrorViewResolver,查看源碼:

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            //status.series() 狀態碼
            modelAndView = this.resolve((String)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);
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }

默認spring boot會去找到某個頁面:error/狀態碼.html。

定義錯誤頁面

有模板引擎的狀況下

跳轉到模板頁面:error/狀態碼,也就是說,咱們若是想自定義錯誤頁面的話,將錯誤頁面命名爲狀態碼.html,並放在模板文件夾(templates)的error文件夾下,發生此狀態碼的錯誤就會來到對應的頁面;查看源碼咱們也能夠發現,命名爲4xx.html(5xx.html)則能夠處理全部以4(5)開頭的錯誤碼錯誤,即均可以跳到該頁面;不過spring boot會優先尋找直接對應的錯誤頁面,若是404錯誤會優先選取404.html做爲錯誤頁面;

錯誤頁面能獲取到的信息(DefaultErrorAttributes):

  • timestamp 時間戳
  • status 狀態碼
  • error 錯誤提示
  • exception 異常對象
  • message 異常信息
  • error JSR303數據校驗的錯誤都在這裏

即咱們能夠在錯誤頁面裏獲取到錯誤信息,示例以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>This is 4xx error page:</h1>
    <p>*timestamp: [[${timestamp}]]</p>
    <p>*status: [[${status}]]</p>
</body>
</html>

沒有模板引擎的狀況下

即咱們沒有對應的error錯誤文件夾,也能夠放在靜態資源文件夾下。

例如,放在static文件夾下,一樣能夠來到該頁面,只不過不能被模板引擎渲染而已。

以上兩種都不知足的狀況下

這種狀況會來到SpringBoot默認的錯誤提示頁面,該視圖對象的信息咱們能夠經過查看源碼獲悉,其位置位於ErrorMvcAutoConfiguration中:

@Configuration
    @ConditionalOnProperty(
        prefix = "server.error.whitelabel",
        name = {"enabled"},
        matchIfMissing = true
    )
    @Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class})
    protected static class WhitelabelErrorViewConfiguration {
        private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView();

        protected WhitelabelErrorViewConfiguration() {
        }

        @Bean(
            name = {"error"}
        )
        @ConditionalOnMissingBean(
            name = {"error"}
        )
        public View defaultErrorView() {
            return this.defaultErrorView;
        }

        @Bean
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver resolver = new BeanNameViewResolver();
            resolver.setOrder(2147483637);
            return resolver;
        }
    }

定義錯誤信息

spring boot會根據請求頭給予不一樣的返回類型數據。上一節講到的是定義錯誤頁面,還差一種方式:即其餘客戶端訪問狀況下返回json數據的問題,這一節,來處理這個問題。即如何定製錯誤的json數據。

自定義異常處理

咱們先自定義一種異常,例如用戶不存在的異常:

exception/UserNotExistException.class

package com.zhaoyi.springboot.restweb.exception;

public class UserNotExistException extends RuntimeException{
    public UserNotExistException(){
        super("用戶不存在");
    }
}

而後在應用程序的某個地方拋出該異常:

controller/HelloController.class

@RequestMapping({"user"})
    public String index(@RequestParam("user") String user){
        if(user.equals("aaa")){
            throw new UserNotExistException();
        }
       return "index";
    }

經過訪問/user?user=aaa觸發該異常。

顯然,若是咱們不作任何處理,SpringBoot會默認將錯誤處理到咱們以前配置過的頁面,運行時錯誤對應的是500,即會跳轉到咱們的5xx頁面:

this is 5xx error page:
status ------ 500

message ------ 用戶不存在

那麼,咱們該如何將此錯誤自定義呢,能夠運用springMVC的知識,在controller下面定義個異常處理器:

controller/MyExceptionHandler.class

package com.zhaoyi.springboot.restweb.controller;

import com.zhaoyi.springboot.restweb.exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExcetionHandler {
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handlerException(Exception e){
        Map<String, Object> map = new HashMap<>();
        map.put("myCode", "custom code");
        map.put("message", e.getMessage());
        return map;
    }
}

咱們在此訪問一樣的觸發異常的地址,就能夠如願的獲得本身想要的自定義錯誤信息了:

{"myCode":"custom code","message":"用戶不存在"}

但這種方式有點問題,沒有自適應效果,也就是咱們用瀏覽器也好,其餘的客戶端也好,返回的都是這段json數據。那麼,咱們如何想springboot那樣作到異常返回的自適應呢?(瀏覽器返回錯誤頁面,其餘客戶端返回json數據)。很簡單,轉發到/error,交由SpringBoot處理便可。

 

注意註釋掉以前的處理代碼。

這時候,咱們若是換用不一樣的客戶端訪問就會獲得相應的反饋了,好比用瀏覽器能夠獲得以下的返回數據:

<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'>Mon Dec 17 16:15:30 CST 2018</div>
<div>There was an unexpected error (type=OK, status=200).</div><div>?????</div></body></html>

可是新的問題又出現了,即:咱們自定義的頁面並無獲得解析,springboot仍是默認使用了以前分析過的,什麼都沒有定義的時候跳轉到空白錯誤頁面的狀況。

仔細觀察咱們會發現,實際上是錯誤狀態碼有問題,這裏是錯誤碼已經由500變爲了200.問題出在哪裏呢?出在咱們在轉發的時候,沒有設置一個錯誤狀態碼:所以,咱們還須要在轉發以前設置狀態碼。如何設置,先來查看springboot相關處理錯誤信息的源碼

BasicErrorController.class

@RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        // 經過此處獲取錯誤碼
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

繼續定位源碼this.getStatus(request);:

AbstractErrorController.class

protected HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        } else {
            try {
                return HttpStatus.valueOf(statusCode);
            } catch (Exception var4) {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }
        }
    }

所以,咱們只須要在request域中添加一個javax.servlet.error.status_code屬性,就能夠以最優先的級別狀況設置狀態碼了。因此,改造後的代碼應該以下所示:

MyExcetionHandler

@ExceptionHandler(UserNotExistException.class)
    public String handlerException(Exception e, HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        request.setAttribute("javax.servlet.error.status_code", 500);
        map.put("myCode", "custom code");
        map.put("message", e.getMessage());
        return "forward:/error";
    }

這時候在運行發現能夠調到咱們自定義的5xx.html錯誤頁面了。顯示以下:

this is 5xx error page:
status ------ 500

message ------ 用戶不存在

myCode ------

問題仍是有,咱們會發現,咱們自定義的數據不見了(myCode),錯誤頁面只能獲取到SpringBoot默認寫入的信息。所以咱們還得繼續探索,如何才能既能調到自定義錯誤頁面,又能攜帶咱們自定義的錯誤數據。

咱們知道,出現錯誤之後,會相應到/error請求,同時交由BasicErrorController進行處理,他在處理錯誤的時候進行了自適應處理,響應回來並能夠獲取的數據是getErrorAttributes(是BasicErrorController的父類AbstractErrorController中定義的)獲得的。

咱們則徹底能夠編寫一個ErrorController的實現類(或者繼承BasicErrorController),放在容器中。想一想有點麻煩,固然,還有選擇。

第二種辦法,注意第一種方法的某句話errorAttributes.getErrorAttributes....,頁面上能用的數據,或者是json返回能用的數據都是經過他來獲得的。查看ErrorAttribute的來源:

ErrorMvcAutoConfiguration.class

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

容器中的DefaultErrorAttributes來進行數據處理的,因此,咱們本身配置一個實現了ErrorAttributes類型接口這樣的Bean就能夠了,可是爲了方便,咱們最好繼承spring boot默認使用的DefaultErrorAttributes來實現便可。

自定義ErrorAttribute,改變默認行爲

componet/MyErrorAttributes.class

package com.zhaoyi.springboot.restweb.component;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

@Componet
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
        // 這裏隨便寫一個本身的
        errorAttributes.put("someCode", "attribute add atrribute");
        // 從request請求域中獲取ext的值
        errorAttributes.put("ext", webRequest.getAttribute("ext", WebRequest.SCOPE_REQUEST));
        return errorAttributes;
    }
}

注意:這是一個組件,不要忘記添加@Componet註解,否則沒法加入到容器中。

在這裏咱們使用request域傳遞信息,而且經過WebRequest.getAttribute("param", SCOPE)獲取其信息,WebRequest.SCOPE_REQUEST的取值對應什麼,點進WebRequest代碼內容就能夠看到對應信息了。這樣,咱們還須要修改異常處理器的代碼,以下所示:

MyExceptionHandler.class

@ExceptionHandler(UserNotExistException.class)
    public String handlerException(Exception e, HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        request.setAttribute("javax.servlet.error.status_code", 500);
        map.put("myCode", "custom code");
        map.put("message", e.getMessage());
        request.setAttribute("ext", map);
        return "forward:/error";
    }

使用postman訪問異常頁面,返回結果以下:

{
    "timestamp": "2018-12-17T08:54:29.906+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "用戶不存在",
    "path": "/user",
    "someCode": "attribute add atrribute",
    "ext": {
        "myCode": "custom code",
        "message": "用戶不存在"
    }
}

錯誤先關的知識就到這裏爲止了,咱們還得繼續往下探索,下面的內容會愈來愈有意思,他是什麼呢?

—— 嵌入式Servlet容器配置修改。

相關文章
相關標籤/搜索