Spring Framework 7.0.9 深度剖析-17-WebMVC 请求处理链路

6 阅读52分钟

Spring Framework 7.0.9 深度剖析-17-WebMVC 请求处理链路

17.1 本章导读

一个 HTTP 请求从被 Servlet 容器接收到最终响应体写回,在 Spring WebMVC 中要穿过多少层?本章的目标,就是把这条链路在 Spring Framework 7.0.9 源码里逐类、逐方法、逐字段地走一遍:请求如何被 HttpServletBean.init 装配、如何经由 FrameworkServlet.processRequest 把请求上下文绑定到当前线程、DispatcherServlet.doDispatch 如何完成「multipart 判定 → 找 Handler → 跑拦截器 preHandle → 找 HandlerAdapter → 反射调用 Controller 方法 → 解析参数 → 处理返回值 → 视图渲染或消息体写回 → 异常解析 → 拦截器 postHandle/afterCompletion」这一整套流水线。

涉及的核心模块与类(均以本章仓库 spring-framework-7.0.9 为准):

  • spring-webmvc 模块:org.springframework.web.servlet 包下的 HttpServletBean、FrameworkServlet、DispatcherServlet、HandlerExecutionChain、HandlerMapping、HandlerAdapter、HandlerInterceptor、ModelAndView、View、ViewResolver、HandlerExceptionResolver;以及 handler、mvc.method.annotation、view、mvc.support 等子包。
  • spring-web 模块:org.springframework.http.converter 包下的消息转换器体系(AbstractHttpMessageConverter 等)、org.springframework.web.method.support 包下的 InvocableHandlerMethod 与两个 Composite 解析器、org.springframework.web.context.request.async 包下的 WebAsyncManager,以及 org.springframework.web.multipart.support.StandardServletMultipartResolver。

前置知识:读者应了解 Servlet 规范中 HttpServlet 的 service/doGet/doPost 分派机制、Spring IoC 容器的基本使用、以及 @Controller/@RequestMapping/@ResponseBody 的常规用法。读完本章,你应当能够回答以下问题:

  1. 为什么 DispatcherServlet 自己不直接调用 Controller,而要经过 HandlerMapping 与 HandlerAdapter 两层间接?这两层各自解决了什么变化点?
  2. @RequestParam、@PathVariable、@RequestBody 这些注解是在哪个类、哪一行被识别并取值的?为什么一个未被任何解析器识别的简单类型参数会被当作请求参数?
  3. @ResponseBody 返回一个对象后,JSON 是在哪一步、由哪个类、按照什么规则选中 MappingJackson2HttpMessageConverter 写出去的?Accept 头与 produces 如何影响最终 Content-Type?
  4. 拦截器的 preHandle 返回 false 时,为什么只有「已成功 preHandle 过的那几个」拦截器的 afterCompletion 会被回调?这个状态记在哪?
  5. 404 与 405 在源码中分别在哪一行被抛出、被哪个 HandlerExceptionResolver 接住?

本章不假设读者读过本系列其他章节,所有机制都自包含地讲透。

17.2 核心概念:链路中的关键抽象

在钻进源码之前,先把链路上反复出现的几个抽象钉死。它们不是名词表,每个概念都对应一个真实接口及其在链路中的职责。

17.2.1 HandlerMapping:请求到处理器的路由器

HandlerMapping(spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java)的核心契约是:

public interface HandlerMapping {
	@Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

它负责回答「这个 URL 应该交给哪个对象处理」。注意返回值不是裸 handler,而是 HandlerExecutionChain——handler 本体加上一组匹配到的拦截器。DispatcherServlet 内部维护一个 List<HandlerMapping> handlerMappings,按 AnnotationAwareOrderComparator 排序后顺序遍历,谁先返回非 null 的 chain 就用谁。7.0.9 中内置的三大实现:BeanNameUrlHandlerMapping(以 bean 名为 URL)、RequestMappingHandlerMapping(扫描 @Controller 的 @RequestMapping,是绝大多数应用的主力)、RouterFunctionMapping(函数式路由)。默认清单见 spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties。

17.2.2 HandlerAdapter:调用处理器的统一端口

为什么有了 HandlerMapping 还需要 HandlerAdapter?因为 handler 的形态千差万别:它可能是一个实现了 Controller 接口的老风格对象、可能是一个 HttpRequestHandler、也可能是一个被包装成 HandlerMethod 的注解式 Controller 方法。直接调用要求每种 handler 都实现同一个接口会束缚扩展性。HandlerAdapter(spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerAdapter.java)用「适配」模式解耦:

public interface HandlerAdapter {
	boolean supports(Object handler);
	@Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

DispatcherServlet 拿到 chain 里的 handler 后,遍历 handlerAdapters 列表,找到第一个 supports(handler) 返回 true 的适配器,由它去完成真正的反射调用。7.0.9 默认内置四个适配器:HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、RequestMappingHandlerAdapter、HandlerFunctionAdapter。

17.2.3 HandlerMethod 与 ServletInvocableHandlerMethod

注解式 Controller 的一个方法(比如 UserController#getUser(Long id))被 RequestMappingHandlerMapping 封装成 HandlerMethod(spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java),它持有 bean 实例、Method 对象、以及把方法参数包装成 MethodParameter 的能力。进入适配器后,它再被包装成 InvocableHandlerMethod(spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java)——负责「解析参数 → 反射调用」;在 Servlet 环境下进一步包装为 ServletInvocableHandlerMethod(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java)——额外负责「调用后用返回值处理器处理返回值、写响应状态码」。

17.2.4 HandlerExecutionChain:处理器 + 拦截器的执行链

如前所述,它是 handler 本体和 List<HandlerInterceptor> 的组合,内部用一个 int interceptorIndex 字段记录「当前执行到第几个拦截器」。preHandle 顺序执行、postHandle 逆序执行、afterCompletion 从 interceptorIndex 逆序回退——这个字段是理解拦截器短路行为的钥匙,本章第 17.6 节会逐行拆。

17.2.5 ModelAndView:视图与模型的载体

ModelAndView(spring-webmvc/src/main/java/org/springframework/web/servlet/ModelAndView.java)内部持有两部分:Object view(可能是视图名字符串,也可能是 View 实例)和 ModelMap model(一个 Map<String,Object>)。HandlerAdapter 调用 handler 后返回它,DispatcherServlet 据此决定是「按名字解析视图再渲染」还是「直接用给定 View 渲染」。对于 @ResponseBody 场景,返回值处理器会把 mavContainer.setRequestHandled(true),于是 doDispatch 后续根本不渲染视图。

17.2.6 ViewResolver 与 View

ViewResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/ViewResolver.java)契约只有一个方法:View resolveViewName(String viewName, Locale locale)。DispatcherServlet 同样维护一个排好序的 List<ViewResolver> viewResolvers,顺序尝试,谁先返回非 null 的 View 就用谁。View 接口(View.java)的核心是 void render(Map<String,?> model, HttpServletRequest request, HttpServletResponse response)。视图名解析与消息体写回是两条互斥的终点。

17.2.7 HandlerExceptionResolver:异常到错误视图的转换器

HandlerExceptionResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java)契约:ModelAndView resolveException(HttpServletRequest, HttpServletResponse, Object handler, Exception ex)。当 doDispatch 内层 try 捕获到异常时,把异常交给排好序的 resolver 链,第一个返回非 null ModelAndView 的 resolver 胜出。7.0.9 默认三链:ExceptionHandlerExceptionResolver(处理 @ExceptionHandler 与 @ControllerAdvice)、ResponseStatusExceptionResolver(处理 @ResponseStatus 注解与 ResponseStatusException)、DefaultHandlerExceptionResolver(把标准 Servlet 异常映射到对应 HTTP 状态码)。

17.2.8 MultipartResolver、WebAsyncManager、FlashMapManager

这三个是 DispatcherServlet 初始化时装配的「边缘策略」:MultipartResolver(spring-web/src/main/java/org/springframework/web/multipart/MultipartResolver.java)在请求进入 doDispatch 第一步把文件上传请求包成 MultipartHttpServletRequest;WebAsyncManager(spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java)管理 Callable/DeferredResult 的异步挂起与恢复;FlashMapManager 负责重定向间的临时参数传递。它们的共同特点是:在 doDispatch 主流程里只占很薄的一层,但每一层都对应一个明确的扩展点。

把这些抽象摆在一起,整条链路的骨架就是:请求 → FrameworkServlet 绑定上下文 → DispatcherServlet 选择 HandlerMapping 得到 chain → 拦截器 preHandle → HandlerAdapter 反射调用方法(内部完成参数解析与返回值处理)→ 成功则渲染视图/写消息体,失败则异常 resolver 链 → 拦截器 postHandle/afterCompletion → FrameworkServlet 清理线程上下文。后续每一节都是对这个骨架的源码级展开。

17.3 源码剖析(一):Servlet 适配与继承链

WebMVC 的请求入口不是 DispatcherServlet 自己,而是三层 Servlet 子类构成的继承链:HttpServlet → HttpServletBean → FrameworkServlet → DispatcherServlet。理解这条链,等于理解了「Spring 为什么能把一个标准 Servlet 改造成 IoC 容器感知的分发器」。

17.3.1 HttpServletBean:把 init-param 变成 bean 属性

HttpServletBean(spring-webmvc/src/main/java/org/springframework/web/servlet/HttpServletBean.java)继承 HttpServlet,实现 EnvironmentCapable, EnvironmentAware。它的全部价值在 init() 方法上,注意它被声明为 final——子类不能重写初始化入口:

@Override
public final void init() throws ServletException {

	// Set bean properties from init parameters.
	PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
	if (!pvs.isEmpty()) {
		try {
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
			}
			throw ex;
		}
	}

	// Let subclasses do whatever initialization they like.
	initServletBean();
}

逐段解读:

  1. ServletConfigPropertyValues(本类的私有静态内部类,第 209-242 行)把 Servlet 容器传来的 ServletConfig 里所有 init-param 枚举出来,封装成 MutablePropertyValues。它还持有一个 requiredProperties 集合——子类可以在构造期调用 addRequiredProperty(String) 声明某个参数必填,构造函数末尾若 missingProps 非空,直接抛 ServletException 并列出缺哪些属性。
  2. BeanWrapper:这是 Spring beans 包的属性访问抽象。PropertyAccessorFactory.forBeanPropertyAccess(this) 把 DispatcherServlet 自己当成一个 bean 来包装。于是 web.xml 里写的 <init-param><param-name>contextConfigLocation</param-name>... 会被自动类型转换后调用 setContextConfigLocation(...) 这类 setter。registerCustomEditor(Resource.class, ...) 注册了自定义编辑器,让 classpath 之类的资源写法能被解析成 Resource。
  3. bw.setPropertyValues(pvs, true) 的第二个参数 true 表示「不允许未知参数」(ignoreUnknown=true 为 false)——也就是说 init-param 名字在 servlet 的 JavaBean 属性里找不到对应 setter 时会直接报错。这是早期 web.xml 配置时代的严格性,注解驱动的现代应用里这段通常几乎不生效。
  4. 模板方法 initServletBean():默认空实现,留给子类。这是整条继承链的第一个扩展点——FrameworkServlet 重写它去建立 WebApplicationContext。

这个设计的本质是:HttpServlet 子类本身被 Spring 当成一个 bean 来装配,用 BeanWrapper 完成了「Servlet 容器的初始化参数 → Spring bean 属性」这一步桥接。

17.3.2 FrameworkServlet.initServletBean:建立 WebApplicationContext

FrameworkServlet(spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java)重写了 initServletBean,它同样是 final:

@Override
protected final void initServletBean() throws ServletException {
	getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
	if (logger.isInfoEnabled()) {
		logger.info("Initializing Servlet '" + getServletName() + "'");
	}
	long startTime = System.currentTimeMillis();

	try {
		this.webApplicationContext = initWebApplicationContext();
		initFrameworkServlet();
	}
	catch (ServletException | RuntimeException ex) {
		logger.error("Context initialization failed", ex);
		throw ex;
	}
	// ... 日志输出耗时
}

initWebApplicationContext()(第 558 行起)做的事,在概念上是:从 ServletContext 拿到根上下文(WebApplicationContextUtils.getWebApplicationContext,即 ContextLoaderListener 建立的那个),再为这个 DispatcherServlet 建立自己的 WebApplicationContext(通常是 XmlWebApplicationContext 或 AnnotationConfigWebApplicationContext),把根上下文设为它的 parent,然后 refresh()。SpringMVC 的 Controller、HandlerMapping 这些 bean 就落在这个 servlet 命名空间的上下文里。initFrameworkServlet() 是另一个空模板方法,给子类(DispatcherServlet 没用到)留的。

关键点:DispatcherServlet 真正的策略初始化(HandlerMapping/HandlerAdapter 等的查找)不是在这一步做的,而是在上下文 refresh 完成后通过 onRefresh(ApplicationContext) 回调做的。这个回调链后面讲。

17.3.3 service 的 PATCH 分支与 doGet/doPost 的统一收口

Servlet 规范的 HttpServlet.service 只会按标准方法分派到 doGet/doPost/doPut/doDelete/doOptions/doTrace。但 PATCH 与 WebDAV 的非标准方法不在其中。FrameworkServlet 重写了 service:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {

	if (HTTP_SERVLET_METHODS.contains(request.getMethod())) {
		super.service(request, response);
	}
	else {
		processRequest(request, response);
	}
}

HTTP_SERVLET_METHODS 是一个包含 GET/POST/PUT/DELETE/HEAD/OPTIONS/TRACE 的集合。标准方法走父类 service,进而分派到下面这些被声明为 final 的 doXxx:

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}

@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}
// doPut / doDelete 同样直接 processRequest(request, response)

注意 doGet/doPost/doPut/doDelete 全是 final,且都只做一件事:调用 processRequest。这意味着所有标准方法在 FrameworkServlet 这一层被统一收口到同一个模板方法。doOptions 与 doTrace 略有不同(第 935、964 行):只有当开发者显式打开 dispatchOptionsRequest/dispatchTraceRequest,或者遇到 CORS 预检请求时,才转给 processRequest;否则走 HttpServlet 默认实现。doOptions 里还包了一层 HttpServletResponseWrapper,专门在父类写完 Allow 头后追加 PATCH——这就是为什么默认响应头里能看到 PATCH。

17.3.4 processRequest:线程上下文绑定与清理模板

processRequest 是每个请求的外层包装,它解决的核心问题是:让任意一层业务代码能通过 RequestContextHolder/LocaleContextHolder 静态拿到当前请求,并且在请求结束后把 ThreadLocal 还原。完整源码(第 982-1019 行):

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {

	long startTime = System.currentTimeMillis();
	Throwable failureCause = null;

	LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
	LocaleContext localeContext = buildLocaleContext(request);

	RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
	ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
	asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

	initContextHolders(request, localeContext, requestAttributes);

	try {
		doService(request, response);
	}
	catch (ServletException | IOException ex) {
		failureCause = ex;
		throw ex;
	}
	catch (Throwable ex) {
		failureCause = ex;
		throw new ServletException("Request processing failed: " + ex, ex);
	}

	finally {
		resetContextHolders(request, previousLocaleContext, previousAttributes);
		if (requestAttributes != null) {
			requestAttributes.requestCompleted();
		}
		logResult(request, response, failureCause, asyncManager);
		publishRequestHandledEvent(request, response, startTime, failureCause);
	}
}

逐段解读:

  1. 保存现场:先从两个 Holder 里读出「当前线程上一次绑定的 LocaleContext / RequestAttributes」。这是必要的,因为 Servlet 容器的线程是池化复用的——同一个线程可能先处理过别的请求,甚至在异步dispatch时被复用。previous* 变量记录下来,finally 里原样还原,保证 ThreadLocal 不串请求。
  2. 构建新上下文:buildLocaleContext(request) 默认返回 new SimpleLocaleContext(request.getLocale());DispatcherServlet 重写了它(第 1072 行),改成用 localeResolver.resolveLocale(request)——也就是说请求处理过程中 Locale 的真正来源是 DispatcherServlet 的 LocaleResolver 策略,而不是 request.getLocale()。
  3. buildRequestAttributes(第 1043-1052 行)有个关键分支:
protected @Nullable ServletRequestAttributes buildRequestAttributes(HttpServletRequest request,
		@Nullable HttpServletResponse response, @Nullable RequestAttributes previousAttributes) {

	if (previousAttributes == null || previousAttributes instanceof ServletRequestAttributes) {
		return new ServletRequestAttributes(request, response);
	}
	else {
		return null;  // preserve the pre-bound RequestAttributes instance
	}
}

如果当前线程上已经绑了一个非 ServletRequestAttributes 的 RequestAttributes(比如某个 Filter 提前绑了),这里返回 null,initContextHolders 就不会覆盖它——保护第三方预先绑定的上下文。 4. 异步回调的注册:WebAsyncUtils.getAsyncManager(request) 从 request 属性里取出(或新建)本请求的 WebAsyncManager,并注册一个名为 FrameworkServlet 的 CallableInterceptor(RequestBindingInterceptor)。这个拦截器在异步线程切换时负责在「执行 Callable 之前/之后」重新绑定与还原请求上下文——否则在业务异步线程里 RequestContextHolder.getRequestAttributes() 会是 null。这是异步场景下 ThreadLocal 不丢的关键机制。 5. 核心委托 doService:这是个抽象方法,由 DispatcherServlet 实现。两层 catch 的区别:ServletException|IOException 原样抛出;其他 Throwable(包括 Error)包成 ServletException("Request processing failed: ...") 抛出——这样 @ExceptionHandler 也能接住业务代码里抛出的 Error(doDispatch 里还有一层对 Throwable 的兜底,后面会看到)。 6. finally 清理:resetContextHolders 把 ThreadLocal 还原成 previous*;requestAttributes.requestCompleted() 触发 ServletRequestAttributes 的销毁回调(释放内部缓存的请求/响应对象引用,防止容器线程池持有的 request 属性泄漏);最后 publishRequestHandledEvent 发布一个 ServletRequestHandledEvent(默认开启,可通过 setPublishContext/相关开关关闭以省一次事件发布开销)。

一句话:FrameworkServlet 是「请求级 ThreadLocal 生命周期」的管理者,DispatcherServlet 是「请求级分发逻辑」的实现者。两者通过 processRequest → doService 这个模板方法衔接。

17.3.5 DispatcherServlet.initStrategies:九大策略的装配清单

DispatcherServlet 重写了 onRefresh,把 WebApplicationContext 刷新完成的事件接到自己的策略初始化上:

@Override
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
	initMultipartResolver(context);
	initLocaleResolver(context);
	initHandlerMappings(context);
	initHandlerAdapters(context);
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	initViewResolvers(context);
	initFlashMapManager(context);
}

这九个 init* 方法的套路高度一致,分两类:

第一类:按固定 bean 名查找,找不到用默认值。 以 initLocaleResolver 为例(第 481-499 行):

private void initLocaleResolver(ApplicationContext context) {
	try {
		this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
		// ... log
	}
	catch (NoSuchBeanDefinitionException ex) {
		// We need to use the default.
		this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
	}
}

LOCALE_RESOLVER_BEAN_NAME 常量值是 "localeResolver"。getDefaultStrategy 从 DispatcherServlet.properties 里读出该接口的默认实现类名并实例化。initRequestToViewNameTranslator(默认 DefaultRequestToViewNameTranslator)、initFlashMapManager(默认 SessionFlashMapManager)、以及特殊的 initMultipartResolver(默认 null,即不支持文件上传)都属于这一类。

第二类:按类型收集所有实现,排序成列表,找不到则用默认列表。 以 initHandlerMappings 为例(第 506-545 行):

private void initHandlerMappings(ApplicationContext context) {
	this.handlerMappings = null;

	if (this.detectAllHandlerMappings) {
		// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
		Map<String, HandlerMapping> matchingBeans =
				BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
		if (!matchingBeans.isEmpty()) {
			this.handlerMappings = new ArrayList<>(matchingBeans.values());
			// We keep HandlerMappings in sorted order.
			AnnotationAwareOrderComparator.sort(this.handlerMappings);
		}
	}
	else {
		try {
			HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
			this.handlerMappings = Collections.singletonList(hm);
		}
		catch (NoSuchBeanDefinitionException ex) {
			// Ignore, we'll add a default HandlerMapping later.
		}
	}

	// Ensure we have at least one HandlerMapping, by registering
	// a default HandlerMapping if no other mappings are found.
	if (this.handlerMappings == null) {
		this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
	}

	for (HandlerMapping mapping : this.handlerMappings) {
		if (mapping.usesPathPatterns()) {
			this.parseRequestPath = true;
			break;
		}
	}
}

逐段解读:

  1. detectAllHandlerMappings 默认 true,意味着「把容器里所有 HandlerMapping bean 都收集来」,并按 @Order/Ordered 排序。只有当开发者把它设为 false 时,才退回「按 bean 名 handlerMapping 找单个」的老模式。
  2. AnnotationAwareOrderComparator.sort 决定了 doDispatch 里 getHandler 遍历的顺序——这就是为什么自定义 HandlerMapping 想插队必须实现 Ordered 或加 @Order。
  3. 一个关键副作用:如果收集到的任意一个 mapping usesPathPatterns() 返回 true,DispatcherServlet 就把 this.parseRequestPath = true。这个标志位会在 doService 里控制「是否提前用 PathPatternParser 解析并缓存 RequestPath」,见下一小节。
  4. initHandlerAdapters(第 552-584 行)、initHandlerExceptionResolvers(第 591-624 行)、initViewResolvers(第 656-688 行)结构与上面几乎逐行相同,区别只在默认列表与默认值。

7.0.9 的默认策略清单写在 spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties 里,原文如下(请以仓库文件为准):

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
		org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
		org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
		org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
		org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
		org.springframework.web.servlet.function.support.HandlerFunctionAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
		org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
		org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

要注意:这份默认清单只在「容器里一个同类型 bean 都没有」时才生效。在注解驱动的 Spring Boot/@EnableWebMvc 场景下,WebMvcConfigurationSupport 会主动注册 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 等 bean,因此上面这些默认实现大多被实际的 bean 覆盖。理解这一点,才能理解为什么「查 DispatcherServlet.properties」和「实际运行时看到的策略」可能对不上。

17.3.6 doService:doDispatch 前的准备工作

DispatcherServlet 实现了抽象方法 doService(第 829-879 行):

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
	logRequest(request);

	// Keep a snapshot of the request attributes in case of an include,
	// to be able to restore the original attributes after the include.
	Map<String, Object> attributesSnapshot = null;
	if (WebUtils.isIncludeRequest(request)) {
		attributesSnapshot = new HashMap<>();
		Enumeration<?> attrNames = request.getAttributeNames();
		while (attrNames.hasMoreElements()) {
			String attrName = (String) attrNames.nextElement();
			if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
				attributesSnapshot.put(attrName, request.getAttribute(attrName));
			}
		}
	}

	// Make framework objects available to handlers and view objects.
	request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
	request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);

	if (this.flashMapManager != null) {
		FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
		if (inputFlashMap != null) {
			request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
		}
		request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
		request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
	}

	RequestPath previousRequestPath = null;
	if (this.parseRequestPath) {
		previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
		ServletRequestPathUtils.parseAndCache(request);
	}

	try {
		doDispatch(request, response);
	}
	finally {
		if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Restore the original attribute snapshot, in case of an include.
			if (attributesSnapshot != null) {
				restoreAttributesAfterInclude(request, attributesSnapshot);
			}
		}
		if (this.parseRequestPath) {
			ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
		}
	}
}

逐段解读:

  1. logRequest:按 debug/trace 级别打印请求方法、URI、参数。注意它对 multipart 只打 "multipart",对 form-urlencoded 默认打 "masked"——这是 7.x 为防敏感信息落日志做的默认保护,可由 enableLoggingRequestDetails 打开。
  2. include 请求的属性快照:当 DispatcherServlet 被 RequestDispatcher.include 调用时,它在原请求的 request 作用域里往里塞了一堆框架属性(WEB_APPLICATION_CONTEXT_ATTRIBUTE 等)。快照保证 include 结束后能把这些属性还原回 include 前的状态,避免污染外层 JSP。
  3. 暴露框架对象:把 WebApplicationContext 与 LocaleResolver 放进 request 属性,View 实现(比如 JSP 标签库)能通过 request.getServletContext()/这些 key 反查到它们。
  4. FlashMap 恢复:redirect: 场景下,上一个请求通过 RedirectAttributes 存放的「一次性参数」由 flashMapManager.retrieveAndUpdate 取出,作为 inputFlashMap 暴露;同时新建一个空的 outputFlashMap 供本次请求写入。
  5. 路径解析缓存:当 parseRequestPath 为 true(即至少有一个 mapping 用 PathPattern),doService 提前调用 ServletRequestPathUtils.parseAndCache(request) 把 RequestPath 解析结果塞进 request 属性。这样后续 HandlerMapping 查找、路径变量解析共用同一份解析结果,不会重复解析。finally 里再把它还原成之前缓存的那个——同样是为 include/嵌套分发做隔离。
  6. 异步判断:finally 里若 isConcurrentHandlingStarted() 为 true(handler 刚发起了异步处理,主线程即将退出),则不还原属性快照,因为异步恢复dispatch时还要用这些属性。

