SpringBoot 异常处理机制详解

异常处理自动配置类 ErrorMvcAutoConfiguration

SpringBoot 异常处理的自动配置类是 ErrorMvcAutoConfiguration,本节将说明该自动配置类中配置了哪些重要组件,以及这些组件有什么作用。

概览

在深入介绍 ErrorMvcAutoConfiguration 所注册的组件前,这里先做一个概览,主要介绍其中一些重要的组件。

  • BasicErrorController 错误处理控制器:

    默认对 /error 错误请求进行处理并响应。

    SpringBoot 各式各样的异常处理,最后都是发送 /error 请求,从而通过 BasicErrorController 中的两个处理器来响应错误页面或者响应错误数据。除非你自定义了 ErrorController 类并注册到容器中。

  • DefaultErrorAttributes 默认错误属性:

    主要功能是规定了错误处理最后对外响应,应该响应哪些信息,比如规定了,要响应时间戳,状态码,错误信息,错误堆栈等,无论是响应错误页面还是响应错误数据。

  • DefaultErrorViewResolver 默认错误视图解析器:

    相应错误页面时,对错误视图名进行解析并得到错误视图的一种策略:返回错误码对应的视图,或者错误码对应的静态 html 页面。

  • StaticView 默认静态错误视图:

    响应错误页面时,错误视图名为 error 的情况下对应的最基本的默认错误视图。该视图渲染出来是一个白页。

  • BeanNameViewResolver Bean 命名视图解析器:

    响应错误页面时,对错误视图名进行解析并得到错误视图的一种策略:在容器中寻找 id 为错误视图名的组件,若找到检查该组件是否为视图,如果是就将其返回,解析完成。

    其实 BeanNameViewResolver 并不是一个错误视图解析器,但却主要用在错误视图解析的场景下,而且一般都是配合上面的 StaticView 来使用。因为一般来说,容器中只会存放 StaticView 这么一个名为 errorView 视图对象,从而可以被解析到。

BasicErrorController 错误处理控制器

BasicErrorController 是异常处理机制下,最核心最关键的组件。

一般情况下,我们不会自定义 ErrorControllerErrorAttributes 来全面接管 SpringBoot 的异常处理机制,所以以下自动配置将会执行:

1
2
3
4
5
6
7
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

自动配置会从容器中取出 ErrorAttributes,以及所有 ErrorViewResolver,来构造 BasicErrorController 并注册到容器中。

ErrorAttributes 一般就是自动配置类中配好的 DefaultErrorAttributes

另外,一般情况下,我们也不会自定义 ErrorViewResolver 来做异常处理的定制化,所以 ErrorViewResolver 一般就只有一个,且就是自动配置类中配好的 DefaultErrorViewResolver


SpringBoot 各式各样的异常处理,基本都是会发送 /error 请求,通过 BasicErrorController 来完成错误响应。

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

@RequestMapping 中的 ${server.error.path:${error.path:/error}} 是 SpEL 表达式,表达式计算过程是:先看 ${server.error.path} 是否有值,如果没有,再看 ${error.path} 是否有值,如果再没有,使用 /error 作为错误请求路径。


/error 请求,主要通过下面的两个处理器进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 多用于响应错误页面给浏览器
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
// 收集错误页面渲染时需要的错误信息,比如时间戳,状态码,错误信息,错误堆栈等
// 这里 model 中收集了哪些错误信息,默认是 DefaultErrorAttributes 中定义好的
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
// 设置响应状态码
response.setStatus(status.value());
// 进行错误视图解析,返回封装了错误视图/错误视图名以及包含错误信息 Model 的 ModelAndView
// 这里默认是 DefaultViewResolver 根据状态码进行视图解析
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 如果错误视图解析失败,返回 null,此时使用视图名为 error 的视图进行错误页面渲染
// 这里视图名为 error 的视图,默认是 StaticView,通过 BeanNameViewResolver 从容器中解析得到
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

// 多用于响应错误数据,如 JSON 数据给其他客户端
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
// 收集需要放到响应体中的错误信息,比如时间戳,状态码,错误信息,错误堆栈等
// 这里响应体中要放哪些错误信息,默认是 DefaultErrorAttributes 中定义好的
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
// 将错误信息和状态码一并放到响应中,响应体中的错误信息一般是转为 JSON 数据响应给其他客户端
return new ResponseEntity<>(body, status);
}

浏览器访问页面时,请求处理出错,此时会调用 errorHtml 处理器响应错误页面。
使用 Postman 发送页面请求时,请求处理出错,此时会调用 error 处理器响应错误数据。

