Spring 源码探险:Spring MVC 的请求之旅——从一次 HTTP 请求到响应
对于绝大多数开发者而言,Spring MVC 就是一个神奇的“黑洞”。我们在浏览器地址栏输入 http://localhost:8080/users/1
,回车。片刻之后,一个渲染着用户信息的漂亮页面就呈现在眼前。
请求进去了,响应出来了。
但在这短短的几十毫秒内,这个请求在服务器内部经历了什么?当它穿过 Tomcat 的大门后,是如何被 Spring 接管的?它又是如何像长了眼睛一样,精确地找到了我们项目中那个用 @GetMapping("/users/{id}")
标注的方法?方法返回的 ModelAndView
对象,又是如何变成浏览器可以理解的 HTML 的?
今天,我们不当旁观者。我们将手持电筒,亲自跳入 DispatcherServlet
这个“黑洞”,以一次请求的视角,追踪它在 Spring MVC 管道中的每一步漂流,绘制出它从一次 HTTP 请求到最终响应的完整航海图。
一、DispatcherServlet
——一切的入口与调度中心
一切 Web 请求在 Spring 世界的旅程,都始于一个貌不惊人却至关重要的 Servlet——DispatcherServlet
。在任何一个 Spring MVC 应用的 web.xml
(或通过 ServletContainerInitializer
的Java配置) 中,它都被注册为核心的请求分发器。当 Tomcat 等 Servlet 容器接收到匹配其 URL-pattern 的请求时,就会调用它的 service()
方法。
而 DispatcherServlet
的核心,都汇聚在它的 doDispatch
方法中。这个方法,就是 Spring MVC 的“refresh()
”,是整个请求处理流程的“心跳”与“主引擎”。
- 源码路径:
org.springframework.web.servlet.DispatcherServlet#doDispatch
// DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
// ...
try {
// ...
// 1. 寻找Handler: 核心步骤之一
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 获取适配的HandlerAdapter: 核心步骤之二
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ... 省略 ETag 等前置处理
// 3. 实际执行Handler: 核心步骤之三
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ... 省略
// 4. 处理分发结果 (渲染视图或处理异常)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// ...
}
doDispatch
的代码逻辑异常清晰,它就像一个经验丰富的项目经理,将整个请求处理任务拆分成了几个明确的阶段,并依次分派给专业的“组件”去完成。我们的探险,就将沿着这四个核心步骤展开。
二、寻找正确的处理人——HandlerMapping
当一个请求(例如 /users/1
)进入 doDispatch
后,首要任务是:我该把这个请求交给谁处理?
这个艰巨的任务,落在了 getHandler(request)
方法上,它会遍历容器中所有注册的 HandlerMapping
实现类,直到找到一个能处理当前请求的为止。
在现代 Spring Boot 应用中,挑大梁的通常是 RequestMappingHandlerMapping
。
- 源码路径:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
准备阶段:启动时扫描
RequestMappingHandlerMapping
在容器启动时(作为 InitializingBean
),就会扫描所有被 @Controller
或 @RequestMapping
标注的 Bean,然后解析其中的每一个方法(@GetMapping
, @PostMapping
等),将注解中的信息(URL 路径、HTTP 方法、请求头、参数等)提取出来,封装成一个 RequestMappingInfo
对象。
最终,它会将 RequestMappingInfo
和处理该请求的具体方法(封装为 HandlerMethod
对象)之间的映射关系,缓存在一个 Map
中。这就像是为整个应用的所有“API接口”建立了一份详尽的“电话黄页”。
匹配阶段:getHandler(request)
当 getHandler
方法被调用时,RequestMappingHandlerMapping
的 lookupHandlerMethod
方法会被触发。它的工作流程如下:
- 构建查询 Key: 从当前的
HttpServletRequest
中提取出 URL 路径(如/users/1
)。 - 直接匹配: 首先尝试用完整的 URL 路径去“电话黄页”(Map)中进行快速查找。如果能直接找到,效率最高。
- 模式匹配: 如果直接查找失败(比如对于
/users/1
这样的路径变量),它会遍历所有注册的RequestMappingInfo
,利用AntPathMatcher
进行路径匹配(判断/users/1
是否符合/users/{id}
的模式),并检查 HTTP Method 等其他条件是否也满足。 - 寻找最佳匹配: 可能会有多个模式都能匹配上(例如
/users/*
和/users/{id}
都匹配/users/1
),此时 Spring 会通过一套复杂的排序算法,找到“最佳匹配”(Best Match)的那个。通常,路径更具体的那个会胜出。 - 封装返回: 一旦找到唯一的、最佳的
HandlerMethod
,Spring 会将它连同适用于它的所有拦截器(HandlerInterceptor
),一同封装进一个HandlerExecutionChain
对象中返回。
至此,请求的第一站完成。DispatcherServlet
已经成功地为 /users/1
这个请求,找到了它的“处理人”—— 我们 Controller 中的那个 getUserById(Long id)
方法。
三、执行处理人——HandlerAdapter
与参数解析
找到了 HandlerMethod
,下一个问题是:如何调用这个方法?
直接用反射调用吗?可以,但不优雅。因为 Controller 的方法签名千奇百怪:有的方法没有参数,有的有 @PathVariable
,有的有 @RequestBody
,还有的需要 HttpServletRequest
本身…… 如果 DispatcherServlet
自己去写 if-else
来处理这些,代码会变得臃肿不堪。
于是,适配器模式登场了。HandlerAdapter
的使命,就是作为 DispatcherServlet
和各种各样的 Handler 之间的桥梁。
getHandlerAdapter()
方法会找到能处理 HandlerMethod
的那个适配器,也就是 RequestMappingHandlerAdapter
。
- 源码路径:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handle
handle
方法会委托给 invokeHandlerMethod
,这里是整个 Spring MVC 中最精密、最复杂的部分。
// RequestMappingHandlerAdapter.java
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// ...
// 获取方法参数解析器
mav.setViewName(this.getWebBindingInitializer().getArgumentResolvers()....);
// ...
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
// ...
// 核心:调用方法并处理返回值
invocableMethod.invokeAndHandle(webRequest, mavContainer, ...);
return getModelAndView(mavContainer, ...);
}
invokeAndHandle
方法内部的逻辑可以概括为两步:
getMethodArgumentValues()
:解析方法参数- 这一步是 Spring MVC “魔法”的核心。它会遍历
HandlerMethod
的所有参数。 - 对于每一个参数,它会从一个
HandlerMethodArgumentResolver
(方法参数解析器)的列表中,找到支持解析该参数的解析器。 - 例如:
- 看到
@RequestParam
注解的参数,RequestParamMethodArgumentResolver
就会上场,从request.getParameter()
中取值。 - 看到
@PathVariable
注解的参数,PathVariableMethodArgumentResolver
就会上场,从之前HandlerMapping
匹配时解析出的 URI 模板变量中取值。 - 看到
@RequestBody
注解的参数,RequestResponseBodyMethodProcessor
就会上场,它会使用注册的HttpMessageConverter
(比如MappingJackson2HttpMessageConverter
)读取请求体中的 JSON 串,并反序列化成对应的 Java 对象。
- 看到
- 所有参数都被解析成 Java 对象后,会存入一个
Object[]
数组。 doInvoke(args)
:真正执行方法- 万事俱备,只欠东风。
doInvoke
会使用 Java 反射,用上一步准备好的参数数组,调用我们 Controller 中的那个HandlerMethod
。 handlerMethod.invoke(bean, args)
- 执行完成后,获取到方法的返回值(可能是
String
、ModelAndView
、或者一个被@ResponseBody
标注的对象)。
至此,我们的 Controller 方法终于被成功执行,并且拿到了返回值。请求之旅,完成了最核心的一段。
四 、渲染视图——ViewResolver
与 View
Controller 方法执行完毕,handle
方法返回了一个 ModelAndView
对象(即便方法返回的是 String
,也会被包装成 ModelAndView
)。现在,DispatcherServlet
的 processDispatchResult
方法接手了工作。
任务是:如何将这个包含数据(Model)和逻辑视图名(View Name)的对象,变成最终的 HTML 响应?
这个过程被称为“视图渲染”。
render(mv, request, response)
:如果
ModelAndView
为空(比如方法被@ResponseBody
标注,返回值已经被HandlerMethodReturnValueHandler
直接写入响应体),则渲染流程结束。否则,开始解析视图。
resolveViewName(viewName, ...)
:解析视图名DispatcherServlet
会调用容器中注册的所有ViewResolver
(视图解析器)。最常见的
InternalResourceViewResolver
会根据我们在配置文件中定义的前缀(prefix
)和后缀(suffix
),将逻辑视图名(如"user/profile"
)解析成一个物理视图路径(如/WEB-INF/views/user/profile.jsp
)。解析成功后,会返回一个
View
对象(例如JstlView
)。View
接口是对所有视图技术(JSP, Thymeleaf, Freemarker 等)的统一抽象。view.render(model, request, response)
:执行渲染DispatcherServlet
调用View
对象的render
方法。- JstlView 的 render方法会做两件事:
将
ModelAndView
中的Model
数据(一个 Map)全部暴露到HttpServletRequest
的attribute
中。- 使用
RequestDispatcher
将请求 forward 到上一步解析出的 JSP 物理路径。
- 使用
最终,JSP 引擎接管请求,执行 JSP 页面,从
request
中获取数据,渲染出动态的 HTML,并将其写入HttpServletResponse
的输出流。
至此,一次完整的请求-响应之旅画上了句号。
回望整个旅程,Spring MVC 的设计思想跃然纸上:
- 前端控制器模式 (Front Controller):
DispatcherServlet
作为唯一的入口,承担了所有请求的接收和分发,以及通用逻辑的处理(如异常、国际化)。它让整个流程高度集中和可控。 - 策略模式 (Strategy Pattern) 的极致运用: 整个框架的核心组件,几乎都是可替换的策略接口。
- 用
HandlerMapping
策略来决定如何映射请求。 - 用
HandlerAdapter
策略来决定如何执行 Handler。 - 用
HandlerMethodArgumentResolver
策略来决定如何解析参数。 - 用
ViewResolver
策略来决定如何解析视图。 - ……
这种设计带来了极高的灵活性和可扩展性。不喜欢 Spring 的默认实现?没问题,写一个自己的实现类,注册到容器里,Spring MVC 就能无缝地使用你的策略。