第03篇 · 请求与响应:从HTTP报文到Controller方法的完整旅程

你平时写 @RestController 的时候,有没有想过这样一个问题:前端发来的 JSON,Spring 是怎么把它变成 User 对象的?我 return 的对象,又是怎么变成 JSON 的?

如果你用的是 Spring Boot,这整个过程对你来说是"黑盒"。你就像一位坐在驾驶舱的飞行员,只知道踩油门车会走,但不太清楚发动机内部的活塞是怎么运动的。

今天,我们就把这个"发动机"拆开,看看从 HTTP 请求到 Controller 方法的完整旅程。


学习目标

  • 理解 HTTP 请求从浏览器到 Controller 方法的完整数据流转
  • 掌握 HttpServletRequestHttpServletResponse 的核心 API 及使用场景
  • 理解 DispatcherServletdoDispatch 方法是如何调度请求
  • 掌握 Spring MVC 参数绑定的底层机制HandlerMethodArgumentResolver

正文

一、HTTP 请求的"旅行":从浏览器到 Controller

我们用"酒店入住"的流程来类比,会非常生动:

角色 类比 职责
浏览器 客人 发出请求(我要订房)
Web 服务器(Tomcat) 酒店大楼 接收客人,维护基础设施
Servlet 容器 酒店管理系统 解析请求,分发给对应部门
DispatcherServlet 前台总机 统一接收所有请求,分发给具体业务部门
HandlerMapping 前台登记册 根据 URL 找到对应的"业务员"(Controller 方法)
HandlerAdapter 前台助理 适配不同类型的业务员,帮他们准备好工具(参数)
Controller 方法 具体业务员 真正处理业务逻辑
ViewResolver / HttpMessageConverter 礼宾部 把处理结果包装成客人需要的形式(JSON/HTML)

完整路径是这样的

  1. 浏览器发起请求 → 2. Tomcat 接收 ,解析 HTTP 报文 → 3. Servlet 容器 将请求封装成 HttpServletRequest 对象 → 4. 容器找到注册的 DispatcherServlet,调用其 service() → 5. DispatcherServletdoDispatch() 开始工作 → 6. 通过 HandlerMapping 找到对应的 HandlerExecutionChain(包含 Controller 方法和拦截器)→ 7. 通过 HandlerAdapter 执行拦截器的 preHandle → 8. 调用 Controller 方法(参数自动绑定)→ 9. 执行拦截器的 postHandle → 10. 处理返回值(HttpMessageConverter 序列化)→ 11. 渲染视图(如果是 JSP)或直接写回响应 → 12. 执行拦截器的 afterCompletion → 13. 响应的 JSON/HTML 回到浏览器。

整个过程中,DispatcherServlet 是绝对的核心------它不干具体的"业务活",但它负责把活派给正确的人。这就是"前端控制器模式"。

二、DispatcherServlet 的 doDispatch:源码级解读

doDispatch 是 Spring MVC 最核心的方法,没有之一。我们来看一下它的简化版源码(Spring 6.x / Boot 3.x):

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) 
        throws Exception {
    
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;

    try {
        // ===== 步骤1:根据请求找到 Handler(Controller 方法)+ 拦截器链 =====
        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }

        // ===== 步骤2:找到能执行该 Handler 的适配器 =====
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

        // ===== 步骤3:执行所有拦截器的 preHandle =====
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;  // 如果某个拦截器返回 false,请求被拦截,直接返回
        }

        // ===== 步骤4:核心------调用 Controller 方法,执行业务逻辑 =====
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

        // ===== 步骤5:执行所有拦截器的 postHandle =====
        mappedHandler.applyPostHandle(processedRequest, response, mv);

        // ===== 步骤6:处理视图渲染(或直接写回响应) =====
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    } catch (Exception ex) {
        // ===== 异常处理 =====
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
}