这一层做完,才真正进入核心的 doDispatch。

17.4 源码剖析(二):doDispatch 全流程逐段拆解

doDispatch(DispatcherServlet.java 第 935-1004 行)是本章最重要的一个方法。先把完整源码摆出来,再逐段拆:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	boolean multipartRequestParsed = false;

	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

		try {
			processedRequest = checkMultipart(request);
			multipartRequestParsed = (processedRequest != request);

			// Determine handler for the current request.
			mappedHandler = getHandler(processedRequest);
			if (mappedHandler == null) {
				noHandlerFound(processedRequest, response);
				return;
			}

			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				return;
			}

			// Determine handler adapter and invoke the handler.
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

			if (asyncManager.isConcurrentHandlingStarted()) {
				return;
			}

			applyDefaultViewName(processedRequest, mv);
			mappedHandler.applyPostHandle(processedRequest, response, mv);
		}
		catch (Exception ex) {
			dispatchException = ex;
		}
		catch (Throwable err) {
			// As of 4.3, we're processing Errors thrown from handler methods as well,
			// making them available for @ExceptionHandler methods and other scenarios.
			dispatchException = new ServletException("Handler dispatch failed: " + err, err);
		}
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}
	catch (Exception ex) {
		triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
	}
	catch (Throwable err) {
		triggerAfterCompletion(processedRequest, response, mappedHandler,
				new ServletException("Handler processing failed: " + err, err));
	}
	finally {
		if (asyncManager.isConcurrentHandlingStarted()) {
			// Instead of postHandle and afterCompletion
			if (mappedHandler != null) {
				mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
			}
			asyncManager.setMultipartRequestParsed(multipartRequestParsed);
		}
		else {
			// Clean up any resources used by a multipart request.
			if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
				cleanupMultipart(processedRequest);
			}
		}
	}
}

这是一个「三层 try-catch-finally」结构,每一层职责不同。逐段解读:

17.4.1 第一层:checkMultipart——文件上传请求的包装

processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

checkMultipart(第 1089-1117 行)的完整逻辑:

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
		if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
			if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
				logger.trace("Request already resolved to MultipartHttpServletRequest, for example, by MultipartFilter");
			}
		}
		else if (hasMultipartException(request)) {
			logger.debug("Multipart resolution previously failed for current request - " +
					"skipping re-resolution for undisturbed error rendering");
		}
		else {
			try {
				return this.multipartResolver.resolveMultipart(request);
			}
			catch (MultipartException ex) {
				if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
					logger.debug("Multipart resolution failed for error dispatch", ex);
				}
				else {
					throw ex;
				}
			}
		}
	}
	// If not returned before: return original request.
	return request;
}

逐段:

  1. 前置条件:multipartResolver == null(默认情况,容器里没有名为 multipartResolver 的 bean)或 isMultipart(request) 返回 false(Content-Type 不是 multipart/ 开头),直接返回原 request。这就是为什么「没配 MultipartResolver 时,文件上传接口根本不会被特殊处理」。
  2. 已经被 MultipartFilter 包过的情况:如果 request 已经是 MultipartHttpServletRequest,直接跳过——避免二次包装。
  3. 错误分发复用请求:当容器因为之前解析失败而转入 ERROR dispatch 时,request 属性里带着 jakarta.servlet.error.exception;hasMultipartException 沿 cause 链找是否有 MultipartException,若有则跳过再次解析,让错误页能正常渲染。
  4. 真正包装:resolveMultipart(request) 返回一个 MultipartHttpServletRequest。StandardServletMultipartResolver 的实现就是 new StandardMultipartHttpServletRequest(request, this.resolveLazily),它内部委托 Servlet 容器的 request.getParts() 枚举各个 part。

multipartRequestParsed = (processedRequest != request) 这个布尔值很关键:它在 finally 里决定是否调用 cleanupMultipart 清理临时文件。

17.4.2 getHandler:顺序遍历 HandlerMapping

mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
	noHandlerFound(processedRequest, response);
	return;
}

getHandler(第 1154-1164 行)非常简单:

protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	if (this.handlerMappings != null) {
		for (HandlerMapping mapping : this.handlerMappings) {
			HandlerExecutionChain handler = mapping.getHandler(request);
			if (handler != null) {
				return handler;
			}
		}
	}
	return null;
}

就是按初始化时排好序的列表,一个一个问「你能处理这个请求吗」,第一个返回非 null 的胜出。注意这里没有「多个 mapping 都匹配时如何取舍」的逻辑——顺序 + 短路。

返回 null 意味着一个 mapping 都没匹配上,此时调用 noHandlerFound(第 1172-1178 行):

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
	if (pageNotFoundLogger.isWarnEnabled()) {
		pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
	}
	throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
			new ServletServerHttpRequest(request).getHeaders());
}

7.0.9 直接抛 NoHandlerFoundException(而不是简单地返回 404)。这个异常随后被 doDispatch 的内层 catch 捕获,交给 processHandlerException → 由 DefaultHandlerExceptionResolver 翻译成 404 状态码。这就是「404 是怎么来的」的源码答案。

17.4.3 applyPreHandle:拦截器前置链与短路

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
	return;
}

applyPreHandle 在 HandlerExecutionChain(spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExecutionChain.java 第 142-152 行):

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
	for (int i = 0; i < this.interceptorList.size(); i++) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		if (!interceptor.preHandle(request, response, this.handler)) {
			triggerAfterCompletion(request, response, null);
			return false;
		}
		this.interceptorIndex = i;
	}
	return true;
}

逐段:

  1. 顺序执行所有拦截器的 preHandle。
  2. 返回 false 的短路:一旦某个 preHandle 返回 false,立刻调用 triggerAfterCompletion(request, response, null) 然后返回 false。注意此时 interceptorIndex 已经被赋值为「上一个返回 true 的拦截器下标」——也就是说,只回调那些 preHandle 成功过的拦截器的 afterCompletion,连当前这个返回 false 的拦截器自己的 afterCompletion 都不会被调(因为它的 preHandle 没成功)。
  3. doDispatch 拿到 false 后直接 return——handler 根本不会被调用。开发者的责任是在 preHandle 返回 false 之前自己把响应写好(比如返回 401 JSON)。

17.4.4 getHandlerAdapter 与 ha.handle

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
	return;
}

getHandlerAdapter(第 1185-1195 行):

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
	if (this.handlerAdapters != null) {
		for (HandlerAdapter adapter : this.handlerAdapters) {
			if (adapter.supports(handler)) {
				return adapter;
			}
		}
	}
	throw new ServletException("No adapter for handler [" + handler +
			"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

又是一次「顺序遍历 + 第一个 supports 返回 true 胜出」。对注解式 Controller,handler 是 HandlerMethod,RequestMappingHandlerAdapter.supportsInternal 恒返回 true(后面会讲)。

ha.handle(...) 是整个链路最重的一步:它内部完成「把方法参数从请求里解析出来 → 反射调用 Controller 方法 → 处理返回值 → 返回 ModelAndView」。这一步后面用整整两节展开。

调用结束后立即检查 asyncManager.isConcurrentHandlingStarted():如果 handler(比如返回了 Callable/DeferredResult)刚发起了异步处理,主线程直接 return,不再走 applyDefaultViewName/applyPostHandle/processDispatchResult。这是异步模型在 doDispatch 里的第一个分叉点。

17.4.5 applyDefaultViewName 与 applyPostHandle

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);

applyDefaultViewName(第 1009-1016 行):

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
	if (mv != null && !mv.hasView()) {
		String defaultViewName = getDefaultViewName(request);
		if (defaultViewName != null) {
			mv.setViewName(defaultViewName);
		}
	}
}

如果 handler 返回的 ModelAndView 既没设视图名也没设 View 对象(!mv.hasView()),就让 RequestToViewNameTranslator 按请求路径推一个默认视图名(默认实现 DefaultRequestToViewNameTranslator 把 /user/list 映射成 user/list)。

applyPostHandle 在 HandlerExecutionChain(第 157-164 行):

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
		throws Exception {

	for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		interceptor.postHandle(request, response, this.handler, mv);
	}
}

逆序调用所有拦截器的 postHandle。注意它是从列表末尾往前遍历,不依赖 interceptorIndex——只要 preHandle 全成功了,所有拦截器的 postHandle 都会被调到。postHandle 拿到的 mv 是「handler 调用之后、渲染之前」的模型,开发者可以在这里往 model 里塞公共属性。

17.4.6 异常捕获与 processDispatchResult

内层 try 把整个 handler 执行阶段的异常都收敛成 dispatchException:

catch (Exception ex) {
	dispatchException = ex;
}
catch (Throwable err) {
	// As of 4.3, we're processing Errors thrown from handler methods as well,
	// making them available for @ExceptionHandler methods and other scenarios.
	dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

注意两层 catch:业务抛的 Exception 原样保留(这样 @ExceptionHandler 能拿到真实异常类型);连 Error(如 NoClassDefFoundError)也包成 ServletException 交给异常解析链。这意味着 handler 里抛的任何东西都不会直接穿透到容器,而是进入 processDispatchResult 统一处理。

17.4.7 外层 catch 与 finally

外层 try 的 catch 专门兜「processDispatchResult 自己都没救回来的异常」——比如视图渲染失败、异常 resolver 链都解析不了。此时调用 triggerAfterCompletion(第 1357-1364 行):

private static void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
		@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {

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

它先把已成功 preHandle 的拦截器的 afterCompletion 跑完,再把异常原样抛出(让容器记录)。

finally 块处理两种收尾:

if (asyncManager.isConcurrentHandlingStarted()) {
	if (mappedHandler != null) {
		mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
	}
	asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
	if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
		cleanupMultipart(processedRequest);
	}
}
  • 异步刚启动:调用每个 AsyncHandlerInterceptor 的 afterConcurrentHandlingStarted(逆序,见 HandlerExecutionChain.applyAfterConcurrentHandlingStarted 第 186-200 行),并把「multipart 是否已解析」这个状态交给 asyncManager,因为异步恢复时还要用。
  • 同步完成:若之前包装过多 part,调用 cleanupMultipart(第 1138-1146 行),它最终调到 StandardServletMultipartResolver.cleanupMultipart,把每个 Part 的临时文件 part.delete() 掉。

这就是 doDispatch 的完整因果链。下一节看 processDispatchResult 如何处理异常与渲染。

17.4.8 processDispatchResult:异常解析与视图渲染的统一出口

doDispatch 内层 try 结束后,无论成功还是异常,最后都走到 processDispatchResult(第 1022-1062 行):

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 mavDefiningException) {
			logger.debug("ModelAndViewDefiningException encountered", exception);
			mv = mavDefiningException.getModelAndView();
		}
		else {
			Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
			mv = processHandlerException(request, response, handler, exception);
			errorView = (mv != null);
		}
	}

	// Did the handler return a view to render?
	if (mv != null && !mv.wasCleared()) {
		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) {
		// Exception (if any) is already handled..
		mappedHandler.triggerAfterCompletion(request, response, null);
	}
}

逐段:

  1. 两种异常分支:ModelAndViewDefiningException 是一种特殊异常——它本身携带一个 ModelAndView,常用于拦截器或视图渲染前就想直接跳到某个错误页的场景,直接取出它的 mav;其他异常走 processHandlerException。
  2. errorView = (mv != null):如果异常被某个 resolver 成功解析成了一个非空 ModelAndView,就标记为 errorView。渲染完之后调用 WebUtils.clearErrorRequestAttributes,把 JSP 错误页常用的 jakarta.servlet.error.* 属性清掉,避免泄漏到后续请求。
  3. mv != null && !mv.wasCleared() 才渲染:两个条件。mv == null 的典型场景是 @ResponseBody——返回值处理器已经把响应体写出去了,mavContainer 被标记为 requestHandled,getModelAndView 返回 null;wasCleared() 则是 model 被显式清空的情况。两者都不渲染。
  4. 异步二次分叉:如果在渲染(比如视图里又发起了异步)过程中异步启动了,直接 return,不再 afterCompletion。
  5. 正常完成后的 afterCompletion:注意这里传入的 exception 是 null——代表「成功走完的拦截器,afterCompletion 时异常参数为 null」。只有外层 catch 路径(渲染本身失败)才会把真实异常传进去。