这种不同的处理器调用是如何实现的?

关键就在于 errorHtml 处理器上方的 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)

如果 @RequestMappingproduces 属性提供了值,那么内容协商过程将会提前到请求映射阶段的查找处理器时就进行:SpringBoot 会从请求中获取客户端期望接收的媒体类型,检查是否和该 @RequestMappingproduces 属性指定的媒体类型兼容,如果兼容,且其他条件均匹配,才会将该 @RequestMapping 对应的处理器添加到成功匹配列表中。

下面结合源码举例说明。

假设浏览器或 Postman 发送请求 /basic_table,请求处理会出错,默认将调用 response.sendError 将请求转发到 /error。因为是请求转发,所以是同一个请求。

/error 的请求映射处理将经过以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// lookupPath="/error"  
// request=从 /basic_table 转发过来的请求
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 先根据请求路径 /error 匹配,找到两个直接匹配的 RequestMappingInfo,分别对应 errorHtml 和 error 两个处理器
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);

// 检查得到的两个 RequestMappingInfo 是否和请求匹配
// (1) error 对应的 @RequestMapping 能够和请求匹配
// (2) errorHtml 对应的 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) 在内容协商后,确定也能够和请求匹配
// 因此两个都会加入到匹配列表 matches 中
addMatchingMappings(directPathMatches, matches, request);

if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
// 找到多个匹配,因此会进行排序
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
// 排序时,会根据多个指标进行排序,其中一个指标就是:
// 请求中的期望接收的媒体类型,与当前 RequestMappingInfo 的 produces 属性指定的输出媒体类型(不指定则默认是 */*)的匹配度
// 请求是浏览器发送的场景下,请求期望接收 text/html,因此 errorHtml 对应的 RequestMappingInfo 的 text/html 更匹配,排序更高
// 请求是 Postman 发送的场景下,请求期望接收 */* 或 application/json,error 对应的 RequestMappingInfo 的 */* 更匹配,排序更高
matches.sort(comparator);
// 取出排序更高的匹配,作为最终选中的处理器返回
bestMatch = matches.get(0);
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
throw new IllegalStateException();
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
}

DefaultErrorAttributes 默认错误属性

某一请求处理过程中发生异常后,异常请求一般会转发到 /error,通过 BasicErrorController 响应错误页面或者响应错误数据。

响应的错误页面上或错误数据中,具体包含哪些错误信息,就是 DefaultErrorAttributes 来规定的。

一般情况下,我们不会自定义 ErrorAttributes,所以下面自动配置会执行:

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

当异常请求转发到 /error 后,就会调用 BasicErrorController 的两个处理器 errorerrorHtml 的其中一个进行错误响应处理。

处理过程中会调用注入到 BasicErrorController 内部的 DefaultErrorAttributesgetErrorAttributes 方法来获取要响应的错误信息。

1
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver {}

getErrorAttributes 其实是 ErrorAttributes 接口的方法。DefaultErrorAttributes 实现了该接口,我们来看下它对 getErrorAttributes 的具体实现,看看默认到底规定了要响应哪些错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
// 先调用另一个重载方法,拿到一个基本的 errorAttributes 错误信息集合
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));

// 根据条件再增添一些错误信息种类,或者删减一些已有的错误信息种类
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(Include.EXCEPTION);
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}

@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
// 基本的 errorAttributes 错误信息集合放入了:
// 时间戳 timestamp,状态码 status,错误细节 errorDetails,发生错误的请求路径 path
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}

回顾我们的错误白页,其中包含的时间戳,状态码,以及其他错误信息,都是 DefaultErrorAttributes 规定好并返回的。

我们再来审视 getErrorAttributes 方法,最终响应的错误信息还包括了异常的相关信息,但是 getErrorAttributes 的形参列表中,我们只是传入了请求和一个需要的错误信息种类,并没有传入异常,那 getErrorAttributes 是怎么拿到异常相关的信息的呢,如异常类型,异常堆栈等?