逐行解读

  1. getHandler()DispatcherServlet 维护了一个 HandlerMapping 列表(包括 RequestMappingHandlerMappingBeanNameUrlHandlerMapping 等)。它会遍历这些映射器,找到第一个能匹配当前 URL 的 HandlerExecutionChain。这个链条里包含了目标 Controller 方法和所有匹配的拦截器。

  2. getHandlerAdapter() :不同类型的 Handler 需要不同的适配器来调用。最常见的是 RequestMappingHandlerAdapter,它专门处理标注了 @RequestMapping(以及衍生注解 @GetMapping@PostMapping 等)的方法。适配器模式的作用是让 DispatcherServlet 用统一的方式调用不同类型的 Handler ------无论是 @Controller 方法、HttpRequestHandler 还是 Servlet 本身。

  3. applyPreHandle() :按顺序执行拦截器的 preHandle 方法。如果任何一个返回 false,请求被拦截,后续的 Controller 和 postHandle 都不会执行。这也是为什么登录校验通常放在拦截器中------因为它在业务逻辑执行之前就介入。

  4. ha.handle() :这是真正执行 Controller 方法的地方。适配器会解析参数(用 HandlerMethodArgumentResolver)、执行方法、处理返回值(用 HandlerMethodReturnValueHandler)。

  5. applyPostHandle() :逆序执行拦截器的 postHandle 方法。注意这里只是"后置处理",即使 postHandle 抛出异常,afterCompletion 仍然会被执行。

  6. processDispatchResult() :处理视图渲染或直接通过 HttpMessageConverter 写回 JSON。

三、参数绑定是如何实现的?

这是很多人最关心的问题:Spring 怎么知道 @RequestParam("name") String name 里的 name 是哪里来的?

答案就是 HandlerMethodArgumentResolver(参数解析器)。

Spring 维护了一个参数解析器列表 ,每个解析器负责处理一种特定类型的参数或注解。当 HandlerAdapter 准备调用 Controller 方法时,它会遍历方法的所有参数,对每个参数,按顺序尝试所有解析器,找到第一个 supportsParameter() 返回 true 的解析器,然后调用 resolveArgument() 解析出参数值。

以下是常见注解对应的解析器:

注解 对应解析器 数据来源
@RequestParam RequestParamMethodArgumentResolver URL 查询参数 或 application/x-www-form-urlencoded 表单体
@PathVariable PathVariableMethodArgumentResolver URL 模板中的占位符(如 /user/{id}
@RequestBody RequestResponseBodyMethodProcessor HTTP 请求体(通常是 JSON/XML),通过 HttpMessageConverter 转换
@RequestHeader RequestHeaderMethodArgumentResolver HTTP 请求头
@ModelAttribute ModelAttributeMethodProcessor 将多个参数封装成对象(表单提交)
无注解的 POJO(Spring 3.1+) ServletModelAttributeMethodProcessor 同上,隐式 @ModelAttribute

特别注意 @RequestBody 的处理 :它不走普通的参数解析链,而是通过 HttpMessageConverter 将请求体(字节流)反序列化为 Java 对象。默认使用 MappingJackson2HttpMessageConverter(依赖 Jackson 库),所以如果你没有引入 jackson-databind@RequestBody 是不生效的。

四、响应数据的"归途"

Controller 执行完后,返回值会经过 HandlerMethodReturnValueHandler 处理。和参数解析类似,Spring 维护了一个返回值处理器列表。

常见场景:

返回场景 对应处理器 行为
返回 String(逻辑视图名) ViewNameMethodReturnValueHandler 解析为视图名,由 ViewResolver 渲染 JSP/Thymeleaf
返回 @ResponseBody 对象 RequestResponseBodyMethodProcessor 通过 HttpMessageConverter 序列化为 JSON/XML
返回 ResponseEntity<T> HttpEntityMethodProcessor 可自定义状态码、响应头,再序列化体
返回 ModelAndView ModelAndViewMethodReturnValueHandler 直接返回视图和数据

当你使用 @RestController 时,相当于每个方法都隐式加了 @ResponseBodyRequestResponseBodyMethodProcessor 会调用 HttpMessageConverter 将返回值转换为 JSON 流,直接写入 HttpServletResponseOutputStream

整个过程到此结束,响应报文回到浏览器。

代码示例

示例一:追踪一次请求的完整调用链

我们在拦截器、Controller、全局异常处理中都打印日志,直观感受执行顺序。

第一步:创建日志拦截器

java 复制代码
package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class TraceInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        log.info("🔵 [拦截器] preHandle ------ 请求开始,URL: {}", request.getRequestURI());
        return true;  // 返回 true 放行
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        log.info("🟢 [拦截器] postHandle ------ Controller 执行完毕,准备渲染视图");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) throws Exception {
        if (ex != null) {
            log.error("🔴 [拦截器] afterCompletion ------ 发生异常: {}", ex.getMessage());
        } else {
            log.info("✅ [拦截器] afterCompletion ------ 请求完成,响应已提交");
        }
    }
}

