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 方法被调用时,RequestMappingHandlerMappinglookupHandlerMethod 方法会被触发。它的工作流程如下:

  1. 构建查询 Key: 从当前的 HttpServletRequest 中提取出 URL 路径(如 /users/1)。
  2. 直接匹配: 首先尝试用完整的 URL 路径去“电话黄页”(Map)中进行快速查找。如果能直接找到,效率最高。
  3. 模式匹配: 如果直接查找失败(比如对于 /users/1 这样的路径变量),它会遍历所有注册的 RequestMappingInfo,利用 AntPathMatcher 进行路径匹配(判断 /users/1 是否符合 /users/{id} 的模式),并检查 HTTP Method 等其他条件是否也满足。
  4. 寻找最佳匹配: 可能会有多个模式都能匹配上(例如 /users/*/users/{id} 都匹配 /users/1),此时 Spring 会通过一套复杂的排序算法,找到“最佳匹配”(Best Match)的那个。通常,路径更具体的那个会胜出。
  5. 封装返回: 一旦找到唯一的、最佳的 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 方法内部的逻辑可以概括为两步:

  1. getMethodArgumentValues():解析方法参数
  2. 这一步是 Spring MVC “魔法”的核心。它会遍历 HandlerMethod 的所有参数。
  3. 对于每一个参数,它会从一个 HandlerMethodArgumentResolver(方法参数解析器)的列表中,找到支持解析该参数的解析器。
  4. 例如:
    • 看到 @RequestParam 注解的参数,RequestParamMethodArgumentResolver 就会上场,从 request.getParameter() 中取值。
    • 看到 @PathVariable 注解的参数,PathVariableMethodArgumentResolver 就会上场,从之前 HandlerMapping 匹配时解析出的 URI 模板变量中取值。
    • 看到 @RequestBody 注解的参数,RequestResponseBodyMethodProcessor 就会上场,它会使用注册的 HttpMessageConverter(比如 MappingJackson2HttpMessageConverter)读取请求体中的 JSON 串,并反序列化成对应的 Java 对象。
  5. 所有参数都被解析成 Java 对象后,会存入一个 Object[] 数组。
  6. doInvoke(args):真正执行方法
  7. 万事俱备,只欠东风。doInvoke 会使用 Java 反射,用上一步准备好的参数数组,调用我们 Controller 中的那个 HandlerMethod
  8. handlerMethod.invoke(bean, args)
  9. 执行完成后,获取到方法的返回值(可能是 StringModelAndView、或者一个被 @ResponseBody 标注的对象)。

至此,我们的 Controller 方法终于被成功执行,并且拿到了返回值。请求之旅,完成了最核心的一段。

四 、渲染视图——ViewResolverView

Controller 方法执行完毕,handle 方法返回了一个 ModelAndView 对象(即便方法返回的是 String,也会被包装成 ModelAndView)。现在,DispatcherServletprocessDispatchResult 方法接手了工作。

任务是:如何将这个包含数据(Model)和逻辑视图名(View Name)的对象,变成最终的 HTML 响应?

这个过程被称为“视图渲染”。

  1. render(mv, request, response)

  2. 如果 ModelAndView 为空(比如方法被 @ResponseBody 标注,返回值已经被 HandlerMethodReturnValueHandler 直接写入响应体),则渲染流程结束。

  3. 否则,开始解析视图。

  4. resolveViewName(viewName, ...):解析视图名

  5. DispatcherServlet 会调用容器中注册的所有 ViewResolver(视图解析器)。

  6. 最常见的 InternalResourceViewResolver 会根据我们在配置文件中定义的前缀(prefix)和后缀(suffix),将逻辑视图名(如 "user/profile")解析成一个物理视图路径(如 /WEB-INF/views/user/profile.jsp)。

  7. 解析成功后,会返回一个 View 对象(例如 JstlView)。View 接口是对所有视图技术(JSP, Thymeleaf, Freemarker 等)的统一抽象。

  8. view.render(model, request, response):执行渲染

  9. DispatcherServlet 调用 View 对象的 render 方法。

    • JstlView 的 render方法会做两件事:
  10. ModelAndView 中的 Model 数据(一个 Map)全部暴露到 HttpServletRequestattribute 中。

    • 使用 RequestDispatcher 将请求 forward 到上一步解析出的 JSP 物理路径。
  11. 最终,JSP 引擎接管请求,执行 JSP 页面,从 request 中获取数据,渲染出动态的 HTML,并将其写入 HttpServletResponse 的输出流。

至此,一次完整的请求-响应之旅画上了句号。

回望整个旅程,Spring MVC 的设计思想跃然纸上:

  1. 前端控制器模式 (Front Controller): DispatcherServlet 作为唯一的入口,承担了所有请求的接收和分发,以及通用逻辑的处理(如异常、国际化)。它让整个流程高度集中和可控。
  2. 策略模式 (Strategy Pattern) 的极致运用: 整个框架的核心组件,几乎都是可替换的策略接口。
  3. HandlerMapping 策略来决定如何映射请求。
  4. HandlerAdapter 策略来决定如何执行 Handler。
  5. HandlerMethodArgumentResolver 策略来决定如何解析参数。
  6. ViewResolver 策略来决定如何解析视图。
  7. ……

这种设计带来了极高的灵活性和可扩展性。不喜欢 Spring 的默认实现?没问题,写一个自己的实现类,注册到容器里,Spring MVC 就能无缝地使用你的策略。