17.4.9 processHandlerException:HandlerExceptionResolver 链

processHandlerException(第 1207-1256 行)是异常解析的核心:

protected @Nullable 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);
	// Reset the response content-type header and body buffer if the response is not committed already,
	// leaving the other response headers in place.
	try {
		response.setHeader(HttpHeaders.CONTENT_TYPE, null);
		response.setHeader(HttpHeaders.CONTENT_DISPOSITION, null);
		response.resetBuffer();
	}
	catch (IllegalStateException illegalStateException) {
		// the response is already committed, leave it to exception handlers anyway
	}

	// Check registered HandlerExceptionResolvers...
	ModelAndView exMv = null;
	if (this.handlerExceptionResolvers != null) {
		for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
			exMv = resolver.resolveException(request, response, handler, ex);
			if (exMv != null) {
				break;
			}
		}
	}
	if (exMv != null) {
		if (exMv.isEmpty()) {
			request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
			return null;
		}
		// We might still need view name translation for a plain error model...
		if (!exMv.hasView()) {
			String defaultViewName = getDefaultViewName(request);
			if (defaultViewName != null) {
				exMv.setViewName(defaultViewName);
			}
		}
		WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
		return exMv;
	}

	throw ex;
}

逐段:

  1. 清场:异常发生在 handler 执行中途,此时 response 可能已经被 handler 设过 Content-Type、写过 buffer。代码先移除「可生产媒体类型」属性、把 Content-Type/Content-Disposition 头置 null、并 resetBuffer()。但只在 response 未提交时有效——如果 response 已提交(比如已经 flush 了半个 JSON),resetBuffer 会抛 IllegalStateException,被 catch 住,然后「留给异常处理器自己看着办」。这是为什么「响应已提交后抛异常」往往无法再换成错误页。
  2. resolver 链顺序遍历:第一个返回非 null exMv 的 resolver 胜出。顺序由初始化时的 AnnotationAwareOrderComparator.sort 决定。
  3. 空 ModelAndView 的特殊含义:exMv.isEmpty() 表示「这个 resolver 已经自己把响应处理完了(比如直接写了 JSON、设了状态码),不需要再渲染视图」。此时把异常放进 request 属性 EXCEPTION_ATTRIBUTE(key 是 exception),返回 null——doDispatch 随后就不会 render。
  4. 错误模型的视图名翻译:如果 resolver 只返回了一个 model 而没给视图名,同样走 getDefaultViewName。
  5. exposeErrorRequestAttributes:把异常、servletName 等暴露成 jakarta.servlet.error.* request 属性,供 JSP 错误页使用。
  6. 所有 resolver 都没接住:throw ex——重新抛出,进入 doDispatch 外层 catch,最终跑到容器的错误页机制。

17.4.10 render:视图名解析与 View.render

render(第 1267-1313 行):

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
	// Determine locale for request and apply it to the response.
	Locale locale =
			(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
	response.setLocale(locale);

	View view;
	String viewName = mv.getViewName();
	if (viewName != null) {
		// We need to resolve the view name.
		view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
		if (view == null) {
			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() + "'");
		}
	}

	if (view instanceof SmartView smartView) {
		smartView.resolveNestedViews(this::resolveViewNameInternal, locale);
	}

	try {
		if (mv.getStatus() != null) {
			request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
			response.setStatus(mv.getStatus().value());
		}
		view.render(mv.getModelInternal(), request, response);
	}
	catch (Exception ex) {
		if (logger.isDebugEnabled()) {
			logger.debug("Error rendering view [" + view + "]", ex);
		}
		throw ex;
	}
}

逐段:

  1. 先设 Locale:通过 localeResolver.resolveLocale(request) 拿到当前区域,设到 response。
  2. 两种 mv 形态:带视图名字符串 → resolveViewName 走 ViewResolver 链;带现成 View 实例(比如 handler 直接返回了 View 对象,或者 RedirectView)→ 直接用。
  3. resolveViewName → resolveViewNameInternal(第 1345-1355 行):就是顺序遍历 viewResolvers,第一个返回非 null View 的胜出。
  4. SmartView 嵌套解析:某些视图(如 RedirectView)可能内部还引用别的视图名,让它自己解析。
  5. ModelAndView 上的状态码:如果 mv 带了 status(比如 ResponseEntity 转换过来的),先设到 response,再渲染。
  6. view.render(model, request, response):这是真正把模型数据塞进 request、forward 到 JSP 或写出 CSV/JSON 的一步。对 InternalResourceView 来说,render 内部就是把 model 的每个 key-value request.setAttribute,然后 request.getRequestDispatcher(forwardUrl).forward(request, response)。

17.4.11 拦截器 afterCompletion 的逆序回调细节

processDispatchResult 末尾的 mappedHandler.triggerAfterCompletion(request, response, null),其实现(HandlerExecutionChain.java 第 171-181 行):

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
	for (int i = this.interceptorIndex; i >= 0; i--) {
		HandlerInterceptor interceptor = this.interceptorList.get(i);
		try {
			interceptor.afterCompletion(request, response, this.handler, ex);
		}
		catch (Throwable ex2) {
			logger.error("HandlerInterceptor.afterCompletion threw exception in interceptor [" + interceptor + "]", ex2);
		}
	}
}

两个设计要点:

  1. 从 interceptorIndex 开始逆序:这就是为什么「只有 preHandle 成功过的拦截器才会被 afterCompletion 回调」。interceptorIndex 在 applyPreHandle 每成功一个就更新为 i;preHandle 返回 false 时调用 triggerAfterCompletion,此时 index 停留在上一个成功的位置。
  2. 每个 afterCompletion 单独 try-catch:某个拦截器的 afterCompletion 抛异常,只记日志,不影响其他拦截器的回调。这是资源清理场景的必要容错——否则一个拦截器的清理异常会中断后续拦截器的清理。

对比 applyPostHandle:postHandle 是「从列表末尾逆序遍历所有」,不看 interceptorIndex。所以「preHandle 成功过的拦截器集合」与「postHandle 会被调用的拦截器集合」在正常情况下是一致的(全成功),但在 preHandle 中途短路时,postHandle 根本不会执行(doDispatch 直接 return 了)。

17.5 源码剖析(三):HandlerMapping 体系——如何把 URL 翻译成 HandlerExecutionChain

doDispatch 里 getHandler 只是顺序遍历,真正的功夫在每个 HandlerMapping.getHandler 内部。这一节拆 AbstractHandlerMapping 与 RequestMappingHandlerMapping。

17.5.1 AbstractHandlerMapping.getHandler:模板方法骨架

AbstractHandlerMapping(spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java)把 getHandler 实现为 final,固定了骨架(第 542-591 行):

@Override
public final @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	ApiVersionHolder versionHolder = initApiVersion(request);
	Object handler = getHandlerInternal(request);
	if (handler == null) {
		handler = getDefaultHandler();
	}
	if (handler == null) {
		return null;
	}

	if (versionHolder.hasError() && !request.getDispatcherType().equals(DispatcherType.ERROR)) {
		throw versionHolder.getError();
	}

	// Bean name or resolved handler?
	if (handler instanceof String handlerName) {
		handler = obtainApplicationContext().getBean(handlerName);
	}

	// Ensure presence of cached lookupPath for interceptors and others
	if (!ServletRequestPathUtils.hasCachedPath(request)) {
		initLookupPath(request);
	}

	HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

	if (request.getAttribute(SUPPRESS_LOGGING_ATTRIBUTE) == null) {
		// ... trace/debug 日志 "Mapped to ..."
	}

	if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
		CorsConfiguration config = getCorsConfiguration(handler, request);
		if (getCorsConfigurationSource() != null) {
			CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
			config = (globalConfig != null ? globalConfig.combine(config) : config);
		}
		if (config != null) {
			config.validateAllowCredentials();
			config.validateAllowPrivateNetwork();
		}
		executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
	}

	return executionChain;
}

逐段解读:

  1. API 版本解析(initApiVersion,第 593-609 行):如果配置了 apiVersionStrategy(7.0 引入的版本号协商机制,从 header/path/参数里解析 API 版本),先解析;解析出错且不是 ERROR dispatch,直接抛异常。
  2. getHandlerInternal 抽象方法:子类(如 RequestMappingHandlerMapping)实现「在自己的注册表里找 handler」。返回 null 则用 getDefaultHandler(),再 null 就返回 null(让 DispatcherServlet 走 noHandlerFound)。
  3. handler 可能是字符串:BeanNameUrlHandlerMapping 返回的 handler 是一个 bean 名字符串,这里通过 obtainApplicationContext().getBean(handlerName) 解析成真正的 bean 实例。这是「handler 形态多态」的第一个体现。
  4. initLookupPath(第 641-651 行):如果 usesPathPatterns,从已缓存的 RequestPath 里取 pathWithinApplication();否则用 UrlPathHelper.resolveAndCacheLookupPath。lookupPath 是后续 MappedInterceptor.matches 判断拦截器是否适用的依据。
  5. getHandlerExecutionChain(handler, request):把裸 handler 包成 chain 并装配拦截器,下一小节展开。
  6. CORS 处理:如果 handler 带 CORS 配置,或这是一个预检请求(OPTIONS),取出配置(局部 + 全局合并),然后通过 getCorsHandlerExecutionChain 给 chain 动态插入一个 CorsInterceptor;对预检请求,handler 会被替换成 PreFlightHandler(一个专门处理 CORS 预检的 HttpRequestHandler),根本不会调到你的 Controller 方法。

17.5.2 getHandlerExecutionChain:拦截器是怎么装进去的

第 681-706 行:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {

	HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain handlerExecutionChain ?
			handlerExecutionChain : new HandlerExecutionChain(handler));

	for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
		if (interceptor instanceof MappedInterceptor mappedInterceptor) {
			if (mappedInterceptor.matches(request)) {
				chain.addInterceptor(mappedInterceptor.getInterceptor());
			}
		}
		else {
			chain.addInterceptor(interceptor);
		}
	}

	if (this.versionStrategy != null) {
		ApiVersionHolder versionHolder = (ApiVersionHolder) request.getAttribute(API_VERSION_ATTRIBUTE);
		if (versionHolder.hasVersion()) {
			Comparable<?> version = versionHolder.getVersion();
			chain.addInterceptor(new ApiVersionDeprecationHandlerInterceptor(this.versionStrategy, version));
		}
	}

	return chain;
}

逐段:

  1. adaptedInterceptors 从哪来?AbstractHandlerMapping 在 initInterceptors()(第 489 行起)里把两类东西合并:setInterceptors(...) 直接配的拦截器,以及所有实现了 MappedInterceptor(带 path pattern)的 bean。adaptInterceptor(第 513 行)会把 WebRequestInterceptor 包装成 InterceptorRegistration 风格的 WebRequestHandlerInterceptorAdapter。
  2. MappedInterceptor 的 path 匹配:普通 HandlerInterceptor 无条件加入 chain;MappedInterceptor(通过 addPathPatterns/excludePathPatterns 注册的那种)要先 matches(request)——依据 lookupPath 判断当前请求是否在拦截范围内。这就是为什么「配置了 addPathPatterns 的拦截器只对部分 URL 生效」。
  3. 7.0 的版本弃用拦截器:如果请求解析出了 API 版本,且该版本被标记为弃用,追加一个 ApiVersionDeprecationHandlerInterceptor,在响应里加 Deprecation/Sunset 头。

17.5.3 RequestMappingHandlerMapping:isHandler 与 getMappingForMethod

这是注解式路由的注册中心。它继承 RequestMappingInfoHandlerMapping → AbstractHandlerMethodMapping → AbstractHandlerMapping。启动时 afterPropertiesSet(继承自 AbstractHandlerMethodMapping)会扫描容器里所有 bean,调用模板方法 isHandler(beanType) 判断「这个 bean 是不是 Controller」。

7.0.9 的 isHandler 实现(RequestMappingHandlerMapping.java 第 181-183 行):

@Override
protected boolean isHandler(Class<?> beanType) {
	return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}

