底层原理分析:探究SpringBoot底层对异常的处理机制

一. 故事背景

最近文哥带的学员又要毕业了,其中有个学员在面试时被面试官问到,SpringBoot底层是如何进行异常处理的

对于底层原理性的东西,文哥认为在学会应用的基础上,适当的去刨根问底,扒拉一下底层,这对提升自己的技术功底是很有好处的。好啦,废话不多说,今天文哥就小试牛刀,带带大家通过对SpringBoot中普通的异常处理,来探究一下SpringBoot底层是如何对异常进行处理的。

二. SpringBoot异常处理的几个核心组件

我们要想搞清楚SpringBoot如何进行异常处理,首先要清楚SpringBoot进行异常处理需要用到哪几个组件,以及这些组件的作用是什么。接下来文哥就带你去慢慢分析。

首先我们可以找到SpringBoot错误处理的自动配置类ErrorMvcAutoConfiguration。

在ErrorMvcAutoConfiguration里面有几个组件我们需要关注一下。

1. DefaultErrorAttributes源码剖析

1.1 DefaultErrorAttributes简介

我们发现ErrorMvcAutoConfiguration的内部,会向容器注入一个id为errorAttributes的DefaultErrorAttributes类型的组件,那么该组件到底有啥用?我们可以进入源码看看。

1.2 DefaultErrorAttributes中的方法

我们再看看DefaultErrorAttributes这个类里面有哪些方法:

getErrorAttributes方法定义了错误信息,并将错误信息封装到了一个名为errorAttributes的Map集合里面,我们处理错误页面时就可以将这些数据取出并展示出来。

2. BasicErrorController源码剖析

2.1 BasicErrorController组件

在ErrorMvcAutoConfiguration里面还有一个组件BasicErrorController。

我们可以看看BasicErrorController的具体描述。

我们发现,当我们要进行错误处理的时候,SpringMVC会默认发送一个/error的请求进行相应的错误处理。

2.2 其他方法

我们再看看这个类里面的其他方法:

我们发现有两个方法,对错误信息进行了响应。

  • errorHtml:当我们使用浏览器发送请求的时候,如果出现了错误,就使用这个方法进行错误信息响应,默认响应的是一个名为error的视图。

  • error:当我们使用其他的方式(比如postman)发送请求的时候,我们就以json格式进行数据响应。默认将数据封装到ResponseEntity里面。

3. BeanNameViewResolver源码剖析

3.1 BeanNameViewResolver简介

在ErrorMvcAutoConfiguration里面还有一个组件BeanNameView Resolver。

3.2 error视图

我们看到BeanNameViewResolver是根据视图名称进行解析视图,解析一个名为error的视图。那这个error的视图是什么样的?我们发现源码中return了this.defaultErrorView。

我们点击进入StaticView类中,发现这样一段代码:

private static class StaticView implements View {
    private static final MediaType TEXT_HTML_UTF8;
    private static final Log logger;

    private StaticView() {
    }

    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (response.isCommitted()) {
            String message = this.getMessage(model);
            logger.error(message);
        } else {
             response.setContentType(TEXT_HTML_UTF8.toString());
             StringBuilder builder = new StringBuilder();
             Object timestamp = model.get("timestamp");
             Object message = model.get("message");
             Object trace = model.get("trace");
             if (response.getContentType() == null) {
                response.setContentType(this.getContentType());
             }

             builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
             if (message != null) {
                builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
             }

             if (trace != null) {
                builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
             }

             builder.append("</body></html>");
             response.getWriter().append(builder.toString());
        }
    }
}

这个render方法就是进行错误页面的具体渲染的地方。

3.3 render方法

到这里我们就清楚了,BasicErrorController里面使用errorHtml方法响应错误视图的原理,就是去寻找一个名为error的视图进行解析。而error视图是StaticView类型的,通过里面的render方法进行错误视图渲染的。我们可以看看我们平时出现错误的视图:

4. DefaultErrorViewResolver源码剖析

4.1 DefaultErrorViewResolver简介

在ErrorMvcAutoConfiguration里面还有一个组件DefaultError ViewResolver。

我们可以点击进入DefaultErrorViewResolver。

4.2 resolve()方法

这个类里面还有一个处理错误视图的方法:

这就回答了为什么我们定义错误页面的时候,为什么要以错误状态码命名,并且将错误页面放在error目录的原因。

