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 就能无缝地使用你的策略。