这是一个与旧版本显著不同的点:在 5.x 及更早版本中,这个判断还包含 AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)——也就是说,一个只在类上打了 @RequestMapping、没打 @Controller 的类,曾经也会被当作 handler。7.0.9 收窄为「只有带 @Controller 的类才是 handler」。注意 @RequestMapping 本身被 @Controller 元注解标注?不——@Controller 是 stereotype,@RestController 是 @Controller + @ResponseBody 的组合注解,所以 @RestController 类也能被 isHandler 命中。

被判定为 handler 后,AbstractHandlerMethodMapping 会枚举类里的每个方法,调用 getMappingForMethod(第 204-222 行):

@Override
protected @Nullable RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
	validateCglibProxyMethodVisibility(method, handlerType);

	RequestMappingInfo info = createRequestMappingInfo(method);
	if (info != null) {
		RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
		if (typeInfo != null) {
			info = typeInfo.combine(info);
		}
		if (info.isEmptyMapping()) {
			info = info.mutate().paths("", "/").options(this.config).build();
		}
		String prefix = getPathPrefix(handlerType);
		if (prefix != null) {
			info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
		}
	}
	return info;
}

逐段:

  1. validateCglibProxyMethodVisibility(7.0 新增,第 228-254 行):如果 handler bean 是 CGLIB 代理(类名含 $$),handler 方法是 private 或跨包 package-private,直接抛 IllegalStateException。这是 7.0 为了避免「代理类上 private 方法被误当 handler」而做的强校验。
  2. 方法级 + 类级合并:先读方法上的 @RequestMapping/@HttpExchange 得到 info;再读类上的,typeInfo.combine(info) 把类路径拼到方法路径前面。
  3. 空映射兜底:类上有 @Controller 但方法没标任何 mapping 且方法本身是默认(无参)的情况,补成空路径。
  4. getPathPrefix:支持全局路径前缀(PathMatchConfigurer.addPathPrefix),拼到最前面。

生成的 RequestMappingInfo(包含路径模式、HTTP method、consumes、produces、params、headers 等条件)与 HandlerMethod 一起,被注册进 AbstractHandlerMethodMapping 内部的 mappingRegistry(一个 MappingRegistry,核心是 Map<T, HandlerMethod> 加上路径匹配器)。运行时请求进来,RequestMappingInfoHandlerMapping.getHandlerInternal 用当前 lookupPath 去匹配,遍历所有注册过的 RequestMappingInfo,选出「路径匹配且 method/consumes/produces 全部满足」的那一个。

17.5.4 路径匹配:7.0.9 默认就是 PathPatternParser

一个常见的疑问:「7.0 是不是默认用 PathPattern 而不是 AntPathMatcher?」看 RequestMappingHandlerMapping.afterPropertiesSet(第 146-159 行):

@Override
@SuppressWarnings("removal")
public void afterPropertiesSet() {
	this.config = new RequestMappingInfo.BuilderConfiguration();
	this.config.setContentNegotiationManager(getContentNegotiationManager());
	this.config.setApiVersionStrategy(getApiVersionStrategy());

	if (getPatternParser() != null) {
		this.config.setPatternParser(getPatternParser());
	}
	else {
		this.config.setPathMatcher(getPathMatcher());
	}

	super.afterPropertiesSet();
}

这里是「二选一」逻辑。那默认 getPatternParser() 是什么?答案在 WebMvcConfigurationSupport.initHandlerMapping(第 492-513 行):

private void initHandlerMapping(
		@Nullable AbstractHandlerMapping mapping, FormattingConversionService conversionService,
		ResourceUrlProvider resourceUrlProvider) {

	if (mapping == null) {
		return;
	}
	PathMatchConfigurer pathConfig = getPathMatchConfigurer();
	if (pathConfig.preferPathMatcher()) {
		mapping.setPatternParser(null);
		mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
		mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());
	}
	else if (pathConfig.getPatternParser() != null) {
		mapping.setPatternParser(pathConfig.getPatternParser());
	}
	// else: AbstractHandlerMapping defaults to PathPatternParser

	mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
	mapping.setCorsConfigurations(getCorsConfigurations());
}

注释写得很明白:否则(既没显式 preferPathMatcher、也没显式设 patternParser),AbstractHandlerMapping 默认就是 PathPatternParser。也就是说,7.0.9 的注解式请求映射默认走的是 PathPatternParser(基于解析后的 RequestPath,支持高效的匹配与路径变量提取),只有显式 spring.mvc.pathmatch.matching-strategy=ant_path_matcher 或代码里 preferPathMatcher() 才回退到老的 AntPathMatcher。这与 doService 里 parseRequestPath 标志位、ServletRequestPathUtils.parseAndCache(request) 是同一套机制——路径在请求一开始就被解析成 RequestPath,后续所有映射都在这份解析结果上做匹配。

17.6 源码剖析(四):HandlerAdapter 与参数解析

找到 chain 之后,doDispatch 调用 ha.handle(...)。对注解式 Controller,ha 就是 RequestMappingHandlerAdapter。这一节拆它如何把一个 HTTP 请求变成方法实参、反射调用、再把返回值交给处理器。

17.6.1 supports 恒为 true:为什么任何 HandlerMethod 都能被适配

RequestMappingHandlerAdapter 继承 AbstractHandlerMethodAdapter,后者的 supports 最终调到 supportsInternal(第 825-828 行):

@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
	return true;
}

注释解释了设计意图:任何方法参数和返回值都会以某种方式被处理——没被识别的简单类型当请求参数,没被识别的复合类型当 model attribute,没被识别的返回值当 model attribute。这是一种「宽容兜底」策略,而不是严格拒绝。

17.6.2 handleInternal:session 同步与响应头准备

第 831-866 行:

@Override
protected @Nullable ModelAndView handleInternal(HttpServletRequest request,
		HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

	ModelAndView mav;
	checkRequest(request);

	// Execute invokeHandlerMethod in synchronized block if required.
	if (this.synchronizeOnSession) {
		HttpSession session = request.getSession(false);
		if (session != null) {
			Object mutex = WebUtils.getSessionMutex(session);
			synchronized (mutex) {
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			// No HttpSession available -> no mutex necessary
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}
	}
	else {
		// No synchronization on session demanded at all...
		mav = invokeHandlerMethod(request, response, handlerMethod);
	}

	if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
		if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
			applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
		}
		else {
			prepareResponse(response);
		}
	}

	return mav;
}

两个要点:

  1. synchronizeOnSession:默认 false。打开后,同一个 HttpSession 内的请求会按 session 互斥锁串行执行——这是早期「有状态 Controller」时代的遗留开关,注解式 Controller 无状态,一般不开。
  2. 缓存响应头准备:如果 handler 类标了 @SessionAttributes,说明它在往 session 里放模型属性,为了防止浏览器缓存旧的 session 数据,自动加一组 cache-control 头;否则 prepareResponse 加默认的 Cache-Control: no-store。

17.6.3 invokeHandlerMethod:异步管理器、模型工厂与 ServletInvocableHandlerMethod

第 885-940 行是适配器最核心的一段:

protected @Nullable ModelAndView invokeHandlerMethod(HttpServletRequest request,
		HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
	AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
	asyncWebRequest.setTimeout(this.asyncRequestTimeout);

	asyncManager.setTaskExecutor(this.taskExecutor);
	asyncManager.setAsyncWebRequest(asyncWebRequest);
	asyncManager.registerCallableInterceptors(this.callableInterceptors);
	asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

	// Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4
	response = asyncWebRequest.getNativeResponse(HttpServletResponse.class);

	ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ?
			(ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response));

	WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
	ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

	ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
	if (this.argumentResolvers != null) {
		invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
	}
	if (this.returnValueHandlers != null) {
		invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
	}
	invocableMethod.setDataBinderFactory(binderFactory);
	invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
	invocableMethod.setMethodValidator(this.methodValidator);

	ModelAndViewContainer mavContainer = new ModelAndViewContainer();
	mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
	modelFactory.initModel(webRequest, mavContainer, invocableMethod);

	if (asyncManager.hasConcurrentResult()) {
		Object result = asyncManager.getConcurrentResult();
		Object[] resultContext = asyncManager.getConcurrentResultContext();
		Assert.state(resultContext != null && resultContext.length > 0, "Missing result context");
		mavContainer = (ModelAndViewContainer) resultContext[0];
		asyncManager.clearConcurrentResult();
		// ... log "Resume with async result"
		invocableMethod = invocableMethod.wrapConcurrentResult(result);
	}

	invocableMethod.invokeAndHandle(webRequest, mavContainer);
	if (asyncManager.isConcurrentHandlingStarted()) {
		return null;
	}

	return getModelAndView(mavContainer, modelFactory, webRequest);
}

逐段:

  1. 异步基础设施:每个请求都建立(或复用)AsyncWebRequest,设超时;把 taskExecutor、callable/deferredResult 拦截器注册进 asyncManager。注意 response = asyncWebRequest.getNativeResponse(...)——它包了一层 response,强制 Servlet 2.3.3.4 生命周期规则(异步开始后再写 response 会被拦截)。
  2. WebDataBinderFactory:为这个 handler 准备 @InitBinder 方法的数据绑定器工厂。
  3. ModelFactory:聚合两类「模型预填充」方法——@ControllerAdvice 里的 @ModelAttribute 方法(全局优先),和本 controller 里的 @ModelAttribute 方法。modelFactory.initModel(...) 在调 handler 方法之前先把这些属性放进 mavContainer。这就是为什么「@ModelAttribute 方法会在每个请求、每个 handler 方法执行前先跑一遍」。
  4. ServletInvocableHandlerMethod 注入全套依赖:参数解析器、返回值处理器、数据绑定工厂、参数名发现器、方法校验器。
  5. ModelAndViewContainer:一个「可变的模型 + 视图 + requestHandled 标志」容器。它不是 ModelAndView——真正的 ModelAndView 是在最后 getModelAndView 时从它提炼出来的。
  6. 异步恢复分支:如果 hasConcurrentResult() 为 true,说明这是异步结果恢复的 dispatch。此时 mavContainer 直接用上一次保存的(resultContext[0]),invocableMethod 被 wrapConcurrentResult(result) 包成一个「直接返回异步结果」的方法对象——也就是说,异步恢复时并不是再调一次原 Controller 方法,而是把异步结果当成「方法返回值」直接走返回值处理器。
  7. invokeAndHandle:真正调方法 + 处理返回值。如果刚发起异步,返回 null(doDispatch 看到 isConcurrentHandlingStarted 就直接 return);否则 getModelAndView 把 mavContainer 转成 ModelAndView 返回给 doDispatch。

17.6.4 内置参数解析器清单(以 getDefaultArgumentResolvers 为准)

参数解析器的装配在 RequestMappingHandlerAdapter.getDefaultArgumentResolvers()(第 644-690 行)。7.0.9 的完整清单(按顺序):

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
	List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

	// Annotation-based argument resolution
	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
	resolvers.add(new RequestParamMapMethodArgumentResolver());
	resolvers.add(new PathVariableMethodArgumentResolver());
	resolvers.add(new PathVariableMapMethodArgumentResolver());
	resolvers.add(new MatrixVariableMethodArgumentResolver());
	resolvers.add(new MatrixVariableMapMethodArgumentResolver());
	resolvers.add(new ServletModelAttributeMethodProcessor(false));
	resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new RequestHeaderMapMethodArgumentResolver());
	resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new SessionAttributeMethodArgumentResolver());
	resolvers.add(new RequestAttributeMethodArgumentResolver());

	// Type-based argument resolution
	resolvers.add(new ServletRequestMethodArgumentResolver());
	resolvers.add(new ServletResponseMethodArgumentResolver());
	resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RedirectAttributesMethodArgumentResolver());
	resolvers.add(new ModelMethodProcessor());
	resolvers.add(new MapMethodProcessor());
	resolvers.add(new ErrorsMethodArgumentResolver());
	resolvers.add(new SessionStatusMethodArgumentResolver());
	resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
	resolvers.add(new ApiVersionMethodArgumentResolver());
	if (KotlinDetector.isKotlinPresent()) {
		resolvers.add(new ContinuationHandlerMethodArgumentResolver());
	}

	// Custom arguments
	if (getCustomArgumentResolvers() != null) {
		resolvers.addAll(getCustomArgumentResolvers());
	}

	// Catch-all
	resolvers.add(new PrincipalMethodArgumentResolver());
	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
	resolvers.add(new ServletModelAttributeMethodProcessor(true));

	return resolvers;
}

按职责分四组讲解:

  1. 注解驱动组(15 个):RequestParamMethodArgumentResolver(false) 处理 @RequestParam;@RequestParam Map 由 RequestParamMapMethodArgumentResolver;@PathVariable 与 @PathVariable Map 由一对处理器;@MatrixVariable 一对;@RequestBody 由 RequestResponseBodyMethodProcessor(它同时也是返回值处理器,后面讲);@RequestPart(multipart 表单里的文件/JSON part)由 RequestPartMethodArgumentResolver;@RequestHeader 一对;@CookieValue 由 ServletCookieValueMethodArgumentResolver;@Value 表达式由 ExpressionValueMethodArgumentResolver;@SessionAttribute/@RequestAttribute 各一个。
  2. 类型驱动组(10 个):直接按参数类型识别,与注解无关。ServletRequestMethodArgumentResolver 认 HttpServletRequest、MultipartRequest、ServletWebRequest 等;ServletResponseMethodArgumentResolver 认 HttpServletResponse、OutputStream、Writer;HttpEntityMethodProcessor 认 HttpEntity<X>/RequestEntity<X>;RedirectAttributesMethodArgumentResolver、ModelMethodProcessor、MapMethodProcessor(往 model 里塞数据)、ErrorsMethodArgumentResolver(校验错误对象,必须紧跟在 @Validated 参数后面)、SessionStatusMethodArgumentResolver、UriComponentsBuilderMethodArgumentResolver;以及 7.0 新增的 ApiVersionMethodArgumentResolver(注入当前解析出的 API 版本)。
  3. Catch-all 组(3 个,最后):PrincipalMethodArgumentResolver(认 java.security.Principal)、RequestParamMethodArgumentResolver(true)(兜底:没带任何注解的简单类型——int/long/String/Date 等——一律当请求参数处理)、ServletModelAttributeMethodProcessor(true)(兜底:没带注解的复合类型,当 model attribute 处理,会走数据绑定与校验)。

顺序极其重要:因为 Composite 是「第一个 supportsParameter 返回 true 的胜出」。例如一个没注解的 String 参数,会被 catch-all 的 RequestParamMethodArgumentResolver(true) 接住(当 query 参数),而不是被后面的 model attribute 处理器接住。这也是为什么「一个没加 @RequestParam 的 String 形参会自动从 query string 取值」——它不是魔法,是这个兜底解析器在起作用。

17.6.5 getMethodArgumentValues:逐参数解析与反射调用

参数解析的执行在 InvocableHandlerMethod(spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java)。注意这个类在 spring-web 而不是 spring-webmvc——它被 WebMVC 与 messaging 模块共用。

invokeForRequest(第 171-192 行):

public @Nullable Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
		@Nullable Object... providedArgs) throws Exception {

	@Nullable Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
	if (logger.isTraceEnabled()) {
		logger.trace("Arguments: " + Arrays.toString(args));
	}

	if (shouldValidateArguments() && this.methodValidator != null) {
		this.methodValidator.applyArgumentValidation(
				getBean(), getBridgedMethod(), getMethodParameters(), args, getValidationGroups());
	}

	Object returnValue = doInvoke(args);

	if (shouldValidateReturnValue() && this.methodValidator != null) {
		this.methodValidator.applyReturnValueValidation(
				getBean(), getBridgedMethod(), getReturnType(), returnValue, getValidationGroups());
	}

	return returnValue;
}

getMethodArgumentValues(第 200-238 行):

protected @Nullable Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
		@Nullable Object... providedArgs) throws Exception {

	MethodParameter[] parameters = getMethodParameters();
	if (ObjectUtils.isEmpty(parameters)) {
		return EMPTY_ARGS;
	}

	@Nullable Object[] args = new Object[parameters.length];
	for (int i = 0; i < parameters.length; i++) {
		MethodParameter parameter = parameters[i];
		parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
		args[i] = findProvidedArgument(parameter, providedArgs);
		if (args[i] != null) {
			continue;
		}
		if (parameter.getParameterType().equals(HandlerMethod.class) && parameter.isOptional()) {
			args[i] = null;
			continue;
		}
		if (!this.resolvers.supportsParameter(parameter)) {
			throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
		}
		try {
			args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				String exMsg = ex.getMessage();
				if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
					logger.debug(formatArgumentError(parameter, exMsg));
				}
			}
			throw ex;
		}
	}
	return args;
}

逐段:

  1. providedArgs 优先:findProvidedArgument 先看是否有「按类型直接给」的参数(比如 ServletRequestMethodArgumentResolver 处理的 HttpServletRequest 由上层直接传入),有就不用走 resolver。
  2. 参数名发现:initParameterNameDiscovery 让 MethodParameter 在需要时通过 ParameterNameDiscoverer(基于 ASM 读 class 元数据)拿到 arg0 之外的真实参数名——这是 @RequestParam 不写 value 时能自动用形参名的前提。
  3. Composite 的缓存:this.resolvers.supportsParameter 与 resolveArgument 都委托给 HandlerMethodArgumentResolverComposite。它内部有个 ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache——第一次解析某个参数后,「哪个 resolver 负责它」被缓存,后续请求同类型方法的同位置参数直接命中缓存,不再线性遍历 20 多个 resolver。
  4. 无 resolver 兜底失败:如果连 catch-all 都认不出该参数(比如一个既不是简单类型也不是 model attribute 的奇怪类型),抛 IllegalStateException("No suitable resolver")——这是启动后第一个请求就可能爆的错。

doInvoke(第 243-276 行):

protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception {
	Method method = getBridgedMethod();
	try {
		if (KOTLIN_REFLECT_PRESENT && KotlinDetector.isKotlinType(method.getDeclaringClass())) {
			if (KotlinDetector.isSuspendingFunction(method)) {
				return invokeSuspendingFunction(method, getBean(), args);
			}
			return KotlinDelegate.invokeFunction(method, getBean(), args);
		}
		return method.invoke(getBean(), args);
	}
	catch (IllegalArgumentException ex) {
		// ... 格式化错误信息
	}
	catch (InvocationTargetException ex) {
		// Unwrap for HandlerExceptionResolvers ...
		Throwable targetException = ex.getCause();
		if (targetException instanceof RuntimeException runtimeException) {
			throw runtimeException;
		}
		else if (targetException instanceof Error error) {
			throw error;
		}
		else if (targetException instanceof Exception exception) {
			throw exception;
		}
		else {
			throw new IllegalStateException(formatInvokeError("Invocation failure", targetException));
		}
	}
}

关键细节:method.invoke 抛的 InvocationTargetException 会把业务真实异常包在 cause 里。这里直接拆包——RuntimeException/Error/Exception 原样抛出。这就是为什么 handler 里抛的业务异常(比如 IllegalArgumentException)能直接被 @ExceptionHandler 接住,而不是一个 InvocationTargetException。

17.7 源码剖析(五):返回值处理与 HttpMessageConverter 调用链

方法反射调用拿到返回值之后,下一站是 ServletInvocableHandlerMethod.invokeAndHandle。它决定「这个返回值最终是走视图渲染,还是直接写消息体」。

17.7.1 invokeAndHandle:返回值分派的总闸

ServletInvocableHandlerMethod(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java 第 114-144 行):

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
		@Nullable Object... providedArgs) throws Exception {

	Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
	setResponseStatus(webRequest);

	if (returnValue == null) {
		if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
			disableContentCachingIfNecessary(webRequest);
			mavContainer.setRequestHandled(true);
			return;
		}
	}
	else if (StringUtils.hasText(getResponseStatusReason())) {
		mavContainer.setRequestHandled(true);
		return;
	}

	mavContainer.setRequestHandled(false);
	Assert.state(this.returnValueHandlers != null, "No return value handlers");
	try {
		this.returnValueHandlers.handleReturnValue(
				returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
	}
	catch (Exception ex) {
		if (logger.isTraceEnabled()) {
			logger.trace(formatErrorForReturnValue(returnValue), ex);
		}
		throw ex;
	}
}

逐段:

  1. 先 invokeForRequest 拿到返回值(上一节讲过的参数解析 + 反射调用)。
  2. setResponseStatus(第 149-168 行):读方法上的 @ResponseStatus 注解。有 reason 则 response.sendError(code, reason);否则 response.setStatus(code);同时把状态码放进 request 属性 View.RESPONSE_STATUS_ATTRIBUTE(供 RedirectView 等使用)。
  3. 返回值为 null 的三种「直接结束」情形:304 Not Modified、已设状态码、或 mavContainer 已被标记为 requestHandled(比如 @ResponseBody 返回 null)——此时把 requestHandled 设为 true 并 return,不再走返回值处理器。这就是为什么 @ResponseBody 方法返回 null 不会触发视图渲染。
  4. @ResponseStatus 带 reason 但返回值非 null:直接 requestHandled,不处理返回值。
  5. 否则交给 Composite 返回值处理器:handleReturnValue 内部按顺序找第一个 supportsReturnType 返回 true 的处理器。

17.7.2 内置返回值处理器清单

getDefaultReturnValueHandlers(第 730-776 行):

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
	List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

	ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler =
			new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
					this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
					initViewResolvers(), initLocaleResolver());

	HttpEntityMethodProcessor httpEntityMethodProcessor = new HttpEntityMethodProcessor(getMessageConverters(),
			this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors);

	// Single-purpose return value types
	handlers.add(new ModelAndViewMethodReturnValueHandler());
	handlers.add(new ModelMethodProcessor());
	handlers.add(new ViewMethodReturnValueHandler());
	handlers.add(responseBodyEmitterHandler);
	handlers.add(new StreamingResponseBodyReturnValueHandler());
	handlers.add(new ResponseEntityReturnValueHandler(httpEntityMethodProcessor, responseBodyEmitterHandler));
	handlers.add(new HttpHeadersReturnValueHandler());
	handlers.add(new CallableMethodReturnValueHandler());
	handlers.add(new DeferredResultMethodReturnValueHandler());
	handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

	// Annotation-based return value types
	handlers.add(new ServletModelAttributeMethodProcessor(false));
	handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
			this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));

	// Multi-purpose return value types
	handlers.add(new ViewNameMethodReturnValueHandler());
	handlers.add(new MapMethodProcessor());

	// Custom return value types
	if (getCustomReturnValueHandlers() != null) {
		handlers.addAll(getCustomReturnValueHandlers());
	}

	// Catch-all
	if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
		handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
	}
	else {
		handlers.add(new ServletModelAttributeMethodProcessor(true));
	}

	return handlers;
}

按组理解:

  1. 单用途组:返回 ModelAndView 由 ModelAndViewMethodReturnValueHandler 处理;返回 Model/Map 由 ModelMethodProcessor;返回 View 由 ViewMethodReturnValueHandler;返回 ResponseBodyEmitter/StreamingResponseBody/ResponseEntity/HttpHeaders/Callable/DeferredResult/WebAsyncTask 各有专属处理器。
  2. 注解组:@ModelAttribute 但没 @ResponseBody 的由 ServletModelAttributeMethodProcessor(false)(把返回值塞进 model);@ResponseBody 由 RequestResponseBodyMethodProcessor——注意它既是参数解析器又是返回值处理器。
  3. 多用途组:返回字符串且不是 @ResponseBody 的,由 ViewNameMethodReturnValueHandler 解释为视图名;返回 Map 塞 model。
  4. Catch-all:最后那个 ServletModelAttributeMethodProcessor(true)——任何没被前面处理器认出来的返回值,一律塞进 model 当 model attribute。这就是为什么「一个普通 POJO 作为返回值且没加任何注解」会被当成 model attribute 暴露给视图。

HandlerMethodReturnValueHandlerComposite.handleReturnValue(spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodReturnValueHandlerComposite.java 第 70-91 行):

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
		ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

	HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
	if (handler == null) {
		throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
	}
	handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

private @Nullable HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
	boolean isAsyncValue = isAsyncReturnValue(value, returnType);
	for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
		if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
			continue;
		}
		if (handler.supportsReturnType(returnType)) {
			return handler;
		}
	}
	return null;
}

注意它与参数 Composite 的区别:返回值 Composite 没有缓存——因为返回值类型组合多、且每次请求都要重新判断。另外有个 isAsyncValue 短路:如果返回值是异步类型(DeferredResult/Callable 等),跳过所有非异步处理器,确保走到 DeferredResultMethodReturnValueHandler 这类异步处理器。

