1. 程式人生 > >SpringBoot定製錯誤頁面及原理

SpringBoot定製錯誤頁面及原理

一、SpringBoot預設的錯誤處理機制


1)、瀏覽器返回的預設錯誤頁面如下:
        
   ☞ 瀏覽器傳送請求的請求頭資訊如下:text/html會在後面的原始碼分析中說到。
      
2)、如果是其他客戶端,預設則響應錯誤的JSON字串,如下所示:
      
   ☞ 其他客戶端傳送請求的請求頭資訊如下:" */* "原始碼中解釋。
      

二、原理分析:參照ErrorMvcAutoConfiguration類:錯誤處理的自動配置類,以下4項為此類的重要資訊。


1)、ErrorMvcAutoConfiguration.ErrorPageCustomizer:當系統出現4xx或者5xx之類的錯誤時,ErrorPageCustomizer就會生效(定製錯誤的響應規則),根據如下原始碼可知,將會來到/error請求。

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

    //進入ErrorPageCustomizer方法,發現registerErrorPages方法:註冊一個錯誤也
    private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
        private final ServerProperties properties;

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

        public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
            ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + 
                                    this.properties.getError().getPath());
            errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
        }
    }

    //進入this.properties.getError().getPath()方法,獲取如下資訊,得到/error請求。
    @Value("${error.path:/error}")
    private String path = "/error";//系統出現錯誤以後來到error請求進行處理;(web.xml註冊的錯誤頁面規則)

2)、BasicErrorController處理/error錯誤請求:注意:text/html和*/*就是在此處生效。

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

    //進入BasicErrorController物件,獲取如下資訊
    @Controller
    @RequestMapping({"${server.error.path:${error.path:/error}}"})
    public class BasicErrorController extends AbstractErrorController {
        private final ErrorProperties errorProperties;

        public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
            this(errorAttributes, errorProperties, Collections.emptyList());
        }

        @RequestMapping(
            produces = {"text/html"}//產生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
        @ResponseBody//產生json資料,其他客戶端來到這個方法處理;
        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);
        }

☞ 如上程式碼中提到的錯誤頁面解析程式碼,進入此方法: this.resolveErrorView(request, response, status, model);

    protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
        Iterator var5 = this.errorViewResolvers.iterator();
        ModelAndView modelAndView;
        do {
            //從所有的ErrorViewResolver得到ModelAndView
            if(!var5.hasNext()) {
                return null;
            }

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

        return modelAndView;
    }

3)、最總的響應頁面是由DefaultErrorViewResolver解析得到的:最重要的資訊是,SpringBoot預設模板引擎的/error目錄下獲取‘status’.xml錯誤頁面,也可以通過4xx.xml來統配404.xml和400.xml等等,但是優先獲取精準的頁面。如果模板引擎中不存在,則會從靜態頁面中獲取錯誤頁面。否則返回系統預設錯誤頁面。

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

    //進入DefaultErrorViewResolver類中
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = this.resolve(String.valueOf(status), model);
        if(modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {

            //呼叫時viewname = status ***重要
            modelAndView = this.resolve((String)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);

        //模板引擎可用的情況下返回到errorViewName指定的檢視地址,
        //當模板引擎不可用,就在靜態資原始檔夾下找errorViewName對應的頁面 error/404.html
        return provider != null?new ModelAndView(errorViewName, model):this.resolveResource(errorViewName, model);
    }

4)、DefaultErrorAttributes:在頁面新增錯誤資訊,供我們使用。

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

    //進入DefaultErrorAttributes類中,發現此方法給檢視中添加了status狀態等資訊,供我們使用。
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, requestAttributes);
        this.addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
        this.addPath(errorAttributes, requestAttributes);
        return errorAttributes;
    }

三、定製錯誤JSON資料


1)、自定義異常處理類,返回定製的JSON資料。通過上述的分析,我們得知:①、可以完全編寫一個ErrorController的實現類,或者繼承AbstractErrorController的子類,放入容器中。②、也可以自定義異常處理類,返回JSON資料。③、頁面上的資料或JSON返回的資料都是可以通過errorAttributes.getErrorAttributes得到的。我們可以自定義屬於自己的ErrorAttributes。

//首先我們可以通過自定義異常處理,來確定返回的資料,但這個不夠靈活,我們可以與③結合使用
/**
 * @RequestMapping啟動應用後,被 @ExceptionHandler、@InitBinder、@ModelAttribute 註解的方法,都會作用在 被 @RequestMapping
 * 註解的方法上。
 */
@ControllerAdvice
public class MyExceptionHandler {
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handlerException(Exception e, HttpServletRequest request){
        Map<String,Object> map = new HashMap<String,Object>();
        request.setAttribute("javax.servlet.error.status_code","500");
        map.put("code","user.notexist");
        map.put("message",e.getMessage());
        return map;
    }
}

//③自定義ErrorAttributes,一定要加入容器
@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("company","yintong");

        //獲取我們在異常處理類中新增的資訊,
        /*注意:當我們需要結合使用的時候異常處理必須return "forward:/error";將請求轉發出去,不能直接返回map物件,
        同時要去掉@responseBody註解,否則ErrorAttributes不生效*/
        map.put("ext",requestAttributes.getAttribute("ext",requestAttributes.SCOPE_REQUEST));
        return map;
    }
}

2)、效果展示: