SpringBoot的异常处理流程是什么样的?
作者:star_chao 时间:2021-07-09 17:54:40
一、默认异常处理机制
默认情况下,SpringBoot 提供 /error
请求,来处理所有异常的。
1.浏览器客户端,请求头里的属性是Accept:text/html
。表明它想要一个html类型的文本数据。因此返回的错误视图以HTML格式呈现,也就是响应一个“ whitelabel”错误视图。
2.如果是其他客户端,请求头里的属性是Accept:/
,默认响应一个json数据 。
二、异常处理流程
介绍异常处理流程前,要先认识HandlerExceptionResolver
:处理器异常解析器接口,可以将异常映射到相应的统一错误界面,从而显示用户友好的界面(而不是给用户看到具体的错误信息)
public interface HandlerExceptionResolver {
//解析处理异常
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
在DispatcherServlet
初始化时,已经把所有的HandlerExceptionResolver
处理器异常解析器接口的实现类放到下面的集合里了
public class DispatcherServlet extends FrameworkServlet {
//异常解析器集合
private List<HandlerExceptionResolver> handlerExceptionResolvers;
}
从上图可以看出该集合里有DefaultErrorAttributes
;还有HandlerExceptionResolverComposite
处理器异常解析器组合,这里面包含了三个能真正处理异常的解析器,分别是ExceptionHandlerExceptionResolver
、ResponseStatusExceptionResolver
、DefaultHandlerExceptionResolver
。下面会介绍他们几个分别用于处理什么异常。
阅读doDispatch
()方法的源码可以看出,Spring MVC对整个doDispatch
()方法用了嵌套的try-catch
语句
内层的try-catch用于捕获
HandlerMapping
进行映射查找HandlerExecutionChain
以及HandlerAdapter
执行具体Handler
时的处理异常,并将异常传入到processDispatchResult
(processedRequest, response, mappedHandler, mv,dispatchException)方法中。外层
try-catch
用于捕获渲染视图时的异常。通过两层嵌套的
try-catch
,SpringMVC就能够捕获到三大组件在处理用户请求时的异常,通过这样的方法能够很方便的实现统一的异常处理。
//处理分发结果
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
//判断HandlerMapping、HandlerAdapter处理时的异常是否为空
if (exception != null) {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//异常不为空,处理异常,进行异常视图的获取
mv = processHandlerException(request, response, handler, exception);
}
//只要存在mv就进行视图渲染
if (mv != null && !mv.wasCleared()) {
//不管视图是正常视图还是异常视图,均进入视图渲染流程
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
...略
...略
...略
}
从上面代码可以看出,当出现异常时会进入processHandlerException
()方法进行异常视图的获取,处理完成后返回是ModelAndView
对象。接下来不管视图是正常视图还是异常视图,只要ModelAndView
不为空,均进入视图渲染流程。下面是如何进行进行异常视图的获取的代码。
//处理异常,进行异常视图的获取
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
//遍历所有的处理器异常解析器handlerExceptionResolvers,看谁能够处理当前异常
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
//解析当前异常
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
//ModelAndView 不为空时,异常视图获取成功,跳出方法,进行异常视图渲染。
break;
}
}
...略
...略
...略
//所有处理器异常解析器都不能处理该异常,抛出异常
throw ex;
}
遍历所有的处理器异常解析器handlerExceptionResolvers
,看谁能够处理当前异常
DefaultErrorAttributes
先来处理异常。把异常信息保存到request域,并且返回null,并不能真正解析。HandlerExceptionResolverComposite
会遍历它包含的三个异常解析器处理异常ExceptionHandlerExceptionResolver
处理器异常解析器支持@ControllerAdvice
+@ExceptionHandler
处理全局异常ResponseStatusExceptionResolver
处理器异常解析器支持@ResponseStatus
+自定义异常DefaultHandlerExceptionResolver
处理器异常解析器支持Spring底层的异常
当没有任何异常解析器能够处理异常,异常就会被抛出,最终Tomcat会发送 /error
请求,映射到底层的BasicErrorController
进入默认的异常处理机制。
总结:
当发生异常时,会被catch
。遍历所有的处理器异常解析器,看谁能够解析。如果你使用了@ControllerAdvice
+@ExceptionHandler
配置了全局异常处理,并指定了错误视图,那么该异常会被处理,然后进入视图渲染流程。如果该异常没能够被任何处理器异常解析器处理,就会抛出异常,由Tomcat发送/error
请求,进入默认的异常处理机制,也就是开头说的,没有配置错误状态码页面,则返回默认我们常见的默认错误页。
访问的是不存在的路径,此时不会发生异常,经过处理器映射,处理器适配调用仍然返回的是空的ModelAndView,所以无法进行视图渲染。Tomcat仍会发送 /error
请求,进入默认的异常处理机制。
三、默认的异常处理机制
要想弄懂错误处理原理,首先得看**ErrorMvcAutoConfiguration
:这是错误处理的自动配置类**,给容器中添加了下面几个非常重要的组件。
ErrorPageCustomizer
BasicErrorController
DefaultErrorViewResolver
DefaultErrorAttributes
首先我们看ErrorPageCustomizer
组件,此组件是一个静态内部类,位于ErrorMvcAutoConfiguration
内。它实现了ErrorPageRegistrar
接口,该接口提供了可以用来注册ErrorPage
的方法。官方将ErrorPage
描述为:简单的服务器独立的错误页面抽象,大致相当于web.xml
中传统的元素<error-page>
。ErrorPage
里包含了状态码、异常、和错误控制器映射路径(server.error.path=/error)。也就是说当发生了异常,而且所有的处理器异常解析器都处理不了该异常,Tomcat就会发送/error
请求映射到BasicErrorController
。
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
//关于服务器的一些配置,如端口号,编码方式等。在这里主要关注server.error.path
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
//注册错误页面
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//ErrorPage里包含了状态码、异常、和错误控制器映射路径
ErrorPage errorPage = new ErrorPage(
//this.properties.getError().getPath()获取的就是server.error.path=/error(默认)
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
//注册ErrorPage
errorPageRegistry.addErrorPages(errorPage);
}
}
====================DispatcherServletPath接口=================================
public interface DispatcherServletPath {
default String getRelativePath(String path) {
String prefix = getPrefix();
if (!path.startsWith("/")) {
path = "/" + path;
}
return prefix + path;
}
}
下面介绍的就是BasicErrorController
。它里面有两个重要的方法,正好对象开头说的默认处理机制。方法一:如果是浏览器请求,则返回HTML响应数据text/html
,方法二:如果是其他客户端请求,则返回JSON响应数据。
@Controller
//server.error.path 为空则用error.path, 再为空,再用/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//1、产生html类型的数据;浏览器客户端发送的请求来到这个方法处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//状态码
HttpStatus status = getStatus(request);
//从DefaultErrorAttributes里获取可以在页面显示的数据
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//调用父类里的方法,寻找错误视图解析器,来解析错误视图
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//!!!如果没有配置具体的状态码错误页面或4xx,5xx这种视图,视图解析不成功,就会返回空的ModelAndView对象。此时就会构造一个默认的error错误视图,通过BeanNameViewResolver视图解析器,根据视图名(error)作为组件id去容器中找到View对象
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//2、产生json数据,其他客户端来到这个方法处理
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
========================AbstractErrorController================================
protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
//错误视图解析器获取错误视图,返回ModelAndView
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
总结一下,BasicErrorController
主要作用:
处理默认/error
路径的请求
调用DefaultErrorViewResolver
进行错误视图解析,分为三种情况
1.模板引擎支持解析,就去 /templates/error/
下寻找我们配置的 状态码错误页面,例如404.html 或4xx.html。
2.模板引擎找不到这个错误页面,就去静态资源文件夹【/resources/、/static/、/public/、/META-INF/resources/】下的error文件夹下寻找状态码错误页面。
3.静态资源文件夹下也找不到,则new ModelAndView("error", model)
构造一个默认的错误视图【就是经常见到的 Whitelabel Error Page】。该默认的错误视图在ErrorMvcAutoConfiguration
里已经注册到容器里了,并且它在容器中的id就是error。后面就会通过BeanNameViewResolver
视图解析器,根据视图逻辑 error,作为组件id去容器中就可以找到默认的错误视图。
=========================ErrorMvcAutoConfiguration========================================
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
//向容器种注册默认的错误视图,id为error
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
}
//可以看到静态内部类StaticView,进行视图渲染的时候,构造了我们经常看到的默认错误页面Whitelabel Error Page。
private static class StaticView implements View {
@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());
}
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
,主要就是进行错误视图解析。如果发生错误,就会以HTTP的状态码 作为视图地址,找到真正的错误页面。但是注意,首先是精确查找具体的错误状态码页面,然后是按照4xx,5xx这种查找。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
//解析错误视图,要去的错误页面
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//先精确查找错误视图
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
//精确查找不到,则查找4xx,5xx这种类型的错误页面
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
//真正解析 viewName是状态码
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//!!!!默认SpringBoot可以去找到一个页面 error/404,注意定制错误页面要放在error文件夹下,如error/4xx.html
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}
//模板引擎不可用,就去静态资源文件夹下寻找
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//这里遍历的是"classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
}
最后介绍DefaultErrorAttributes
,里面存放了错误页面能够显示的数据。比如状态码、错误提示、异常消息等。
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
//帮我们在页面共享信息
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
}
总结:
当发生了异常,而且所有的处理器异常解析器都处理不了该异常,
ErrorPageCustomizer
就会生效(定制错误的响应规则)。Tomcat就会发送/error
请求,然后被HandlerMapping
映射到BasicErrorController
处理。解析错误视图:前提是配置了4xx.html、5xx.html错误状态码页面,去哪个状态码错误页面就由
DefaultErrorViewResolver
解析得到。如果没有配置错误状态码页面,就是默认的错误视图StaticView
,它是位于ErrorMvcAutoConfiguration
里的一个静态内部类,被自动注册到容器中。后面进行视图渲染的时候,就是StaticView
里的render
()方法构造了我们经常看到的默认错误页面【Whitelabel Error Page】。提取数据:页面能够获取什么数据是由
DefaultErrorViewResolver
设置的。
四、自定义异常处理
1、自定义异常处理页
有模板引擎的情况下
没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找。依然要将错误页面放在error文件夹下。
error/状态码.html,就是将错误页面命名为
状态码.html
放在模板引擎文件夹里面的 error文件夹下;我们可以使用4xx
和5xx
作为错误页面的文件名来匹配这种类型的所有错误,不过优先寻找精确的状态码.html。以上都没有找到错误页面,就是默认来到SpringBoot默认的错误提示页面。
错误页面能获取的信息DefaultErrorAttributes
:
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
2、@ControllerAdvice+@ExceptionHandler处理全局异常
底层是由 ExceptionHandlerExceptionResolver
处理器异常解析器支持的
3、@ResponseStatus+自定义异常
底层是由 ResponseStatusExceptionResolver
处理器异常解析器支持的,但是它解析完成后,调用了 **response.sendError(statusCode, resolvedReason);**由tomcat发送的/error请求,进入默认异常处理机制。
Spring底层的异常
如 参数类型转换异常;由DefaultHandlerExceptionResolver
处理器异常解析器支持的,处理框架底层的异常。但是它解析完成后,调用了 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); 由tomcat发送的/error请求,进入默认处理机制。
扩展:【可不看】
自定义处理器异常解析器、错误视图解析器:
实现 HandlerExceptionResolver
接口自定义处理器异常解析器;可以作为默认的全局异常处理规则
实现ErrorViewResolver
自定义错误视图解析器
来源:https://blog.csdn.net/qq_40949465/article/details/117716543