17.7.3 RequestResponseBodyMethodProcessor:进入消息转换器

@ResponseBody 的处理最终调到 AbstractMessageConverterMethodProcessor.writeWithMessageConverters(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java 第 205-368 行)。这是消息协商与转换器选择的核心。节选关键段:

MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
	selectedMediaType = contentType;
}
else {
	HttpServletRequest request = inputMessage.getServletRequest();
	List<MediaType> acceptableTypes;
	try {
		acceptableTypes = getAcceptableMediaTypes(request);
	}
	catch (HttpMediaTypeNotAcceptableException ex) {
		// 4xx/5xx 错误响应时忽略协商异常,直接 return
	}

	List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
	if (body != null && producibleTypes.isEmpty()) {
		throw new HttpMessageNotWritableException(
				"No converter found for return value of type: " + valueType);
	}

	List<MediaType> compatibleMediaTypes = determineCompatibleMediaTypes(acceptableTypes, producibleTypes);

	// For ProblemDetail, fall back on RFC 9457 format
	if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
		compatibleMediaTypes = determineCompatibleMediaTypes(PROBLEM_MEDIA_TYPES, producibleTypes);
	}

	if (compatibleMediaTypes.isEmpty()) {
		if (body != null) {
			throw new HttpMediaTypeNotAcceptableException(producibleTypes);
		}
		return;
	}

	MimeTypeUtils.sortBySpecificity(compatibleMediaTypes);

	for (MediaType mediaType : compatibleMediaTypes) {
		if (mediaType.isConcrete()) {
			selectedMediaType = mediaType;
			break;
		}
		else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
			selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
			break;
		}
	}
}

if (selectedMediaType != null) {
	selectedMediaType = selectedMediaType.removeQualityValue();

	ResolvableType targetResolvableType = null;
	for (HttpMessageConverter converter : this.messageConverters) {
		ConverterType converterTypeToUse = null;
		if (converter instanceof GenericHttpMessageConverter genericConverter) {
			if (genericConverter.canWrite(targetType, valueType, selectedMediaType)) {
				converterTypeToUse = ConverterType.GENERIC;
			}
		}
		else if (converter instanceof SmartHttpMessageConverter smartConverter) {
			targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forType(targetType));
			if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) {
				converterTypeToUse = ConverterType.SMART;
			}
		}
		else if (converter.canWrite(valueType, selectedMediaType)){
			converterTypeToUse = ConverterType.BASE;
		}
		if (converterTypeToUse != null) {
			body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
					(Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
			if (body != null) {
				addContentDispositionHeader(inputMessage, outputMessage);
				switch (converterTypeToUse) {
					case BASE -> converter.write(body, selectedMediaType, outputMessage);
					case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage);
					case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType,
							selectedMediaType, outputMessage, getAdvice().determineWriteHints(...));
				}
			}
			return;
		}
	}
}

