SpringMVC:请求链路、参数绑定、异常处理与生产实践
前言
一个 HTTP 请求在 Spring MVC 中会经过容器、过滤器、安全链、路由、参数处理、业务代理和响应序列化等多个环节。本文按真实请求链路梳理这些组件的职责,帮助在接口开发、异常治理和线上排查时明确问题所在层级。
Spring MVC 解决什么问题
Spring MVC 是基于 Servlet API 的 Web 框架,负责把 HTTP 请求路由到 Java 方法、完成参数绑定和校验、调用业务服务,并将结果序列化为 HTTP 响应。它的核心入口是 DispatcherServlet,采用前端控制器模式:请求先进入统一入口,再由不同策略组件完成映射、执行和响应处理。
不要把 Spring MVC 简化为“写 @RestController”。生产中一次接口调用还涉及 Filter、安全链、路由、拦截器、参数转换、事务、异常映射、消息序列化、超时和日志链路。面试回答应能把这些组件按执行顺序串起来。
flowchart LR
A[Client] --> B[Servlet Container]
B --> C[Filter Chain]
C --> D[DispatcherServlet]
D --> E[HandlerMapping]
E --> F[HandlerAdapter]
F --> G[HandlerInterceptor preHandle]
G --> H[Argument Resolver and Validation]
H --> I[Controller]
I --> J[Service / AOP / Transaction]
J --> K[ReturnValue Handler]
K --> L[HttpMessageConverter]
L --> M[Interceptor postHandle / afterCompletion]
M --> N[HTTP Response]
DispatcherServlet 请求链路
一次 @RestController 请求如何执行
以 GET /api/orders/1001?includeItems=true 为例,典型过程如下:
- Tomcat、Jetty 等 Servlet 容器接收请求,依次执行匹配的 Filter,例如字符编码、安全过滤器、TraceId Filter。
- 请求进入
DispatcherServlet,它向HandlerMapping查询哪个 Handler 能处理当前 URL、HTTP Method、Header、Content-Type 和 Accept 条件。 RequestMappingHandlerMapping找到目标 Controller 方法及其HandlerInterceptor列表。HandlerAdapter调用目标方法前,依次执行拦截器preHandle;任何一个返回false,后续 Controller 不执行。HandlerMethodArgumentResolver将路径变量、查询参数、请求头、JSON Body、登录用户等解析为方法参数,并触发@Valid校验。- Controller 调用 Service;事务、审计、权限等 AOP 通常在 Service 代理层执行。
HandlerMethodReturnValueHandler处理返回值。@ResponseBody或@RestController场景选择HttpMessageConverter,例如 Jackson 将对象写成 JSON。- 成功后执行
postHandle;请求结束后无论成功失败都会执行afterCompletion,适合清理 MDC、ThreadLocal 等资源。
| 组件 | 核心职责 | 常见面试误区 |
|---|---|---|
DispatcherServlet |
前端控制器,协调后续策略组件 | 它不直接靠 if/else 调用每个 Controller |
HandlerMapping |
根据请求找到 Handler | @RequestMapping 的映射关系通常由 RequestMappingHandlerMapping 管理 |
HandlerAdapter |
用统一方式调用不同类型 Handler | 找到 Handler 后仍需要 Adapter 执行 |
HandlerInterceptor |
围绕 Spring MVC Handler 的前后处理 | 不等价于 Servlet Filter,不能覆盖所有容器层请求 |
HttpMessageConverter |
HTTP Body 与 Java 对象互转 | 不负责把每个请求参数绑定到方法参数 |
HandlerExceptionResolver |
将异常解析为 HTTP 响应 | 不能替代业务事务回滚和日志记录 |
@Controller、@RestController 和 @ResponseBody
@Controller 表示 MVC Controller。方法返回字符串时,默认可能被当作视图名,由 ViewResolver 渲染页面;在方法或类上加 @ResponseBody 后,返回值写入 HTTP Body。
@RestController 等价于 @Controller + @ResponseBody,适合 JSON API。若接口需要同时返回 HTML 页面和 JSON API,可分别使用 @Controller 和 @RestController,不要依赖返回值字符串“碰巧被解析成 JSON”。
路由匹配与 API 设计
@RequestMapping 可以按路径、HTTP Method、请求参数、Header、consumes 和 produces 共同限定映射。@GetMapping、@PostMapping 等是更明确的语法糖。
1 |
|
| 场景 | 推荐方式 | 注意点 |
|---|---|---|
| 资源标识 | @PathVariable |
/orders/{orderId} 表示一个明确资源 |
| 可选筛选、分页、排序 | @RequestParam 或查询对象 |
设置默认值和最大 pageSize,不能让调用方无限分页 |
| 创建/更新复杂对象 | @RequestBody DTO |
不要直接接收数据库 Entity,避免字段越权绑定 |
| 文件上传 | MultipartFile / @RequestPart |
限制文件大小、类型、数量并做流式处理 |
| 版本演进 | 路径或 Header 版本 | 团队统一一种版本策略,避免无规则混用 |
路径匹配应避免歧义。例如 /users/{id} 与 /users/search 同时存在时,框架会按映射规则选择更具体路径,但 API 设计上仍应让动作型路径与资源型路径清晰分开。生产接口还要限制 URL、Header、查询参数和 Body 大小,防止异常大请求占用容器内存或解析线程。
参数绑定、类型转换与校验
参数如何变成 Java 对象
Spring MVC 不是反射地把所有内容“自动塞进对象”。它为不同参数类型选择不同 HandlerMethodArgumentResolver:@PathVariable 从路径模板取值,@RequestParam 从 query/form 参数取值,@RequestHeader 从 Header 取值,@RequestBody 交给消息转换器读取 Body。
| 注解/类型 | 数据来源 | 常见问题 |
|---|---|---|
@PathVariable |
URL 路径 | 路径变量名和模板不一致,或未显式声明名称 |
@RequestParam |
查询参数、表单参数 | 基本类型未传会绑定失败;可用包装类型或 defaultValue |
@RequestHeader |
HTTP Header | 不应把不可信 Header 直接作为用户身份 |
@CookieValue |
Cookie | Cookie 是客户端可修改数据,敏感状态仍需服务端校验 |
@RequestBody |
JSON/XML 等请求体 | Body 通常只能读取一次;字段类型错误会在转换阶段失败 |
@ModelAttribute |
多个 query/form 字段绑定对象 | 注意白名单字段,避免 Mass Assignment |
Principal / 自定义用户参数 |
安全上下文或自定义解析器 | 认证身份应由安全框架验证后提供 |
类型转换由 Converter、Formatter、ConversionService 等完成。若有统一的枚举、日期、租户或当前用户参数,可注册 Converter 或自定义参数解析器,而不是在每个 Controller 中重复 parse。但不要把数据库查询、远程调用等重操作放进参数解析器,否则每个请求在进入业务前都可能被隐藏地阻塞。
Bean Validation 的正确使用
@Valid 或 @Validated 触发 Jakarta Bean Validation。Controller 接收 DTO 时应优先校验边界,业务层仍要校验依赖数据库状态的规则,例如“优惠券是否属于当前用户”“库存是否充足”。格式校验不能替代业务校验。
1 | public record CreateOrderRequest( |
@RequestBody 校验失败通常抛出 MethodArgumentNotValidException;@RequestParam、@PathVariable 的 @Validated 校验常见为 ConstraintViolationException 或框架版本对应的处理异常。统一异常处理应把它们映射为稳定的 400 错误格式,并包含字段名、规则和可展示的提示,不能把内部堆栈返回给客户端。
消息转换与内容协商
HttpMessageConverter 负责读写 HTTP Body。请求侧依据 Content-Type 选择转换器,例如 application/json 常由 Jackson 转成 DTO;响应侧会综合返回类型、Accept、produces 选择转换器。
| 场景 | 关键点 | 生产建议 |
|---|---|---|
| JSON 序列化 | Jackson ObjectMapper、字段命名、日期格式 |
统一日期/时区和 null 策略;DTO 避免暴露敏感字段 |
| 枚举输出 | 名称、编码或对象 | API 契约固定后不要随 Java 枚举重命名而改变 |
| 大响应 | 序列化和网络写出都占用 Servlet 线程 | 分页、字段裁剪、异步导出或流式下载 |
| 内容协商 | Accept、produces |
API 通常固定 JSON,避免为不需要的 XML/扩展名协商增加复杂度 |
| Body 读取 | InputStream 通常一次性消费 | 需要记录请求 Body 时使用包装器并控制大小,不能无界缓存 |
@ResponseBody 不等于一定返回 JSON。若返回 String,可能由 String Converter 直接写为文本;如果接口声明 JSON 却返回手工拼接字符串,容易造成双重编码或 Content-Type 不一致。让 Controller 返回 DTO,由统一 Jackson 配置序列化。
Filter、Interceptor、AOP 的边界
这三者都能做“前后处理”,但所在层次不同,不能互相替代。
| 机制 | 所在层次 | 适合做什么 | 不适合做什么 |
|---|---|---|---|
| Filter | Servlet 容器 | 编码、CORS、请求大小限制、原始访问日志、安全框架前置处理 | 依赖 Controller 方法注解的业务授权 |
| Interceptor | Spring MVC Handler 调用周围 | 登录态校验、接口审计、轻量限流、请求上下文 | 包装所有响应字节、处理静态资源以外的容器级场景 |
| AOP | Spring Bean 方法调用 | 事务、服务层审计、权限校验、指标 | 判断原始 HTTP Body 或设置响应 Header |
Filter 的 doFilter 可以包围整个请求链;Interceptor 的 preHandle 在 Handler 调用前,postHandle 通常只在正常返回后执行,afterCompletion 在请求完成后执行。若在 Interceptor 中设置 MDC、租户或 ThreadLocal,必须在 afterCompletion 的 finally 语义中清理,线程复用时遗漏清理会导致串请求。
认证与授权不要只靠一个“登录拦截器”。生产系统通常由 Spring Security 或网关验证 Token、签名和权限,Controller/Service 只处理已经验证的主体。业务数据权限仍需在查询条件和服务层校验,例如“当前用户是否可以读取该订单”,不能只根据前端传来的 userId 判断。
统一异常、响应与可观测性
@RestControllerAdvice 可集中处理异常,底层依赖 HandlerExceptionResolver。统一响应的目标是稳定 API 契约,而不是把所有异常包装成 HTTP 200。
1 |
|
建议至少区分:参数校验错误(400)、未认证(401)、无权限(403)、资源不存在(404)、业务冲突如幂等键重复或状态不允许(409)、限流(429)、依赖失败/服务异常(5xx)。系统异常记录完整堆栈、traceId、请求路径、方法、业务主键和脱敏后的参数;响应只返回稳定错误码和可理解提示。
异常处理不能吞掉异常后返回成功,也不能在 @ControllerAdvice 中直接打印 e.getMessage() 就结束。对于超时、连接池满、线程池拒绝等异常,除了返回合适状态码,还要关联下游指标、入口流量和重试量,避免重试风暴。
文件上传、下载与大响应
文件处理是 Spring MVC 常见的生产追问。MultipartFile 方便,但不代表可以把任意大文件一次读入内存。
上传流程:限制 max-file-size、max-request-size 和文件数量;校验扩展名、MIME、魔数和业务归属;使用流式上传到对象存储或临时文件;异步做病毒扫描、转码和解析;数据库只保存对象 Key、大小、校验和、状态和权限元数据。不要信任客户端传来的文件名和 Content-Type。
下载流程:小文件可由 Resource/ResponseEntity 返回;大文件使用流式输出、对象存储预签名 URL 或 Nginx/X-Accel-Redirect,避免应用 JVM 读取完整文件。支持断点续传时正确处理 Range、Content-Range、Content-Length,并在下载前校验租户和用户权限。
异步请求、超时与线程模型
Spring MVC 基于 Servlet 线程模型。普通 Controller 从容器工作线程开始处理,执行同步阻塞的数据库或 HTTP 调用时会占用该线程。Callable、DeferredResult、WebAsyncTask 或 CompletableFuture 可以将部分工作交给业务线程池,并在完成后恢复响应处理,但不会让阻塞 IO 自动变成非阻塞。
1 |
|
异步必须显式使用具名、有界、受监控的 queryExecutor,不要默认使用 commonPool,也不要把异步当作无限并发。容器线程池、业务线程池、HTTP 客户端连接池、数据库连接池和下游限流共同决定系统最大并发。外层请求超时后,底层任务未必自动停止,因此 HTTP、数据库和任务都要设置超时;有副作用的操作必须幂等。
WebFlux 是另一套以响应式、非阻塞运行时为重点的框架,不等同于“Spring MVC 加 CompletableFuture”。当团队主要使用 Servlet/Tomcat、JPA/JDBC 和阻塞 SDK 时,Spring MVC 仍是常见且成熟的选择;不要只为“非阻塞”把阻塞调用搬进 WebFlux 事件循环。
常见生产问题排查
| 现象 | 先查什么 | 常见根因和处理 |
|---|---|---|
| 接口 404 | HandlerMapping 日志、路径、Method、context-path |
映射没扫描到、HTTP Method 不匹配、网关重写路径错误 |
| 接口 405 | Controller 映射支持的方法 | 客户端使用了错误 HTTP Method,或 CORS 预检未正确放行 |
| 接口 415 | 请求 Content-Type、consumes、Body 格式 |
JSON 未设置 application/json,或缺少对应 Converter |
| 接口 406 | Accept、produces、返回类型 |
服务无法按客户端要求的媒体类型响应 |
| 参数总是 null/400 | 参数来源、注解、字段名、Converter、校验异常 | @RequestBody/@RequestParam 用错,日期格式不一致,字段类型不匹配 |
| P99 升高且容器线程忙 | 线程 dump、连接池等待、下游耗时 | 慢 SQL、慢 HTTP、锁等待、线程池/连接池耗尽;先止血限流和超时 |
| 事务/审计不生效 | 调用是否经过 Spring 代理 | 自调用、对象自行 new、异步切线程、异常被吞掉 |
| 内存上涨 | 请求体大小、文件上传、响应大小、缓存包装器 | 无界 Body 缓存、大文件读入内存、超大分页、响应序列化堆积 |
排查顺序应从“请求有没有到应用”开始:网关访问日志和状态码 -> Filter/安全链 -> HandlerMapping -> 参数/Converter -> Controller -> Service/事务 -> 数据库和外部依赖 -> 响应序列化与网络写出。每一层都用 traceId 串联,避免只凭一个接口超时现象直接调大 Tomcat 线程数。
高频面试题
| 问题 | 回答要点 |
|---|---|
| Spring MVC 请求执行流程? | Filter -> DispatcherServlet -> HandlerMapping -> HandlerAdapter -> Interceptor -> 参数解析/校验 -> Controller -> 返回值处理 -> MessageConverter -> 响应。 |
@RequestBody 与 @RequestParam 区别? |
前者通过 MessageConverter 读取 Body,后者读取 URL query/form 参数;复杂 JSON 通常使用前者。 |
| Filter 和 Interceptor 区别? | Filter 属于 Servlet 容器,可包围整个请求;Interceptor 属于 Spring MVC,更容易获得 Handler 信息。 |
@ControllerAdvice 做什么? |
统一处理 Controller 层异常、绑定器和响应体增强;应映射稳定 HTTP 状态和错误码,不能吞异常伪造成功。 |
HttpMessageConverter 做什么? |
负责 HTTP Body 与 Java 对象转换,依据 Content-Type、Accept、返回类型选择。 |
为什么大文件下载不能直接读 byte[]? |
文件会完整进入 JVM 堆,容易 OOM;应流式传输或让对象存储/Nginx 承担文件下发。 |
| Spring MVC 异步就一定更高性能吗? | 不一定。它只释放或转移部分容器线程;阻塞下游、业务线程池、连接池和超时仍是容量边界。 |
总结
Spring MVC 的主线是:DispatcherServlet 协调映射、参数解析、Handler 执行、返回值处理和消息转换。生产上要明确 Filter、Interceptor、AOP 的边界,在 Controller 层完成输入校验,在 Service 层完成业务与数据权限校验,通过统一异常维持 API 契约,并将超时、线程池、连接池、文件大小和 TraceId 纳入整体治理。