答案就包含在 addErrorDetails 方法中,其实也不难想到,是从传入的请求的请求域中拿到异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
boolean includeStackTrace) {
// 从请求中取出异常
Throwable error = getError(webRequest);
if (error != null) {
errorAttributes.put("exception", error.getClass().getName());
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
addErrorMessage(errorAttributes, webRequest, error);
}

@Override
public Throwable getError(WebRequest webRequest) {
// 具体是从请求的请求域中,以 ERROR_ATTRIBUTE 为键,取出异常
Throwable exception = getAttribute(webRequest, ERROR_ATTRIBUTE);
return (exception != null) ? exception : getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
}

这里明确一点,当前请求是 /error 请求,且这个请求是从前一个异常请求转发过来的。

所以不难想到,以 ERROR_ATTRIBUTE 为键,将前一个请求发生的异常放入请求域中的操作,肯定是在前一个异常请求转发到 /error 前完成的。具体是什么时候呢?

我们回到 DispatchServletdoDispatch 方法,这次我们重点关注请求异常情况下的处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// DispatchServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {

mappedHandler = getHandler(processedRequest);

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行目标方法,返回一个 ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
// 如果以上步骤中发生了异常,将异常先记录保存到 dispatchException
dispatchException = ex;
}
// 处理派发结果,对于发生异常的情况,此时:
// (1) 异常已经记录保存到 dispatchException 变量
// (2) 目标方法大概率还未被执行,因此 mv 大概率还未被赋值,保持初始化值 null
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
}

假设本轮请求是 /basic_table,执行目标方法时发生了异常,异常抛出后被保存到 dispatchException,此时 mvnull

接下来,我们深入 processDispatchResult 函数,看异常是如何处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

// 如果有异常
if (exception != null) {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 对异常进行处理,返回一个针对错误情况的 ModelAndView,并赋值给值为 null 的 mv
mv = processHandlerException(request, response, handler, exception);
}

if (mv != null && !mv.wasCleared()) {
// 如果有异常,这里的 mv 则是用于处理错误情况的 mv,一般是做错误视图的渲染
render(mv, request, response);
}

if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

接下来,继续深入看 processHandlerException 函数是怎么处理异常,并返回 ModelAndView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {

ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
// 遍历所有处理器异常解析器,看哪个处理器异常解析器可以解析异常
// (1) 返回一个 ModelAndView 就是能解析异常
// (2) 返回 null 就是不能解析异常,看下一个处理器异常解析器能否处理
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
// 将异常解析得到的 ModelAndView 返回
return exMv;
}

// 如果所有处理器异常解析器,都不能处理异常,也即都返回 null,则将异常抛出
// 此时该异常会一直抛到 Tomcat 底层,然后 Tomcat 底层调用 response.sendError() 转发到 /error,最后还是交给 BasicErrorController 来处理
// PS:默认情况下,其实就是所有的处理器异常解析器均不能处理异常!
throw ex;
}

我们看到了处理器异常解析器 HandlerExceptionResolver 的影子。

回顾 DefaultErrorAttributes 的类定义,可以发现,其实它实现了 HandlerExceptionResolver 接口。也就是说 DefaultErrorAttributes 是一个处理器异常解析器!

1
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver {}

DispatchServlet 中的 handlerExceptionResolvers 属性是一个 List<HandlerExceptionResolver>,默认情况下,列表中第一个处理器异常解析器就是 DefaultErrorAttributes

我们来看 DefaultErrorAttributesresolveException 实现:

1
2
3
4
5
6
7
8
9
10
11
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
storeErrorAttributes(request, ex);
return null;
}

private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
// 将异常以 ERROR_ATTRIBUTE 为键,放到请求域中
request.setAttribute(ERROR_ATTRIBUTE, ex);
}

可以看到,DefaultErrorAttributes 的异常解析就是把异常 ex 放到了请求域中,然后就直接返回一个值为 null

至此,我们终于确定了,处理 /error 时从请求域中取出的异常,其实是前一个异常请求处理流程中,调用默认的处理器异常解析器 DefaultErrorAttributes 进行异常解析时放入的。

DefaultErrorAttributesresolveException 总是返回 null,这说明它 存在的意义其实并不是为了解析异常,而是将异常放入请求域,方便转发到 /error 后还可以拿到前一个异常请求发生的异常。


最后简单介绍一下处理器异常解析器。

处理器异常解析器的 resolveException 工作是,对处理器调用执行过程中产生的异常进行处理,然后返回一个 ModelAndView 对象。

resolveException 方法常见的处理一般有两种:

  • 返回一个包含错误响应视图的 ModelAndView

    这种情况下,异常请求后续将直接渲染 ModelAndView 中的错误响应视图,不会再请求转发到 /error,再经过 BasicErrorController 来完成错误/异常响应。

  • 手工调用 response.sendError(status, message),最后返回一个 ModelAndView 壳对象,至少视图为空。

    这种情况下,异常请求后续会请求转发到 /error,交给BasicErrorController 来完成错误/异常响应。

    response.sendError() 并不立即停止当前请求的处理,它只是设置了一个标志,告诉 Servlet 容器这个请求已经结束,然后 Servlet 容器将会在适当的时机转发到错误页面,Tomcat 默认是转发到 /error