第二步:注册拦截器

java 复制代码
package com.example.demo.config;

import com.example.demo.interceptor.TraceInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final TraceInterceptor traceInterceptor;

    public WebConfig(TraceInterceptor traceInterceptor) {
        this.traceInterceptor = traceInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(traceInterceptor)
                .addPathPatterns("/api/**");  // 只拦截 /api 开头的请求
    }
}

第三步:编写 Controller 和全局异常处理

java 复制代码
package com.example.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/demo")
public class TraceController {

    @GetMapping("/hello")
    public String hello() {
        log.info("🟡 [Controller] 执行业务逻辑");
        return "Hello, World!";
    }

    @GetMapping("/error")
    public String error() {
        log.info("🟡 [Controller] 即将抛出异常");
        throw new RuntimeException("模拟业务异常");
    }
}
java 复制代码
package com.example.demo.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(RuntimeException e) {
        log.error("🟠 [全局异常处理] 捕获异常: {}", e.getMessage());
        return "Error: " + e.getMessage();
    }
}

第四步:观察控制台输出

正常请求 /api/demo/hello

复制代码
🔵 [拦截器] preHandle ------ 请求开始,URL: /api/demo/hello
🟡 [Controller] 执行业务逻辑
🟢 [拦截器] postHandle ------ Controller 执行完毕,准备渲染视图
✅ [拦截器] afterCompletion ------ 请求完成,响应已提交

异常请求 /api/demo/error

复制代码
🔵 [拦截器] preHandle ------ 请求开始,URL: /api/demo/error
🟡 [Controller] 即将抛出异常
🟠 [全局异常处理] 捕获异常: 模拟业务异常
🟢 [拦截器] postHandle ------ Controller 执行完毕,准备渲染视图  ← 注意:即使异常,postHandle 仍会执行!
✅ [拦截器] afterCompletion ------ 请求完成,响应已提交  ← 不管有无异常,afterCompletion 必执行

注意:postHandle 在 Controller 抛出异常时仍会执行,但若异常未捕获抛到容器层,postHandle 可能不执行(取决于具体的异常处理机制)。上述例子中全局异常处理捕获后,postHandle 仍会被调用。

示例二:四种参数接收方式的对比

java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/params")
public class ParamController {

    /**
     * 1. @RequestParam:接收 URL 查询参数或表单参数
     * GET /api/params/query?name=张三&age=25
     */
    @GetMapping("/query")
    public String queryParam(
            @RequestParam String name,
            @RequestParam(defaultValue = "0") int age) {
        return "姓名: " + name + ", 年龄: " + age;
    }

    /**
     * 2. @PathVariable:接收 URL 路径中的占位符
     * GET /api/params/path/1001
     */
    @GetMapping("/path/{userId}")
    public String pathVariable(@PathVariable Long userId) {
        return "用户ID: " + userId;
    }

    /**
     * 3. @RequestBody:接收 JSON 请求体(POST 请求)
     * POST /api/params/json
     * Content-Type: application/json
     * {"username": "jack", "password": "123456"}
     */
    @PostMapping("/json")
    public String requestBody(@RequestBody LoginRequest request) {
        return "登录用户: " + request.getUsername();
    }

    /**
     * 4. @RequestPart:接收文件上传
     * POST /api/params/upload
     * Content-Type: multipart/form-data
     * file: (选中的文件)
     */
    @PostMapping("/upload")
    public String uploadFile(@RequestPart MultipartFile file) {
        return "上传文件: " + file.getOriginalFilename() + 
               ", 大小: " + file.getSize() + " 字节";
    }