逐段:

  1. Content-Type 预设优先:如果 response 已经被显式 setContentType 且是具体类型(比如 handler 里自己设了 application/pdf),直接用它,跳过协商。
  2. acceptableTypes vs producibleTypes:getAcceptableMediaTypes 从 Accept 头(通过 ContentNegotiationManager)解析出客户端能接受的类型;getProducibleMediaTypes 算出「当前有哪些转换器能写这个 valueType」。两者取交集得到 compatibleMediaTypes。
  3. produces 注解的作用:@RequestMapping(produces="application/json") 会把 producibleTypes 收窄到 json——这就是为什么 produces 能强制返回类型。
  4. 协商失败:compatibleMediaTypes 为空且 body 非 null,抛 HttpMediaTypeNotAcceptableException(406)。ProblemDetail 有特殊兜底(RFC 9457 的 application/problem+json)。
  5. 按 specificity 排序选最具体的:MimeTypeUtils.sortBySpecificity 后,挑第一个具体类型;如果是 */* 这种泛类型,回退成 octet-stream。
  6. 转换器遍历:按 messageConverters 列表顺序,第一个 canWrite 返回 true 的胜出。三种 canWrite 分支对应三种转换器接口:老的 HttpMessageConverter(BASE)、带泛型的 GenericHttpMessageConverter(GENERIC)、7.0 引入的 SmartHttpMessageConverter(SMART,带 hints)。
  7. beforeBodyWrite 钩子:在真正 write 之前,调 RequestBodyAdvice.beforeBodyWrite——这是全局请求体增强的扩展点。
  8. 写出去:按分支调对应 write 方法,然后 return(找到第一个就停)。遍历完所有 converter 都没 canWrite 的,最后抛 HttpMessageNotWritableException(500)或 HttpMediaTypeNotAcceptableException。

17.7.4 AbstractHttpMessageConverter:canRead/canWrite/read/write 模板

AbstractHttpMessageConverter<T>(spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java)是绝大多数具体转换器的基类。canRead/canWrite(第 132-186 行):

@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
	return supports(clazz) && canRead(mediaType);
}

protected boolean canRead(@Nullable MediaType mediaType) {
	if (mediaType == null) {
		return true;
	}
	for (MediaType supportedMediaType : getSupportedMediaTypes()) {
		if (supportedMediaType.includes(mediaType)) {
			return true;
		}
	}
	return false;
}

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
	return supports(clazz) && canWrite(mediaType);
}

protected boolean canWrite(@Nullable MediaType mediaType) {
	if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
		return true;
	}
	for (MediaType supportedMediaType : getSupportedMediaTypes()) {
		if (supportedMediaType.isCompatibleWith(mediaType)) {
			return true;
		}
	}
	return false;
}

模板方法 read/write(第 193-235 行):

@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
		throws IOException, HttpMessageNotReadableException {

	return readInternal(clazz, inputMessage);
}

@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
		throws IOException, HttpMessageNotWritableException {

	final HttpHeaders headers = outputMessage.getHeaders();
	addDefaultHeaders(headers, t, contentType);

	if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
		streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
			@Override
			public void writeTo(OutputStream outputStream) throws IOException {
				writeInternal(t, new HttpOutputMessage() {
					@Override
					public OutputStream getBody() {
						return outputStream;
					}
					@Override
					public HttpHeaders getHeaders() {
						return headers;
					}
				});
			}
			@Override
			public boolean repeatable() {
				return supportsRepeatableWrites(t);
			}
		});
	}
	else {
		writeInternal(t, outputMessage);
		outputMessage.getBody().flush();
	}
}

要点:

  1. supports(clazz) 由子类实现(判断能不能处理这个 Java 类型);canRead/canWrite(MediaType) 由基类实现(遍历 supportedMediaTypes)。两者与运算才是完整的 canRead。
  2. read 是 final:模板方法模式,子类只能实现 readInternal/writeInternal。
  3. write 处理流式输出:如果 outputMessage 是 StreamingHttpOutputMessage(用于 ResponseBodyEmitter/SseEmitter 这种边生成边写的场景),不立即写,而是把 writeInternal 包成一个 Body 留给容器在异步阶段调用;否则同步写并 flush。
  4. addDefaultHeaders(第 244-271 行):没设 Content-Type 时取 getDefaultContentType(默认是 supportedMediaTypes 的第一个),并附加 defaultCharset、Content-Length。

17.7.5 几个常用转换器的默认 supportedMediaTypes

  • StringHttpMessageConverter(spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java):构造函数 super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL),默认 charset 是 ISO_8859_1(第 51 行 DEFAULT_CHARSET = StandardCharsets.ISO_8859_1)。也就是说,默认情况下 @ResponseBody 返回 String 时 Content-Type 是 text/plain;charset=ISO-8859-1——中文会乱码,这是历史遗留,需要显式配 charset 或用 produces。
  • ByteArrayHttpMessageConverter:super(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)——字节数组默认当 octet-stream。
  • MappingJackson2HttpMessageConverter(spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java):默认 supportedMediaTypes 包含 application/json、application/*+json。canWrite 命中后,writeInternal 内部用 Jackson ObjectMapper.writeValue 把对象序列化成 JSON 字节流。

7.0.9 服务端默认转换器注册顺序由 HttpMessageConverters.forServer() 文档明确列出(spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java 第 67-84 行):ByteArray → String → Resource → ResourceRegion → Kotlin Serialization JSON → JSON(Jackson)→ Smile → Kotlin CBOR → CBOR → YAML → XML → Protobuf → ATOM/RSS → Multipart。这个顺序决定了「谁先被 canWrite 命中」:比如返回 String 时,StringHttpMessageConverter 在 Jackson 前面,所以不会落到 JSON 转换器——这就是为什么返回纯文本与返回对象走的是不同转换器。

17.8 源码剖析(六):视图解析、异常解析、异步与 Multipart

17.8.1 ViewResolver 链与 UrlBasedViewResolver

DispatcherServlet 的 render 通过 resolveViewNameInternal 顺序遍历 viewResolvers:

private @Nullable View resolveViewNameInternal(String viewName, Locale locale) throws Exception {
	if (this.viewResolvers != null) {
		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				return view;
			}
		}
	}
	return null;
}

UrlBasedViewResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.java)是最常用的基类。它的 createView(第 449 行起)先做 prefix/suffix 拼接:视图名 "user/list" 经过 setPrefix("/WEB-INF/views/") + setSuffix(".jsp") 变成 /WEB-INF/views/user/list.jsp,再通过 requiredViewClass() 校验(InternalResourceViewResolver 要求是 InternalResourceView 子类),最后反射实例化 View 并把 URL、contentType 等属性塞进去。

InternalResourceView 的 renderMergedOutputModel 做的事:把 model 里每个 key-value request.setAttribute(key, value),然后 request.getRequestDispatcher(forwardUrl).forward(request, response)。这就是 JSP 视图的完整渲染路径。

ContentNegotiatingViewResolver 是个特殊的「复合」ViewResolver:它不自己渲染,而是根据 Accept 头从所有 ViewResolver 里挑出最合适的 View(甚至可以返回 RequestResponseBodyMethodProcessor 用的那种消息转换视图)。它通常要被配在 ViewResolver 链的最前面(order 最高)。

17.8.2 ExceptionHandlerExceptionResolver:@ExceptionHandler 的解析

ExceptionHandlerExceptionResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java)维护两个缓存:

  • exceptionHandlerCache(第 117 行):Map<Class<?>, ExceptionHandlerMethodResolver>——按 controller 类缓存「本类内部 @ExceptionHandler 方法的查找表」。
  • exceptionHandlerAdviceCache(第 120 行):Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>——按 @ControllerAdvice bean 缓存。

doResolveHandlerMethodException 的逻辑:先在当前 handler 所在类的 ExceptionHandlerMethodResolver 里找能处理这个异常类型的方法(ExceptionHandlerMethodResolver 内部维护 Map<Class<?>, Method>,按异常类型继承链精确匹配);找不到再遍历 exceptionHandlerAdviceCache 里所有 @ControllerAdvice,按 @ControllerAdvice 的 basePackages/assignableTypes 等过滤条件判断是否适用于当前 controller。找到后,把该方法包装成 ServletInvocableHandlerMethod,复用与正常 handler 相同的参数解析器与返回值处理器去调用——这就是为什么 @ExceptionHandler 方法也能用 @RequestBody、@PathVariable 这些参数。

17.8.3 ResponseStatusExceptionResolver 与 DefaultHandlerExceptionResolver

ResponseStatusExceptionResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java)的 doResolveException(节选):

if (ex instanceof ResponseStatusException rse) {
	return resolveResponseStatusException(rse, request, response, handler);
}
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
	return resolveResponseStatus(status, request, response, handler, ex);
}

它处理两种情况:异常本身是 ResponseStatusException(5.0+),或异常类上标了 @ResponseStatus。最终调 response.sendError(statusCode) 或 sendError(code, reason)(第 150、156 行)——注意 sendError 会触发容器的错误页机制。返回的 ModelAndView 是空的(new ModelAndView()),DispatcherServlet 看到 exMv.isEmpty() 后返回 null,不再渲染。

DefaultHandlerExceptionResolver(spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java)是一张「标准异常 → HTTP 状态码」的硬编码映射表:NoSuchRequestHandlingMethodException→404、HttpRequestMethodNotSupportedException→405(sendError 并设 Allow 头)、HttpMediaTypeNotSupportedException→415、HttpMediaTypeNotAcceptableException→406、MissingServletRequestParameterException→400、MissingPathVariableException→500、NoHandlerFoundException→404、AsyncRequestTimeoutException→503 等。405 的源码答案就在这里:当一个 URL 映射到了某个 controller 但 HTTP method 不在该方法的 consumes/method 列表里时,RequestMappingInfoHandlerMapping 会抛 HttpRequestMethodNotSupportedException,由本 resolver 翻译成 405 并设 Allow 响应头。

17.8.4 WebAsyncManager:三线程异步模型

WebAsyncManager(spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java)的类 javadoc 把模型讲得很清楚(第 43-51 行):

异步场景以一个普通线程(T1)开始请求处理。调用 startCallableProcessing 或 startDeferredResultProcessing 会在另一个线程(T2)产生结果。结果被保存后,请求被分派回容器,在第三个线程(T3)里用保存的结果恢复处理。在 dispatch 线程(T3)里,结果通过 getConcurrentResult() 访问。

对应 doDispatch 里的几个分叉点:

  1. T1(容器线程):handler 方法返回 Callable/DeferredResult,由 CallableMethodReturnValueHandler/DeferredResultMethodReturnValueHandler 调到 asyncManager.startCallableProcessing/startDeferredResultProcessing。这些方法在 T2 上执行业务逻辑,完成时把结果存起来。
  2. T1 的 doDispatch 立即 return:因为 isConcurrentHandlingStarted() 返回 true,doDispatch 不渲染、不 afterCompletion,只调 applyAfterConcurrentHandlingStarted 通知拦截器「异步开始了」。
  3. T3(容器恢复 dispatch):容器再次把请求 dispatch 进 DispatcherServlet(DispatcherType.ASYNC)。invokeHandlerMethod 里检测到 hasConcurrentResult(),把异步结果当成「方法返回值」重新走返回值处理器,完成真正的渲染或消息写回。

超时由 AsyncWebRequest.setTimeout 控制;超时后 WebAsyncManager 会触发 CallableProcessingInterceptor/DeferredResultProcessingInterceptor 的超时回调,最终抛 AsyncRequestTimeoutException 被 DefaultHandlerExceptionResolver 翻译成 503。

17.8.5 StandardServletMultipartResolver

StandardServletMultipartResolver(spring-web/src/main/java/org/springframework/web/multipart/support/StandardServletMultipartResolver.java)非常薄:

@Override
public boolean isMultipart(HttpServletRequest request) {
	return StringUtils.startsWithIgnoreCase(request.getContentType(),
			(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}

@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
	return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
	if (!(request instanceof AbstractMultipartHttpServletRequest abstractMultipartHttpServletRequest) ||
			abstractMultipartHttpServletRequest.isResolved()) {
		try {
			for (Part part : request.getParts()) {
				part.delete();
			}
		}
		catch (Throwable ex) {
			LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
		}
	}
}

要点:

  1. isMultipart 只看 Content-Type 是否 multipart/ 开头。strictServletCompliance 为 true 时只认 multipart/form-data(更严格的 Servlet 规范对齐)。
  2. resolveMultipart 就是包一个 StandardMultipartHttpServletRequest——它内部延迟调用 request.getParts() 枚举 Servlet 容器解析好的 part,包装成 MultipartFile。真正的解析器是 Servlet 容器(Tomcat 等),Spring 只是适配。
  3. cleanupMultipart:遍历所有 part 调 part.delete() 删除临时文件。这就是 doDispatch finally 里调用的那个清理。
  4. resolveLazily:默认 false,立即解析(解析失败立即抛 MultipartException);true 则延迟到首次访问 part 时才解析。

17.9 运行机制:全链路时序图与状态流转

把前面逐段拆解的源码串成一张图。下面是一次同步 HTTP 请求(GET /users/1?detail=true,返回 JSON)从容器到响应写回的完整调用栈:

Servlet 容器 (T1)
   │
   ▼
FrameworkServlet.service(request, response)
   │  HTTP_SERVLET_METHODS.contains("GET")? ──yes──▶ super.service
   │                                                    │
   ▼                                                    ▼
doGet ────────────────────────────────────────▶ processRequest
                                                     │
                                       ① LocaleContextHolder.setLocaleContext
                                       ② RequestContextHolder.setRequestAttributes
                                       ③ asyncManager.registerCallableInterceptor(RequestBindingInterceptor)
                                                     │
                                                     ▼
                                          DispatcherServlet.doService
                                                     │
                                       ④ attributesSnapshot (include 请求才需要)
                                       ⑤ 暴露 WEB_APPLICATION_CONTEXT_ATTRIBUTE / LOCALE_RESOLVER_ATTRIBUTE
                                       ⑥ flashMapManager.retrieveAndUpdate
                                       ⑦ ServletRequestPathUtils.parseAndCache (parseRequestPath=true 时)
                                                     │
                                                     ▼
                                          DispatcherServlet.doDispatch
                                                     │
                     ┌─────────────────────────────────┼──────────────────────────────────┐
                     ▼                                 ▼                                  ▼
          checkMultipart(request)              getHandler(request)                  applyPreHandle
          multipartResolver.isMultipart?      遍历 handlerMappings:                  for interceptor:
            yes → resolveMultipart()           RequestMappingHandlerMapping            interceptor.preHandle
            no  → 原 request                      .getHandlerInternal                 false → triggerAfterCompletion
                                                   返回 HandlerExecutionChain;            → return
                                                 (装配 adaptedInterceptors /           true  → interceptorIndex=i
                                                 MappedInterceptor / CORS)
                                                     │
                                                     ▼
                                          getHandlerAdapter(handler)
                                          遍历 handlerAdapters, supports(handler)=true
                                          → RequestMappingHandlerAdapter
                                                     │
                                                     ▼
                                          ha.handle(...)
                                                     │
                                                     ▼
                                          RequestMappingHandlerAdapter.handleInternal
                                                     │
                                                     ▼
                                          invokeHandlerMethod
                                            │
                                            ├─ modelFactory.initModel (跑 @ModelAttribute 方法)
                                            ├─ invocableMethod.invokeAndHandle(webRequest, mavContainer)
                                            │       │
                                            │       ▼
                                            │   InvocableHandlerMethod.invokeForRequest
                                            │       │
                                            │       ├─ getMethodArgumentValues
                                            │       │     for 参数 i:
                                            │       │        Composite.getArgumentResolver (带缓存)
                                            │       │        → RequestParamMethodArgumentResolver 等
                                            │       │        → resolver.resolveArgument(...)
                                            │       │
                                            │       └─ doInvoke → method.invoke(controller, args)
                                            │             (拆包 InvocationTargetException)
                                            │
                                            └─ returnValueHandlers.handleReturnValue
                                                  │
                                                  ▼
                                          RequestResponseBodyMethodProcessor.handleReturnValue
                                                  │
                                                  ▼
                                          writeWithMessageConverters
                                                  │
                                  acceptableTypes(Accept) × producibleTypes(转换器能写的)
                                                  │
                                          selectedMediaType = application/json
                                                  │
                                          遍历 messageConverters:
                                            MappingJackson2HttpMessageConverter.canWrite=true
                                                  │
                                          beforeBodyWrite 钩子
                                                  │
                                          converter.write → Jackson 序列化为 JSON → response
                                          mavContainer.setRequestHandled(true)
                                                     │
                                                     ▼
                                          getModelAndView(mavContainer, ...) → null (requestHandled)
                                                     │
                                                     ▼
                                          applyDefaultViewName(mv==null, 跳过)
                                          applyPostHandle (逆序 postHandle)
                                                     │
                                                     ▼
                                          processDispatchResult
                                            │
                                            ├─ exception==null?
                                            │     mv==null (requestHandled) → 不 render
                                            │     mappedHandler.triggerAfterCompletion(逆序, 从 interceptorIndex)
                                            ▼
                                          (doDispatch finally)
                                            └─ multipartRequestParsed? cleanupMultipart
                                                     │
                                                     ▼
                                          processRequest finally
                                            ├─ resetContextHolders (还原 previous*)
                                            ├─ requestAttributes.requestCompleted()
                                            └─ publishRequestHandledEvent
                                                     │
                                                     ▼
                                               response 返回容器

17.9.1 状态流转的关键状态点

整条链路上有几个「隐式状态」决定了下一步走向,理解它们比记方法名更重要:

  1. multipartRequestParsed(doDispatch 局部布尔):决定 finally 是否 cleanup。它不影响 handler 选择,只影响资源释放。
  2. interceptorIndex(HandlerExecutionChain 字段):决定 afterCompletion 回调范围。preHandle 每成功一个 +1;短路时停在最后一个成功的位置。
  3. mavContainer.requestHandled:决定是否走视图渲染。@ResponseBody、@ResponseStatus、304、返回 null 等都会把它设为 true;为 true 时 doDispatch 的 processDispatchResult 看到 mv==null 直接跳过 render。
  4. asyncManager.isConcurrentHandlingStarted():决定是否跳过 postHandle/render/afterCompletion。异步一旦启动,T1 的收尾全部简化为「通知拦截器 afterConcurrentHandlingStarted」。
  5. dispatchException(doDispatch 局部):非 null 时进入 processHandlerException,走 resolver 链。resolver 链全空才把异常重新抛出。

17.9.2 为什么这样设计:变化点分析

整条链路本质上是「用适配器模式 + 责任链模式」把四个变化点隔离开:

变化点谁负责扩展方式
URL → handler 的映射规则HandlerMapping自定义 HandlerMapping / 实现 WebMvcConfigurer 加拦截器
handler 形态(Controller 接口 / HttpRequestHandler / 注解方法 / RouterFunction)HandlerAdapter一般不需要自定义,注解场景只用 RequestMappingHandlerAdapter
参数从请求里怎么取HandlerMethodArgumentResolver实现 WebMvcConfigurer.addArgumentResolvers
返回值怎么写出去HandlerMethodReturnValueHandler + HttpMessageConverter自定义返回值处理器 / 注册新的 HttpMessageConverter
异常怎么变成响应HandlerExceptionResolver@ControllerAdvice / 自定义 resolver
视图名怎么变成页面ViewResolver自定义 ViewResolver

DispatcherServlet 本身对这些变化点一无所知,它只按固定顺序调用接口方法。这就是为什么 SpringMVC 能在不改 DispatcherServlet 一行代码的前提下,从 JSP 时代演进到 REST JSON、再到函数式路由、再到异步响应式。

17.9.3 线程上下文的生命周期

RequestContextHolder/LocaleContextHolder 是 ThreadLocal。整条链对它的管理有一条铁律:谁绑定,谁还原。FrameworkServlet.processRequest 在开头绑定、在 finally 还原;异步场景下 RequestBindingInterceptor 保证 T2(Callable 执行线程)在执行业务前后也绑定/还原。因此:

  • 同步业务代码里任何一层都能 RequestContextHolder.getRequestAttributes() 拿到当前请求。
  • 但如果你在业务代码里 new Thread() 自己起线程,那个线程里没有绑定——必须显式包装或用 Spring 的 TaskDecorator 传递。
  • 请求结束后 ThreadLocal 被还原成 previous 值,线程归还容器线程池,下一个请求拿到的是干净上下文。

17.10 本章小结

本章从 Servlet 继承链讲起,一路拆到:

  1. HttpServletBean 用 BeanWrapper 把 init-param 变成 bean 属性,再调模板方法 initServletBean。
  2. FrameworkServlet.processRequest 负责请求级 ThreadLocal 的绑定与还原,所有 HTTP method 统一收口到 doService。
  3. DispatcherServlet.initStrategies 装配九大策略,默认值来自 DispatcherServlet.properties。
  4. doDispatch 是主流水线:checkMultipart → getHandler → applyPreHandle → getHandlerAdapter → ha.handle → applyDefaultViewName → applyPostHandle → processDispatchResult(异常解析 + render)→ afterCompletion。
  5. HandlerMapping 体系:AbstractHandlerMapping.getHandler 是 final 模板,子类实现 getHandlerInternal;RequestMappingHandlerMapping 用 isHandler 扫 @Controller,7.0.9 默认 PathPatternParser。
  6. 参数解析:RequestMappingHandlerAdapter 装配 27 个内置参数解析器(注解组 + 类型组 + catch-all),InvocableHandlerMethod.getMethodArgumentValues 逐个解析,Composite 带缓存。
  7. 返回值处理:ServletInvocableHandlerMethod.invokeAndHandle 决定是否 requestHandled;@ResponseBody 走 RequestResponseBodyMethodProcessor → writeWithMessageConverters 做媒体类型协商,再顺序遍历 HttpMessageConverter 选第一个 canWrite 的。