DefaultErrorViewResolver 默认错误视图解析器

一般情况下,我们不会自定义 ErrorViewResolver,所以下面自动配置会执行:

1
2
3
4
5
6
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}

来看 DefaultErrorViewResolver 的类定义:

1
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {}

DefaultErrorViewResolver 实现了 ErrorViewResolver 接口,是一个错误视图解析器。它提供了一种默认的错误视图解析策略:根据错误状态码进行错误视图解析,返回错误状态码对应的错误视图。

回过头来看 BasicErrorController 中的 errorHtml 处理器:

1
2
3
4
5
6
7
8
9
10
11
// 多用于响应错误页面给浏览器
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 进行错误视图解析,返回封装了错误视图/错误视图名以及包含错误信息 Model 的 ModelAndView
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

errorHtml 处理器方法中的 resolveErrorView 方法从命名上看是解析错误视图,应该和 DefaultErrorViewResolver 有关,深入看下其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 遍历所有错误视图解析器,看哪个错误视图解析器可以解析
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
// (1) 返回一个 ModelAndView 就是能解析得到错误视图
// (2) 返回 null 就是不能解析,看下一个错误视图解析器能否处理
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}

果然,在 resolveErrorView 方法内部,我们看到了 ErrorViewResolver 的影子。

BasicErrorController 中的 errorViewResolvers 属性是一个 List<ErrorViewResolver>,前面我们提到过,该属性是 BasicErrorController 在自动配置时,获取 Spring 容器中所有的 ErrorViewResolver 实现类来进行赋值的。

默认情况下,Spring 容器中只存在 DefaultErrorViewResolver 一个 ErrorViewResolver 的实现类,且我们一般也不自定义 ErrorViewResolver

所以 BasicErrorControllererrorViewResolvers 集合中,默认只包含一个错误视图解析器,且就是 DefaultErrorViewResolver

下面来看 DefaultErrorViewResolverresolveErrorView 具体实现,看看它是怎么进行错误视图解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;

static {
Map<Series, String> views = new EnumMap<>(Series.class);
// (1) 客户端错误,映射到 4xx:以 4 开头的状态码,如 404,如果找不到精确对应的视图,那么就会退而求其次,检查是否存在 4xx 对应的视图
views.put(Series.CLIENT_ERROR, "4xx");
// (2) 服务端错误,映射到 5xx:以 5 开头的状态码,如 500,如果找不到精确对应的视图,那么就会退而求其次,检查是否存在 5xx 对应的视图
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 根据精确状态码进行错误视图解析,返回一个 ModelAndView
// 比如状态码是 404,那么尝试解析得到 404 对应的错误视图
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
// 如果精确状态码解析失败,那么检查错误是否是客户端错误或服务端错误,也即错误状态码是否以 4 开头或以 5 开头
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 如果是的话,进行模糊状态码的错误视图解析
// 比如是客户端错误的话,尝试解析得到 4xx 对应的错误视图
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
}

具体的解析逻辑在 resolve 方法中,我们继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 将状态码与 error/ 拼接得到错误视图名,比如得到 error/404 或者 error/5xx 等
String errorViewName = "error/" + viewName;
// 检查 templates 下是否存在错误视图名对应的视图模板
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
// 如果存在,则直接将错误视图名和传入的 model 封装为 ModelAndView 返回
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 如果不存在,则额外检查静态资源下是否存在
return resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
// 遍历四个默认的静态资源文件夹
Resource resource = this.applicationContext.getResource(location);
// 每个文件夹中找是否有类似 error/404.html 的静态 html 页面
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
// 如果有,将对应的静态 html 页面资源封装为 HtmlResourceView,然后再和传入的 model 封装为 ModelAndView 返回
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}

StaticView 默认静态错误视图 + BeanNameViewResolver Bean 命名视图解析器

还是回过头来看 BasicErrorController 中的 errorHtml 处理器方法:

1
2
3
4
5
6
7
8
9
10
11
12
// 多用于响应错误页面给浏览器
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 如果错误视图解析失败,返回 null,那么最终直接返回一个视图名为 error 的 ModelAndView
// 这里视图名为 error 的视图,默认是 StaticView,在后续的视图解析环节通过 BeanNameViewResolver 从容器中解析得到
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

