SpringBoot2.x系列教程34--整合SpringMVC之错误处理底层原理及源码分析

SpringBoot2.x系列教程34--整合SpringMVC之错误处理底层原理及源码分析

作者:一一哥

一. SpringBoot的默认错误处理策略

1. 对404的默认处理策略

我们在发送请求的时候,如果发生了404异常,SpringBoot是怎么处理的呢?

我们可以随便发送一个不存在的请求来验证一下,就会看到如下图所示:

2. 对500的默认处理策略

当服务器内部发生代码等错误的时候,会发生什么呢?

比如我们人为的制造一个异常出来,如下面的代码所示:

@GetMapping("/user/{id:\\d+}")
public User get(@PathVariable String id) {
    throw new RuntimeException();
}

结果产生了如下所示效果图:

3.在error页面中可以获取的错误信息

timestamp: 时间戳.
status: 状态码.
error: 错误提示.
exception: 异常对象.
message: 异常消息.
errors: 数据效验相关的信息.

二. Spring Boot错误处理机制探究

通过上面的两个案例,我们发现无论发生了什么错误,Spring Boot都会返回一个对应的状态码以及一个错误页面,那么这个错误页面是怎么来的呢?

要弄明白这个问题,我们需要从Spring Boot中错误处理的底层源码来进行分析。

1. SpringBoot的错误配置信息

SpringBoot的错误配置信息是通过ErrorMvcAutoConfiguration这个类来进行配置的,这个类中帮我们注册了以下组件:

DefaultErrorAttributes: 帮我们在页面上共享错误信息;
ErrorPageCustomizer: 项目中发生错误后,该对象就会生效,用来定义请求规则;
BasicErrorController: 处理默认的 ’/error‘ 请求,分为两种处理请求方式:一种是html方式,一种是json方式;
DefaultErrorViewResolver: 默认的错误视图解析器,将错误信息解析到相应的错误视图.

2. Spring Boot处理error的流程

  • 一旦系统中出现 4xx 或者 5xx 之类的错误, ErrorPageCustomizer就会生效(定义错误的相应规则);
  • 然后内部的过滤器就会映射到 ’/error‘ 请求,接着该/error请求就会被BasicErrorController处理;
  • 然后BasicErrorController会根据请求中的Accept来区分该请求是浏览器发来的,还是由其它客户端工具发来的.此时一般分为两种处理方式:errorHtml()和error().
  • 在errorHtml()方法中,获取错误状态信息,由resolveErrorView解析器解析到默认的错误视图页面,默认的错误页面是/error/404.html页面;
  • 而如果templates目录中的error目录里面有这个页面,404错误就会精确匹配404.html;
  • 如果没有这个404.html页面它就会模糊匹配4xx.html页面;
  • 如果templates中没有找到错误页面,它就会去static文件中找.

注:

static文件夹存放的是静态页面,它没有办法使用模板引擎表达式.

简单概括就是:

1️⃣. 当出现4xx或5xx的错误:ErrorPageCustomizer开始生效;

2️⃣. 然后ErrorPageCustomizer发起 /error 请求;

3️⃣. /error 请求被BasicErrorController处理;

4️⃣. BasicErrorController根据请求头中的Accept决定如何响应处理.

其实在以上的所有类中,最重要的一个类就是BasicErrorController,所以接下来我们来分析BasicErrorController源码,看看Spring Boot底层到底是怎么实现error处理的。

三. BasicErrorController源码详解

在Spring Boot中,当发生了错误后,默认情况下,Spring Boot提供了一个处理程序出错的结果映射路径 /error。当发生错误后,Spring Boot就会将请求转发到BasicErrorController控制器来处理这个错误请求。

1. BasicErrorController源码

所以我们重点分析BasicErrorController源码,首先呈上源码内容:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    private final ErrorProperties errorProperties;

    /**
     * Create a new {@link BasicErrorController} instance.
     * @param errorAttributes the error attributes
     * @param errorProperties configuration properties
     */
    public BasicErrorController(ErrorAttributes errorAttributes,
            ErrorProperties errorProperties) {
        this(errorAttributes, errorProperties,
                Collections.<ErrorViewResolver>emptyList());
    }

    /**
     * Create a new {@link BasicErrorController} instance.
     * @param errorAttributes the error attributes
     * @param errorProperties configuration properties
     * @param errorViewResolvers error view resolvers
     */
    public BasicErrorController(ErrorAttributes errorAttributes,
            ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorViewResolvers);
        Assert.notNull(errorProperties, "ErrorProperties must not be null");
        this.errorProperties = errorProperties;
    }

    @Override
    public String getErrorPath() {
        return this.errorProperties.getPath();
    }

    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        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);
    }

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

2. error.path分析

这个源码的注释信息中说明了,这是一个Spring Boot自带的全局错误Controller.

这个Controller中有一个RequestMapping注解,内部有一个相当于三元运算符的操作。如果你在配置文件配置了server.error.path的话,就会使用你配置的异常处理地址,如果没有就会使用你配置的error.path路径地址,如果还是没有,则默认使用/error来作为发生异常时的处理地址。

所以我们可以按照如下图中的配置,来设置自定义的错误处理页面。

3. errorHtml()与error()方法解析

从上面的源码我们可以看到,BasicErrorController中有两个RequestMapping方法,分别是errorHtml()与error()方法来处理错误请求。

那么为什么会是两个呢?请看下面的截图:

BasicErrorController内部是通过读取request请求头中的Accept属性值,判断其内容是否为text/html;然后以此来区分请求到底是来自于浏览器(浏览器通常默认自动发送请求头内容Accept:text/html),还是客户端,从而决定是返回一个页面视图,还是返回一个 JSON 消息内容。

可以看到其中errorHtml()方法是用来处理浏览器发送来的请求,它会产生一个白色标签样式(whitelabel)的错误视图页面,该视图将以HTML格式渲染出错误数据。

该方法返回了一个error页面,如果你的项目静态页面下刚好存在一个error所对应的页面,那么Spring Boot会得到你本地的页面,如下图:

而error()方法用来处理来自非浏览器,也就是其他软件app客户端(比如postman等)发送来的错误请求,它会产生一个包含详细错误,HTTP状态及其他异常信息的JSON格式的响应内容。

注:

BasicErrorController可以作为自定义ErrorController的基类,我们只需要继承BasicErrorController,添加一个public方法,并添加@RequestMapping注解,让该注解带有produces属性,然后创建出该新类型的bean即可。

4. /error映射默认处理策略总结

经过上面的源码分析得知,在默认情况下,Spring Boot为这两种情况提供了不同的响应方式.

4.1 响应'Whitelabel Error Page'页面

一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,这时候一般Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”:

4.2 响应JSON字符串

另一种是当我们使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的一个 JSON 字符串信息:

{
    "timestamp": "2019-05-20T06:12:45.209+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/login.html"
} 
发布了315 篇原创文章 · 获赞 84 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/syc000666/article/details/105141095