【SpringBoot】默认错误处理机制的源码大白话分析,以及自定义配置(遇坑)

前言

SpringBoot版本:v2.2.6.RELEASE

本博客,主要总结以下3个方面:

  • 一、SpringBoot的错误页面和数据
  • 二、有关类的介绍
  • 三、源码追踪,默认处理机制的流程
  • 四、自定义错误数据

本博客站在学习者的角度,以通俗的大白话、图文并茂地,将整个层次和思路清晰分明地呈现,相信如果耐心看完的同学,应该也会有不一样的收获。任何一个框架的源码都是一个庞大的体系,阅读源码,不须苛求理解代码的每个细节,而应该关注并理解主要(关键)代码的主体思路。最后,看完不妨点赞收藏?自己花了大半天总结,感觉收获满满,对于SpringBoot的理解又更清晰深入了。

一、SpringBoot的错误页面和数据

1、来访者是浏览器

2、来访者是其他客户端(此处,我以Postman来测试)

3、如何区分来访者?请求头!

来访者是浏览器:

来访者是其他客户端:

从上面来看,我们可以把SpringBoot的错误处理分为两类:

(1)来访者是浏览器,返回一个对应的错误页面

(2)来访者是除了浏览器以外的其他客户都安,返回对应的Json数据

二、有关类的介绍

这里之对于关键的类,只作简单的介绍,只需要知道这个类在整个默认错误处理机制中所扮演的主要角色即可。在第四部分的流程讲述中,会深入设计到这些类。

ErrorMvcAutoConfiguration:顾名思义,这是错误处理机制的自动配置类

查看源码,发现,这个类往容器中,添加了以下3个与错误处理机制有关的重要组件:

 

1、ErrorPageCustomizer:定制错误的产生规则,产生/error请求

2、BasicErrorController:顾名思义,它是一个处理/error请求的Controller

(1)返回Html页面

(2)返回Json数据

3、DefaultErrorViewResolver:错误视图的解析

4、DefaultErrorAttributes:共享错误的相关信息

三、源码追踪,默认处理机制的流程

以响应Html页面为例子

1、从上面的介绍可知,当返回一个错误页面时,会调用BasicErrorController中的这个方法(见下图)。

该方法中,拿到请求的状态码和一些Model数据(getErrorAttributes()方法也很重要,后面会介绍),然后调用resolveErrorView()方法进行视图解析,返回对应的错误视图,ModelAndView里面包含错误页面地址和页面内容。

点进this.resolveErrorView()方法, 跳转到AbstractErrorController类中,见下图。

resolveErrorView(),顾名思义:解析错误视图!该方法的大概意思就是,拿到所有的错误视图的解析器(ErrorViewResolver),然后逐一遍历,只要成功获取到一个ModelAndView(匹配到对应的错误视图),就立即返回。

2、关注上面截图中划横线附近的代码。

ErrorViewResolver是什么?点进去看发现它是一个接口。划横线部分调用的resolveErrorView()也是该接口中的方法。把鼠标放到“ErrorViewResolver”附近,然后ctrl + H,查看它的体系结构。发现该接口有一个实现类:DefaultErrorViewResolver

是不是很熟悉?DefaultErrorViewResolver 就是前面介绍中提到的一个组件。那么我们进入DefaultErrorViewResolver中查看它如何进行错误视图的解析(下面截图)。

这3个方法是什么意思呢?见下面的代码注释。

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())) {
                // 如果无法找到准确状态码的错误页面,就模糊匹配:4xx(客户端错误),5xx(服务端错误)
		modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
	}

	return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// 从上面的方法可以看出来,这里传入的viewName实际上就是状态码
	// 拼接视图名称,例如:error/404
	String errorViewName = "error/" + viewName;
	
	// 模板引擎可以解析这个页面地址就用模板引擎解析
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
			.getProvider(errorViewName, this.applicationContext);
								
	
	
	return provider != null ? 
        // 模板引擎可用的情况下返回到errorViewName指定的视图地址
                new ModelAndView(errorViewName, model) 
        // 否则就在静态资源文件夹下找errorViewName对应的页面,例如:error/404.html
                : this.resolveResource(errorViewName, model);
}

3、顺着思路,到这里。其实我们可以清楚地知道,如何定制我们自己的错误页面!这里我就顺着思路提一下,后面会做总结。

从上面我们知道,只要我们在resources下面的static或templates文件夹下,新建一个error文件夹,然后把对应状态码的Html页面(以状态码命名,例如:404.html)放到error文件夹下。或者简单一点,放4xx.html、5xx.html,这样凡是客户端错误,都会使用4xx.html这个页面,服务端错误都会使用5xx.html这个页面。

但是,从上面的代码分析,我们也可以知道,假如同时存在404.html和4xx.html,SpringBoot会优先使用404.html。

 4、到上面为止,我们知道了如何更改对应错误状态的静态页面。但是,假如我需要在页面中取出错误信息,该怎么做?比如说,我想取出这次错误的时间戳、状态码、异常信息等等。我们如何取出这些信息,并显示到我们自己的静态页面中?

这些信息是不是很熟悉?没错,当来访者是非浏览器时,SpringBoot返回的Json数据时包含了这些信息。那么在SpringBoot默认的错误页面上,也显示有相关的信息。那么当我们自己定制错误页面时,SpringBoot肯定也给我们提供了这些数据供我们取用。甚至我们可以添加额外的信息!

关于这个问题,源码的切入点在哪呢?其实在上面一开始提及BasicErrorController时也提到了(见下图)。这次我们关注getErrorAttributes()这个方法,看名字也能猜到是啥意思。点进去。

跳转到BasicErrorController的基类 AbstractErrorController中(见下图),我们再点进入(下图划线部分的getErrorAttributes())。