可以看到,其实 StaticViewBeanNameViewResolver 是配套使用,来为 DefaultErrorViewResolver 处理不了的情况进行兜底的,保证最后还是能响应出一个错误页面,这个错误页面渲染出来就是最开始提到的白页。

DefaultErrorViewResolver 如果解析成功,假设返回的 ModelAndView 中的 view 就是 error/404,那么使用 Thymeleaf 视图模板技术的情况下,视图解析环节就会选中使用 ThymeleafViewResolver 作为视图解析器来解析,得到对应 templates/404.htmlThymeleafView 对象。

DefaultErrorViewResolver 如果解析失败,返回的 ModelAndView 中的 view 就是 error,视图解析环节就会选中使用 BeanNameViewResolver 作为视图解析器来解析,从 Spring 容器中获取 id 为 errorView 对象,发现 StaticView 满足要求,就将其返回。

可以来简单看一下 StaticView 的渲染逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
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(getContentType());
}
// 拼接 html 标签和模型数据
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(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
// 最后写到响应体中
response.getWriter().append(builder.toString());
}

定制异常处理逻辑

上面我们已经说过一种异常处理定制,借助 DefaultErrorViewResolver 来实现自定义错误页:

  • 如果 templates 或静态路径下有精确的错误状态码页面,如 error/404.html,就显示精确的错误状态码页面
  • 否则,如果 templates 或静态路径下有模糊的错误状态码页面,如 error/4xx.html,就显示模糊的错误状态码页面

都没有的话,自动就是显示错误白页了。

SpringBoot 支持各式各样的异常处理定制,比如:

  • 你可以自己向容器中注入一个 id 为 errorView 对象,来替代兜底的错误白页,让渲染出来的页面更好看。
  • 你可以自定义 ErrorAttributes,或者修改 DefaultErrorAttributes,让它支持输出更多种类的错误信息。
  • 你可以自定义 ErrorViewResolver,或者修改 DefaultErrorViewResolver,让它可以额外支持 3xx 的模糊状态码。
  • 你甚至可以自定义 ErrorController,或者修改 BasicErrorController 的默认行为。

但一般来说,不太建议自定义 ErrorAttributesErrorViewResolver 以及 ErrorController


SpringBoot 异常处理定制里面,更推荐更常用的定制点是 HandlerExceptionResolver

上面讲解 DefaultErrorAttributes 时,我们提到了处理器异常解析器。我们提到 DispatchServlet 中有一个 List<HandlerExceptionResolver> handlerExceptionResolvers 属性,默认情况下该属性列表中第一个处理器异常解析器就是 DefaultErrorAttributes

我们都知道 DefaultErrorAttributes 本身不做异常解析,那么除它之外,handlerExceptionResolvers 中默认情况下还有哪些处理器异常解析器呢?

默认情况下,handlerExceptionResolvers 中包含两个处理器异常解析器,第一个是 DefaultErrorAttributes,第二个是 HandlerExceptionResolverComposite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {

// 默认包含三个处理器异常解析器
@Nullable
private List<HandlerExceptionResolver> resolvers;

@Override
@Nullable
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

if (this.resolvers != null) {
// 遍历处理器异常解析器,看哪个可以解析异常
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}

}

HandlerExceptionResolverComposite 见名知义,其实它是一个处理器异常解析器的组合,它内部的 resolvers 属性默认还包括三个处理器异常解析器:

  • ExceptionHandlerExceptionResolver:支持 @ControllerAdvice + @ExceptionHandler 处理全局异常
  • ResponseStatusExceptionResolver:支持 @ResponseStatus 处理自定义异常
  • DefaultHandlerExceptionResolver:处理 Spring 框架底层异常

前两个处理器异常解析器所支持的两种机制,就是 SpringBoot 异常处理为我们预留的扩展点/定制点,其中 @ControllerAdvice + @ExceptionHandler 处理全局异常的机制是最常用的。

另外,我们也可以自定义处理器异常解析器来进行异常处理定制:比如自定义实现 HandlerExceptionResolver 接口,然后将实现类以 @Component 方式注册到容器中,DispatchServlet 自动配置时,会将 Spring 容器中的所有 HandlerExceptionResolver 实现收集注入到内部属性 handlerExceptionResolvers 中。

以下三个定制点具体如何定制,这里就不做赘述了,雷丰阳老师的视频已经讲得挺清楚了:

  • @ControllerAdvice + @ExceptionHandler 处理全局异常
  • @ResponseStatus 处理自定义异常
  • 自定义处理器异常解析器

详情可以看:55、错误处理-【源码流程】几种异常处理原理