DispatcherServlet 请求处理主逻辑 : 4. 处理 Handler 执行结果

本文深入探讨SpringMVC框架中DispatcherServlet如何处理异常,包括通过HandlerExceptionResolver解析异常到ModelAndView,以及如何渲染视图。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列上一篇 : DispatcherServlet 请求处理主逻辑 : 3. 通过 HandlerAdapter 执行 Handler

经过DispatcherServlet请求处理主逻辑#doDispatch以上各个步骤的执行(Handler选择和执行),现在请求已经被处理。处理分为两种情况 :

  1. 处理过程没有遇到错误/异常,处理结果为一个ModelAndView mv;
  2. 处理过程遇到错误/异常,该错误/异常被统一为一个Exception dispatchException;

以上两种情况不可能同时出现,要么是第一种,要么是第二种。针对这两种情况,DispatcherServlet做了统一的处理,具体的调用语句如下 :

// DispatcherServlet#doDispatch 代码片段
// 参数介绍 :
// 1. processedRequest 是正在处理中的异常
// 2. response 是响应对象
// 3. mappedHandler 是处理当前请求的 Handler, 以 HandlerExecutionChain 形式存在
// 4. mv 是以上步骤无异常时执行 Handler 处理请求的结果
// 5. dispatchException 时以上步骤遇到异常时异常对象
// 注意 : 
// 1. 这里 mv 和 dispatchException 不会同时有值,至少有一个是 null
// 2. 这里 mv 和 dispatchException 也可能都是 null, 比如控制器方法使用了 @ResponseBody 注解直接返回了 JSON/XML数据,
//    而无需视图渲染的情况
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

该过程主要是消费之前Handler处理请求的结果mv/dispatchException,渲染相应的视图,开发人员指定的视图,缺省视图,或者错误视图,并将最终结果写入到响应对象中去。

该过程对应方法processDispatchResult实现如下 :

	/**
	 * Handle the result of handler selection and handler invocation, which is
	 * either a ModelAndView or an Exception to be resolved to a 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) {
           // 以上 Hander 选择/执行遇到错误/异常的情况 
           // 注意这种情况下,此方法调用开始时参数 mv 一定为 null,这是由DispatcherServlet#doDispatch
           // 方法的逻辑流程所决定的。
           // 针对这种出现错误/异常的情况,试图根据错误/异常信息分析得到相应的数据模型和错误视图,
           // 包装为一个 ModelAndView, 记录到 mv。
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
              // 尝试从异常分析得到对应的错误数据模型和错误页面视图ModelAndView,也保存到 mv
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

        // 该方法以上代码对异常存在的情况做了特殊处理:从异常信息中解析相应的ModelAndView到mv,
        // 这样,针对Handler选择/执行中异常和正常两种情况,该方法就将它们可以统一到同样的处理模式了:
        // 解析和渲染 mv
        // 此时还有一点需要注意 : 当前 mv 也可能是 null,比如控制器方法直接返回 JSON/XML而无需
        // 视图渲染的情况,这种情况下当前方法在这里就算结束了,仅为它进入不了下面的 if 语句块

		// Did the handler return a view to render?        
		if (mv != null && !mv.wasCleared()) {
          // 解析和渲染 mv 到响应对象  
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
          // 调用各个 HandlerInterceptor 的完成时拦截处理逻辑
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

从以上代码可以看出,该过程主要的任务是 :

  1. 如果是Handler选择/执行出现错误/异常的情况,尝试根据错误/异常信息分析得到相应的ModelAndViewmv;

    如果不是是Handler选择/执行出现错误/异常的情况,这一步跳过;

  2. 使用ModelAndView mv 解析和渲染视图,将结果写入到响应对象;

为了完成任务,processDispatchResult方法又使用了两个重要的方法 :

  1. ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

    根据异常信息解析相应的渲染错误视图的ModelAndView

  2. void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response)

    解析视图和渲染视图

我们接下来对这两个重要方法做分析。

1. processHandlerException 翻译异常到模型和视图ModelAndView

	/**
	 * Determine an error ModelAndView via the registered HandlerExceptionResolvers.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param handler the executed handler, or {@code null} if none chosen at the time of the exception
	 * (for example, if multipart resolution failed)
	 * @param ex the exception that got thrown during handler execution
	 * @return a corresponding ModelAndView to forward to
	 * @throws Exception if no error ModelAndView found
	 */
	@Nullable
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		// Success and error responses may use different content types
		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

		// Check registered HandlerExceptionResolvers...
        // 检查 DispatcherServlet  的属性 handlerExceptionResolvers,看看其中的哪个
        // HandlerExceptionResolver 能够处理该异常。如果某个 HandlerExceptionResolver
        // 能处理指定的异常,他会返回一个非 null ModelAndView 对象。该非 null
        // ModelAndView 对象又有两种状态 :
        // 1. 为空, #isEmpty 方法返回true,表示该异常可被处理但是没有相应的错误页面视图
        // 2. 不为空,#isEmpty 方法返回false,表示该异常可被处理且有相应的错误页面视图
		ModelAndView exMv = null;
		if (this.handlerExceptionResolvers != null) {
           // 遍历 handlerExceptionResolvers 中的每个 HandlerExceptionResolver 看它能否
           // 解析指定异常
           // 这里 handlerExceptionResolvers 是在 DispatcherServlet 初始化时被填充的
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {            
				exMv = resolver.resolveException(request, response, handler, ex);
              // 返回 exMv 不为 null 表示当前HandlerExceptionResolver能够解析该异常,则跳出循环;
              // 返回 exMv 为 null 表示当前HandlerExceptionResolver不能够解析该异常,继续循环下一个;
				if (exMv != null) {
					break;
				}
			}
		}
		if (exMv != null) {
           //  异常能被某个HandlerExceptionResolver解析的情况
			if (exMv.isEmpty()) {
              // 异常能被某个HandlerExceptionResolver解析,但是不存在对应的错误页面的情况  
				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
				return null;
			}
			// We might still need view name translation for a plain error model...
          // 如果 exMv 中没有 View,也尝试应用缺省视图  
			if (!exMv.hasView()) {
				String defaultViewName = getDefaultViewName(request);
				if (defaultViewName != null) {
					exMv.setViewName(defaultViewName);
				}
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Using resolved error view: " + exMv, ex);
			}
			if (logger.isDebugEnabled()) {
				logger.debug("Using resolved error view: " + exMv);
			}
			WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
          // 异常能被某一个HandlerExceptionResolver解析,并且有相应的错误页面的情况,
          // 返回相应的错误数据模型和错误页面视图 ModelAndView exMv
			return exMv;
		}

       // 异常不能被任何一个HandlerExceptionResolver解析, 继续抛出该异常
		throw ex;
	}