来到ErrorAttributes这个接口。我们ctrl + H查看这个接口的体系结构。

果然,它有个实现类:DefaultErrorAttributes。是不是很眼熟?它也是上面提及到的4个关键组件中的一个。这里注意!!!SpringBoot里面有两个同名的DefaultErrorAttributes。我们要看的在org.springframework.boot.web.servlet.error包下。别搞错了!!!

5、来到DefaultErrorAttributes,看看它怎么共享错误信息,如何共享错误信息。

查看这个类,我们发现:Map里面存有以下信息:

timestamp:时间戳

status:状态码

error:错误提示

exception:异常对象

message:异常消息

errors:JSR303数据校验的错误都在这里

trace:栈信息

这些信息,其实我们可以在页面中,通过thymeleaf标签以变量形式取出来。示例如下:

这里注意一个小问题,我所使用的版本的SpringBoot默认没有办法取出Exception的信息。原因可以从源码中获知(这里我不深入展开了,我贴一张图作为切入点,大家可自行跟踪查看源码),为了解决这个问题,只需要在配置文件中添加以下配置:server.error.include-exception=true

原因:

四、自定义错误数据

到上面为止,我们知道了:

(1)如何定制自己的错误页面

(2)如何在自己定制的错误页面取用现有的错误信息

(3)了解了SpringBoot默认的错误处理机制的流程

相信各位对“二”中提及的几个关键的类,也有了更加深刻的了解。那么,现在我还想 自定义 错误数据。换言之,我想共享自己想要的错误信息。这个错误信息是我自定义的,由我加入到共享域中,然后又由我在自己的错误页面中取用!

出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的错误数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法)。我们想要添加自定义的错误信息,可以编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放入到容器中,完全替换掉SpringBoot的DefaultErrorAttributes组件。

代码如下:

package com.ysq.springbootweb.component;


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

import java.util.Map;

/**
 *
 * @author passerbyYSQ
 * @create 2020-07-29 16:16
 */
@Component // 给容器中加入我们自己的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    //  如果不复写,Exception又获取不到了
    public MyErrorAttributes() {
        super(true);
    }

    /**
     * 这里返回的Map,就是页面和Json数据能获取到的所有字段
     * @param webRequest
     * @param includeStackTrace
     * @return
     */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        // 额外信息
        map.put("company", "ysq");

        return map;
    }

}

到这里我们会遇到两个问题:

(1)如果不复写空参的构造方法,当前版本的SpringBoot又获取不到Exception信息了。关于Exception的获取,前面我也提到过了。但现在又是什么情况呢?

原本SpringBoot使用的是DefaultErrorAttributes,DefaultErrorAttributes里面默认是不把Exception信息放到共享域中的。而我们之前通过配置文件修改的是,允许DefaultErrorAttributes将Exception信息放到共享域中。而如今,我门是通过编写DefaultErrorAttributes的子类,并将它放入到容器中,将DefaultErrorAttributes这个组件完全替换掉。SpringBoot将不会使用DefaultErrorAttributes,而改为完全使用我们的自定义组件MyErrorAttributes。所以,配置文件的配置是无效的。所以,我们可以直接复写MyErrorAttributes的父类的空参构造,允许获取Exception信息。

这个问题是个小插曲。下面第(2)个问题才是重点。

(2)直接编写MyErrorAttributes,复写父类方法getErrorAttributes,在改方法返回结果之前,往Map中添加自定义的错误信息。这样会导致一个问题:无论什么错误,都会添加一模一样的错误信息。我想针对不同的错误,添加特有的信息。

比如:某个自定义的异常导致的500错误,添加其特有的信息,以供错误页面取用。

这个需求如何解决呢?见下面代码

package com.ysq.springbootweb.controller;

import com.ysq.springbootweb.exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

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

/**
 * @author passerbyYSQ
 * @create 2020-07-29 15:47
 */
@ControllerAdvice // 在SpringMVC要成为异常处理器,需要增加此注解
public class MyExceptionHandler {

    // 只要发生UserNotExistException异常,SpringMVC就调用该方法,并将异常对象对传递进来
    @ExceptionHandler(UserNotExistException.class) 
    public String handleException(Exception e, HttpServletRequest request) {

        Map<String, Object> map = new HashMap<>();

        // 需要传入我们自己的错误状态
        // 如果不传,就默认200(成功),就不会进入自己定制的错误页面的解析流程
        // 虽然有定制的页面效果了,但是我定制的Map数据不生效
        request.setAttribute("javax.servlet.error.status_code", 500);

        // 自定义的错误信息
        map.put("code", "user not exist");
        map.put("message", "用户不存在!!!!");
        // 将额外的信息添加到请求域中,以供MyErrorAttributes取出
        request.setAttribute("ext", map);

        // 转发到/error请求,让BasicErrorController处理
        return "forward:/error"; 
    }
}

同时MyErrorAttributes作下面改动:

package com.ysq.springbootweb.component;


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

import java.util.Map;

/**
 *
 * @author passerbyYSQ
 * @create 2020-07-29 16:16
 */
@Component // 给容器中加入我们自己的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    //  如果不复写,Exception又获取不到了
    public MyErrorAttributes() {
        super(true);
    }

    /**
     * 这里返回的Map,就是页面和Json数据能获取到的所有字段
     * @param webRequest
     * @param includeStackTrace
     * @return
     */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        // 额外信息
        map.put("company", "ysq");

        // 针对UserNotExistException异常的错误信息
        Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        if (ext != null) {
            map.put("ext", ext);
        }

        return map;
    }

}

我的页面效果:

看到这里的朋友,别忘了点赞收藏哦。谢谢!

猜你喜欢

转载自blog.csdn.net/qq_43290318/article/details/107669722