到这里,文哥就把SpringBoot进行错误处理的几个主要的组件介绍清楚了,下一节我们就来详细介绍这几个组件是如何协同工作的。

三. SpringBoot异常处理核心流程

上一节中,我们讨论了SpringBoot异常处理所需要的几个核心组件。在接下来的这一节中,我们再来讨论一下SpringBoot进行异常处理的主要流程。

1. 启动debug模式

我们可以先启动debugg模式,然后分别在指定的位置打上断点。

2. 执行请求

然后发送请求:http://localhost:8080/findById?id=1,这里将断点放行到mv = ha.handle

3. 继续调试

我们继续放行断点,如下图所示:

按下F8继续放行,如下图所示:

我们发现,如果我们的目标方法出现了异常,会进入catch语句被捕获处理,并且目标方法的返回值ModelAndView为Null。这里我们继续按F8,会如下图所示:

4. 断点DispatcherServlet

我们在DispatcherServlet类上面的this.processDispatchResult方法上打断点,并将断点放行。

此时发现,不管目标方法有没有出现异常,总会被processDis patchResult方法执行(也就是进行视图处理的方法)。我们看它是如何处理的,执行step into,如下图所示:

我们发现,虽然我们的目标方法出现了异常,返回值mv为null。但是在this.processHandlerException处理目标方法的异常的时候,仍然将处理结果返回成ModelAndView。

那么这是如何处理的呢?我们继续step into:

5. 异常处理器工作流程

我们看看有哪些异常处理器参与工作。

我们发现有两类异常处理器参与处理工作。一个是Default ErrorAttributes,上一节我们介绍过这个异常处理器,它是SpringBoot默认配置的异常处理器。还有一类是组合异常处理器,有分为三个处理器。我们现在分别使用这4个异常处理器进行处理:

我们发现所有异常处理器处理之后,都会返回一个ModelAnd View。首先我们看看默认异常处理器DefaultErrorAttri butes如何进行异常处理的,在resolver.resolveException上面step into:

我们发现这个方法将错误信息封装之后,返回了一个null值。也就是使用DefaultErrorAttributes异常处理器处理之后,处理的结果为Null。

那么this.storeErrorAttributes又是如何封装异常信息的呢?继续step into:

我们发现就是将异常信息封装到request作用域。按F8,将代码放行到:

我们看看HanlderExceptionResolverComposite如何处理的?执行step into:

这3个异常处理器我们刚刚看到过,如下图所示:

通过上面代码我们看出也就是分别遍历:

  • ExceptionHandlerExceptionResolver(方法上标注了@Exception Handler注解才解析)

  • ReponseStatusExceptionResolver(方法上标注了@ReponseStatus注解才解析)

  • DefaultHandlerException Resolver帮助我们进行异常处理。具体的处理逻辑我们不看了

需要注意的是,这些异常处理器都实现了HandlerException Resolver接口:

结论:这个三个异常处理器最后处理的结果都为Null,也就是ModelAndView为null。

6. error方法的处理过程

此时我们放行断点:

我们发现当所有的错误异常处理器都不能处理的时候(目标方法的返回值ModelAndView为null),此时默认会转发到/error请求。我们把断点放行到如下位置:

观察目前处理的是哪个Handler:

7. errorHtml的处理过程

我们发现目前请求的是BasicErrorController里面的errorHtml方法。

BasicErrorController是什么?我们上一节讨论过这个类。里面分别定义了响应html错误页面和json信息的方法。我们断点执行到如下位置:

我们在BasicErrorController里面的errorHtml方法打上断点,继续放行断点:

此时会进入BasicErrorController里面的errorHtml方法。

我们发现在执行this.resolveErrorView方法之前,将所有的错误信息放在了一个Map里面,我们可以看看:

我们step into进入this.resolveErrorView方法:

我们发现和之前一样,还是遍历所有的异常处理器进行异常处理,那么到底有哪些异常处理器呢?

我们发现只有一个异常处理器,那就是DefaultErrorViewResolver。而这个错误视图解析器我们上一节给大家介绍过。

8. 错误视图的解析过程

我们看看它是如何进行错误视图解析的,如下图所示:

我们step into进入resolver.resolveErrorView方法。

至此,文哥就把SpringBoot默认的异常处理流程给大家介绍完了,希望大家能够从里面汲取精华,对整个SpringBoot的异常处理流程及原理能有深刻的理解。

*威哥Java学习交流Q群:691533824
加群备注:CSDN推荐


版权声明:本文为finally_vince原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。