Spring MVC SpringBoot源码解析:异常请求处理流程;发送/error并返回error视图的详细原理
引言
一个错误请求的处理流程主要是这样的:
-
当发送一个错误请求时,spring mvc会尝试处理这个请求,比如尝试去寻找静态资源等等;
-
如果处理失败,会将错误信息保存,然后重新发送一个/error请求;
-
/error请求会被errorController处理,返回一个error视图;
本文主要通过源码分析异常请求处理的整个流程,对于springboot提供的BasicErrorController等解析error的组件不做过多描述;
SpringBoot默认处理规则
默认情况下,Spring Boot提供 “/error” 处理所有错误的映射(BasicErrorController);
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息;
对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据;
不论error html页面还是error json,能够得到的属性是一样的,有:
timestamp, status, error, exception, message, errors, trace, path
*异常请求处理流程
异常请求处理的流程主要分成四步:
- 解析错误请求被作为异常抛出
- 尝试处理错误请求
- 如果处理失败,会给底层response发送错误信息;然后再重新发送一个/error请求
- /error请求被ErrorController处理(SpringBoot帮我们自动配置了BasicErrorController)
步骤一:将错误请求作为异常抛出
当发送请求时,DispatcherServlet的doDispatch方法会寻找合适的Handler,然后通过Handler去找合适的Adapter去最终处理这个请求;
当发送错误请求时,寻找Handler过程就会抛出异常并被catch捕获;
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
捕获的异常会调用processDispatchResult尝试处理异常;
例:当发送一个405请求:Method Not Allowed,
部分栈轨迹:
handleNoMatch:250, RequestMappingInfoHandlerMapping (org.springframework.web.servlet.mvc.method)
lookupHandlerMethod:417, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
getHandlerInternal:364, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
getHandlerInternal:123, RequestMappingInfoHandlerMapping (org.springframework.web.servlet.mvc.method)
getHandlerInternal:66, RequestMappingInfoHandlerMapping (org.springframework.web.servlet.mvc.method)
getHandler:491, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:1255, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1037, DispatcherServlet (org.springframework.web.servlet)
doService:961, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:626, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:733, HttpServlet (javax.servlet.http)
最终handleNoMatch方法抛出HttpRequestMethodNotSupportedException:
@Override
protected HandlerMethod handleNoMatch(
Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
PartialMatchHelper helper = new PartialMatchHelper(infos, request);
if (helper.isEmpty()) {
return null;
}
if (helper.hasMethodsMismatch()) {
Set<String> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
HttpOptionsHandler handler = new HttpOptionsHandler(methods);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
}
步骤二:尝试处理异常
抛出的异常都会被DispatcherServlet.doDispatch的catch捕获到,然后带着这个异常信息调用processDispatchResult方法:
这个方法首先会尝试处理异常信息去获取ModelAndView对象:
该方法的主要逻辑:
- 寻找能够处理异常的对象:如果是ModelAndView定义异常,会返回一个特殊的视图(表明视图定义异常);否则,说明是handler的异常,会调用processHandlerException处理(具体看下面该方法的处理流程);
- 如果最终成功处理了异常,会得到ModelAndView对象,然后将视图渲染,处理完成;否则抛出异常。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 将得到的视图渲染
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
//渲染完成,将异常处理器DefaultErrorAttributes中加入request域的错误信息清除
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
}
processHandlerException会遍历所有handler异常的解析器去尝试处理这个异常:
(DefaultErrorAttributes和HandlerExceptionResolverComposite);
最终返回的是ModelAndView对象,如果返回的非空,说明异常处理成功了;
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// 寻找注册的HandlerExceptionResolvers去解析handlerException
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
//HandlerExceptionResolver尝试解析异常,如果能返回MV对象说明成功解析
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
//如果得到了ModelAndView对象,但该对象的view和model都是空
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
解析handlerException的过程:
- 首先会由DefaultErrorAttributes处理异常,它并不会真正处理异常,只是把异常信息放到request域中;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);
}
- 然后由HandlerExceptionResolverComposite处理异常,该对象由三个HandlerExceptionResolver组成,见上图;如果所有的都不能处理这个异常,这个异常就会抛出;
步骤三:对无法解决的请求异常给响应(response)写error信息,然后底层重新发一个/error请求
- 结束无法处理的异常请求:假设processHandlerException方法最终无法处理该请求异常,那么就会调用response.sendError给响应写错误信息,并最终挂起本次响应(setSuspended)从而结束请求;
public class Response implements HttpServletResponse {
@Override
public void sendError(int status, String message) throws IOException {
//如果是已经提交的请求或者是servlet内部的请求,就不需要发送错误信息
if (isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteResponse.sendError.ise"));
}
if (included) {
return;
}
setError();
getCoyoteResponse().setStatus(status);
getCoyoteResponse().setMessage(message);
//清空数据缓存并挂起响应
// Clear any data content that has been buffered
resetBuffer();
// Cause the response to be finished (from the application perspective)
setSuspended(true);
}
- 重新发送一个/error请求:ApplicationFilterChain的invoke方法:
- 先将该请求响应对的挂起取消:response.setSuspended(false);
- 然后调用status方法重新生成一个/error请求;
status(request, response):将指定请求生成的http状态码和相关的信息进行处理,从而产生特定的响应;
status方法中将原请求改为/error并给request添加之前解析得到的错误信息:
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, errorPage.getLocation());//将请求改为/error
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,DispatcherType.ERROR);
...
然后,/error请求又会经过doDispatch去寻找能够映射这个请求的controller;默认的,这个controller就是springboot帮我们配置的BasicErrorController;
步骤四:/error请求被BasicErrorController映射并处理
若没有重写ErrorController方法并注册到容器中,调用默认的(springboot帮忙配置的)BasicErrorController;该类只有两个处理请求(带@RequestMapping)的方法:
-
如果浏览器发送的请求,会被errorHTML方法处理(produces = MediaType.TEXT_HTML_VALUE表示返回值类型是text/html);
-
如果是机器客户端,就会被**error(HttpServletRequest request):ResponseEntity<Map<String, Object>>**方法处理;
errorHTML主要做了两件事:
- 该方法会将request的错误信息封装到model中;然后去解析错误视图;
- 解析错误视图会调用错误视图解析器完成,默认的只有一个:DefaultErrorViewResolver;
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //MediaType.TEXT_HTML_VALUE=text/html
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);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//解析错误视图会通过错误视图解析器来完成
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//this.errorViewResolvers = DefaultErrorViewResolver
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
DefaultErrorViewResolver解析错误视图主要的流程是:
-
resolveErrorView方法首先得到错误的http状态码,并根据状态码名称调用resolve方法:
-
resolve方法去/templates/error下面寻找名称与状态码对应的模板,如404.html,如果有就会将其渲染成视图,返回ModelAndView对象;
-
如果没有,再调用resolveResource去静态资源目录下寻找:
“classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”
-
若仍然没有,resolve方法最终返回ModelAndView为null;
-
-
当ModelAndView为null,再去验证错误代码是否是4xx或5xx,然后再按照上面resolve方法的逻辑去找是否有4xx.html或5xx.html;
-
如果仍然没找到,返回null;
综上所述,错误视图解析器首先会去/templates/error(和静态资源目录)下找精确匹配的模板(404.html,405.html, …);如果没找到,再去找模糊匹配的,如状态码是405就去找4xx.html;如果最后还是没有,返回null。
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
//当没有找到名称完全相符的,如(404.html),再去找是否有4xx.html和5xx.html的错误模板
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { //Series_VIEWS: "4xx","5xx"
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
//如果在/templates/error下找到了模板名称与错误名相同的,这里的TemplateAvailabilityProvider就是thymeleaf模板引擎;否则就是null
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//resolveResource方法会去静态资源目录下寻找;
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resources.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;
}
当DefaultErrorViewResolver返回的ModelAndView对象为null且没有其他错误视图解析器时,意味着不存在我们自定义的错误视图;BasicErrorController的errorHtml方法最终会创建默认的error视图:new ModelAndView(“error”, model);
而名为error的视图springboot在ErrorMvcAutoConfiguration已经自动配置好了(whitelabel)。