常请求处理流程 发送/error请求以及得到error视图的详细原理

Spring MVC SpringBoot源码解析:异常请求处理流程;发送/error并返回error视图的详细原理

引言

一个错误请求的处理流程主要是这样的:

  1. 当发送一个错误请求时,spring mvc会尝试处理这个请求,比如尝试去寻找静态资源等等;

  2. 如果处理失败,会将错误信息保存,然后重新发送一个/error请求;

  3. /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

*异常请求处理流程

异常请求处理的流程主要分成四步:

  1. 解析错误请求被作为异常抛出
  2. 尝试处理错误请求
  3. 如果处理失败,会给底层response发送错误信息;然后再重新发送一个/error请求
  4. /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对象:

该方法的主要逻辑:

  1. 寻找能够处理异常的对象:如果是ModelAndView定义异常,会返回一个特殊的视图(表明视图定义异常);否则,说明是handler的异常,会调用processHandlerException处理(具体看下面该方法的处理流程);
  2. 如果最终成功处理了异常,会得到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的过程:

  1. 首先会由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);
}
  1. 然后由HandlerExceptionResolverComposite处理异常,该对象由三个HandlerExceptionResolver组成,见上图;如果所有的都不能处理这个异常,这个异常就会抛出;

步骤三:对无法解决的请求异常给响应(response)写error信息,然后底层重新发一个/error请求

  1. 结束无法处理的异常请求:假设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);
	}
  1. 重新发送一个/error请求:ApplicationFilterChain的invoke方法:
    1. 先将该请求响应对的挂起取消:response.setSuspended(false);
    2. 然后调用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主要做了两件事:

  1. 该方法会将request的错误信息封装到model中;然后去解析错误视图;

在这里插入图片描述

  1. 解析错误视图会调用错误视图解析器完成,默认的只有一个: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解析错误视图主要的流程是:

  1. resolveErrorView方法首先得到错误的http状态码,并根据状态码名称调用resolve方法:

    1. resolve方法去/templates/error下面寻找名称与状态码对应的模板,如404.html,如果有就会将其渲染成视图,返回ModelAndView对象;

    2. 如果没有,再调用resolveResource去静态资源目录下寻找:

      ​ “classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”

    3. 若仍然没有,resolve方法最终返回ModelAndView为null;

  2. 当ModelAndView为null,再去验证错误代码是否是4xx或5xx,然后再按照上面resolve方法的逻辑去找是否有4xx.html或5xx.html;

  3. 如果仍然没找到,返回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)。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值