你平时写 @RestController 的时候,有没有想过这样一个问题:前端发来的 JSON,Spring 是怎么把它变成 User 对象的?我 return 的对象,又是怎么变成 JSON 的?
如果你用的是 Spring Boot,这整个过程对你来说是"黑盒"。你就像一位坐在驾驶舱的飞行员,只知道踩油门车会走,但不太清楚发动机内部的活塞是怎么运动的。
今天,我们就把这个"发动机"拆开,看看从 HTTP 请求到 Controller 方法的完整旅程。
学习目标
- 理解 HTTP 请求从浏览器到 Controller 方法的完整数据流转
- 掌握
HttpServletRequest和HttpServletResponse的核心 API 及使用场景 - 理解
DispatcherServlet的doDispatch方法是如何调度请求的 - 掌握 Spring MVC 参数绑定的底层机制 (
HandlerMethodArgumentResolver)
正文
一、HTTP 请求的"旅行":从浏览器到 Controller
我们用"酒店入住"的流程来类比,会非常生动:
| 角色 | 类比 | 职责 |
|---|---|---|
| 浏览器 | 客人 | 发出请求(我要订房) |
| Web 服务器(Tomcat) | 酒店大楼 | 接收客人,维护基础设施 |
| Servlet 容器 | 酒店管理系统 | 解析请求,分发给对应部门 |
| DispatcherServlet | 前台总机 | 统一接收所有请求,分发给具体业务部门 |
| HandlerMapping | 前台登记册 | 根据 URL 找到对应的"业务员"(Controller 方法) |
| HandlerAdapter | 前台助理 | 适配不同类型的业务员,帮他们准备好工具(参数) |
| Controller 方法 | 具体业务员 | 真正处理业务逻辑 |
| ViewResolver / HttpMessageConverter | 礼宾部 | 把处理结果包装成客人需要的形式(JSON/HTML) |
完整路径是这样的:
- 浏览器发起请求 → 2. Tomcat 接收 ,解析 HTTP 报文 → 3. Servlet 容器 将请求封装成
HttpServletRequest对象 → 4. 容器找到注册的DispatcherServlet,调用其service()→ 5.DispatcherServlet的doDispatch()开始工作 → 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);
}
}
逐行解读:
-
getHandler():DispatcherServlet维护了一个HandlerMapping列表(包括RequestMappingHandlerMapping、BeanNameUrlHandlerMapping等)。它会遍历这些映射器,找到第一个能匹配当前 URL 的HandlerExecutionChain。这个链条里包含了目标 Controller 方法和所有匹配的拦截器。 -
getHandlerAdapter():不同类型的 Handler 需要不同的适配器来调用。最常见的是RequestMappingHandlerAdapter,它专门处理标注了@RequestMapping(以及衍生注解@GetMapping、@PostMapping等)的方法。适配器模式的作用是让 DispatcherServlet 用统一的方式调用不同类型的 Handler ------无论是@Controller方法、HttpRequestHandler还是Servlet本身。 -
applyPreHandle():按顺序执行拦截器的preHandle方法。如果任何一个返回false,请求被拦截,后续的 Controller 和postHandle都不会执行。这也是为什么登录校验通常放在拦截器中------因为它在业务逻辑执行之前就介入。 -
ha.handle():这是真正执行 Controller 方法的地方。适配器会解析参数(用HandlerMethodArgumentResolver)、执行方法、处理返回值(用HandlerMethodReturnValueHandler)。 -
applyPostHandle():逆序执行拦截器的postHandle方法。注意这里只是"后置处理",即使postHandle抛出异常,afterCompletion仍然会被执行。 -
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 时,相当于每个方法都隐式加了 @ResponseBody。RequestResponseBodyMethodProcessor 会调用 HttpMessageConverter 将返回值转换为 JSON 流,直接写入 HttpServletResponse 的 OutputStream。
整个过程到此结束,响应报文回到浏览器。
代码示例
示例一:追踪一次请求的完整调用链
我们在拦截器、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 |
使用 Integer、Long 等包装类型 ,或设置 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 → 其他。如果多个解析器都声明支持同一类型(比如 ModelAttributeMethodProcessor 和 ServletModelAttributeMethodProcessor),则第一个被遍历到的生效 。Spring Boot 3.x 中,ServletModelAttributeMethodProcessor 排在 ModelAttributeMethodProcessor 之后,但具体顺序取决于版本和配置。
Q3:如果前端传的参数名是 user_name,后端接收的是 userName,如何在不改前端代码的情况下完成绑定?
有四种方案,按推荐度排序:
- 使用
@RequestParam("user_name") String userName明确指定参数名。 - 在 Jackson 反序列化时,使用
@JsonProperty("user_name")注解。 - 在
application.yml中开启 Spring 的宽松绑定(spring.jackson.property-naming-strategy=SNAKE_CASE),让 Jackson 自动转换蛇形到驼峰。但注意这会影响全局。 - 对于
@RequestBody的 POJO,可以结合@JsonProperty或全局命名策略。
思考与延伸
-
动手验证 :在
TraceInterceptor的preHandle中返回false,观察后续拦截器、Controller、postHandle是否还被调用。 -
思考题 :为什么
doDispatch方法中,applyPreHandle在ha.handle之前执行,而applyPostHandle在之后?这背后体现了什么样的设计原则? -
延伸阅读 :查阅 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 中文站