    // 内部类,用于 @RequestBody 示例
    public static class LoginRequest {
        private String username;
        private String password;
        // getter/setter 必须要有,否则 JSON 反序列化失败
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }
}

测试建议 :用 Postman 分别发送对应格式的请求,观察返回结果。特别注意 @RequestBody 的请求头必须包含 Content-Type: application/json,否则会返回 415 错误。

新手错误 vs 正确姿势

错误表象 根本原因 正确姿势
int 接收 URL 参数,不传值导致 500 异常 基本类型不能为 null,而参数缺失时值为 null 使用 IntegerLong包装类型 ,或设置 defaultValue
POST 请求的 JSON 参数用 @RequestParam 接收不到 混淆了表单参数application/x-www-form-urlencoded)和请求体application/json 表单/查询参数用 @RequestParam,JSON 体用 @RequestBody
@RequestBody 反序列化失败,报 HttpMessageNotReadableException 请求体的 JSON 字段名与 Java 对象的字段名不一致 使用 @JsonProperty 注解指定映射关系,或开启驼峰映射
拦截器的 postHandle 中修改响应数据无效 postHandle 执行时视图尚未渲染,但响应流可能已被 Controller 写入 使用 ResponseBodyAdvice 统一处理响应体,更合适

疑难深度追问

Q1:为什么 Spring MVC 能自动将 JSON 字符串转换为 Java 对象?

依赖 HttpMessageConverter@RequestBody 的处理由 RequestResponseBodyMethodProcessor 完成,它内部维护了一个 HttpMessageConverter 列表。当需要反序列化时,按顺序遍历转换器,找到第一个 canRead() 返回 true 的转换器来执行------对于 JSON,默认是 MappingJackson2HttpMessageConverter(底层使用 Jackson 的 ObjectMapper)。

Q2:如果多个 HandlerMethodArgumentResolver 都能处理同一种参数,如何决定用哪个?

按照注册顺序 ,先注册的先匹配。RequestMappingHandlerAdapter 在初始化时会组装一个固定的解析器列表,顺序是:@PathVariable 相关解析器 → @RequestParam 相关 → @RequestBody → 其他。如果多个解析器都声明支持同一类型(比如 ModelAttributeMethodProcessorServletModelAttributeMethodProcessor),则第一个被遍历到的生效 。Spring Boot 3.x 中,ServletModelAttributeMethodProcessor 排在 ModelAttributeMethodProcessor 之后,但具体顺序取决于版本和配置。

Q3:如果前端传的参数名是 user_name,后端接收的是 userName,如何在不改前端代码的情况下完成绑定?

有四种方案,按推荐度排序:

  1. 使用 @RequestParam("user_name") String userName 明确指定参数名。
  2. 在 Jackson 反序列化时,使用 @JsonProperty("user_name") 注解。
  3. application.yml 中开启 Spring 的宽松绑定(spring.jackson.property-naming-strategy=SNAKE_CASE),让 Jackson 自动转换蛇形到驼峰。但注意这会影响全局。
  4. 对于 @RequestBody 的 POJO,可以结合 @JsonProperty 或全局命名策略。

思考与延伸

  1. 动手验证 :在 TraceInterceptorpreHandle 中返回 false,观察后续拦截器、Controller、postHandle 是否还被调用。

  2. 思考题 :为什么 doDispatch 方法中,applyPreHandleha.handle 之前执行,而 applyPostHandle 在之后?这背后体现了什么样的设计原则?

  3. 延伸阅读 :查阅 Spring 官方文档中关于 HandlerMethodArgumentResolver 的说明,思考如何自定义一个参数解析器------比如让 Controller 方法能直接接收当前登录的用户对象。

参考与延伸阅读

  • Spring Framework. Web MVC Framework --- DispatcherServlet. Spring Framework Documentation, 6.0.x
  • Spring Framework. HandlerMethodArgumentResolver. Spring Framework Javadoc
  • Baeldung. Spring MVC HandlerMethodArgumentResolver Example. Baeldung, 2023
  • InfoQ. Spring MVC 请求处理流程深度解析. InfoQ 中文站