2. render 解析和渲染视图

	/**
	 * Render the given ModelAndView.
	 * This is the last stage in handling a request. It may involve resolving the view by name.
	 * @param mv the ModelAndView to render
	 * @param request current HTTP servlet request
	 * @param response current HTTP servlet response
	 * @throws ServletException if view is missing or cannot be resolved
	 * @throws Exception if there's a problem rendering the view
	 */
	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) 
			throws Exception {
		// Determine locale for request and apply it to the response.
       // 本地化处理, 属性 this.localeResolver 在 DispatcherServlet 初始化时由初始化方法 initLocaleResolver 填充
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

       // 尝试从 ModelAndView mv中分析得到 View view 
		View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			// We need to resolve the view name.
          // 根据视图名称找到相应的视图对象,该方法使用到了 this.viewResolvers 属性中注册的各个
          // ViewResolver 尝试解析该视图名称,如果某个 ViewResolver 能解析该视图名称,
          // 则使用该 ViewResolver 将视图名称解析成功一个 View 对象返回
          // this.viewResolvers 是 DispatcherServlet 初始化时通过初始化方法 initViewResolvers 
          // 填充的,常见的一些视图解析器 :
          // ContentNegotiatingViewResolver
          // BeanNameViewResolver
          // FreeMarkerViewResolver (仅在开发人员使用了Freemarker模板引擎时才被注册)
          // ViewResolverComposite
          // InternalResourceViewResolver
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
              // 如果不存在能够解析当前视图名称的  ViewResolver,则抛出该异常
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}

        // 现在已经从 ModelAndView 中分析得到 View 对象了,现在使用它进行结合要渲染的数据进行视图渲染       
		// Delegate to the View object for rendering.
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view [" + view + "] ");
		}
		try {
			if (mv.getStatus() != null) {
              // 设置响应状态  
				response.setStatus(mv.getStatus().value());
			}
          
          // 视图渲染 :
          // 结合数据模型和视图模板形成最终的视图数据写入到响应对象,
          // 不过这里视图数据写入后并不会发送给请求者。将响应数据最终
          // 发送给请求者也就是浏览器端的动作,会在随后调用控制返回给
          // 应用服务器时,由应用服务器执行 :
          // 1. 从用户直观的感受来讲,当前方法结束时,用户在浏览器上看不到写入的视图数据。
          // 2. 针对应用服务器是Tomcat的情况,是在 CoyoteAdapter#service 中请求被Spring MVC
          // 处理完之后调用 response.finishResponse() 才真正将数据返回给请求者。        
          // view 类型分析 :
          // 1. 如果是页面跳转,此处 view 是 RedirectView
          // 2. 如果是一般视图模板,比如Freemarker 模板,此处是 
          // 3. Spring Boot + Spring MVC 应用中,缺省情况下,如果是错误页面,
          //   这里是  ErrorMvcAutoConfiguration$StaticView
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}

相关文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值