前言

一个 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 为例,典型过程如下:

  1. Tomcat、Jetty 等 Servlet 容器接收请求,依次执行匹配的 Filter,例如字符编码、安全过滤器、TraceId Filter。
  2. 请求进入 DispatcherServlet,它向 HandlerMapping 查询哪个 Handler 能处理当前 URL、HTTP Method、Header、Content-Type 和 Accept 条件。
  3. RequestMappingHandlerMapping 找到目标 Controller 方法及其 HandlerInterceptor 列表。
  4. HandlerAdapter 调用目标方法前,依次执行拦截器 preHandle;任何一个返回 false,后续 Controller 不执行。
  5. HandlerMethodArgumentResolver 将路径变量、查询参数、请求头、JSON Body、登录用户等解析为方法参数,并触发 @Valid 校验。
  6. Controller 调用 Service;事务、审计、权限等 AOP 通常在 Service 代理层执行。
  7. HandlerMethodReturnValueHandler 处理返回值。@ResponseBody@RestController 场景选择 HttpMessageConverter,例如 Jackson 将对象写成 JSON。
  8. 成功后执行 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、consumesproduces 共同限定映射。@GetMapping@PostMapping 等是更明确的语法糖。

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping(value = "/{orderId}", produces = MediaType.APPLICATION_JSON_VALUE)
public OrderView detail(@PathVariable Long orderId,
@RequestParam(defaultValue = "false") boolean includeItems) {
return orderApplicationService.detail(orderId, includeItems);
}
}
场景 推荐方式 注意点
资源标识 @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 / 自定义用户参数 安全上下文或自定义解析器 认证身份应由安全框架验证后提供

类型转换由 ConverterFormatterConversionService 等完成。若有统一的枚举、日期、租户或当前用户参数,可注册 Converter 或自定义参数解析器,而不是在每个 Controller 中重复 parse。但不要把数据库查询、远程调用等重操作放进参数解析器,否则每个请求在进入业务前都可能被隐藏地阻塞。

Bean Validation 的正确使用

@Valid@Validated 触发 Jakarta Bean Validation。Controller 接收 DTO 时应优先校验边界,业务层仍要校验依赖数据库状态的规则,例如“优惠券是否属于当前用户”“库存是否充足”。格式校验不能替代业务校验。

1
2
3
4
5
6
7
8
9
10
public record CreateOrderRequest(
@NotBlank String requestId,
@NotEmpty List<@Valid OrderItemRequest> items,
@NotNull @FutureOrPresent LocalDateTime expectedDeliveryTime) {
}

@PostMapping
public OrderCreatedResponse create(@Valid @RequestBody CreateOrderRequest request) {
return orderApplicationService.create(request);
}

@RequestBody 校验失败通常抛出 MethodArgumentNotValidException@RequestParam@PathVariable@Validated 校验常见为 ConstraintViolationException 或框架版本对应的处理异常。统一异常处理应把它们映射为稳定的 400 错误格式,并包含字段名、规则和可展示的提示,不能把内部堆栈返回给客户端。

消息转换与内容协商

HttpMessageConverter 负责读写 HTTP Body。请求侧依据 Content-Type 选择转换器,例如 application/json 常由 Jackson 转成 DTO;响应侧会综合返回类型、Acceptproduces 选择转换器。

场景 关键点 生产建议
JSON 序列化 Jackson ObjectMapper、字段命名、日期格式 统一日期/时区和 null 策略;DTO 避免暴露敏感字段
枚举输出 名称、编码或对象 API 契约固定后不要随 Java 枚举重命名而改变
大响应 序列化和网络写出都占用 Servlet 线程 分页、字段裁剪、异步导出或流式下载
内容协商 Acceptproduces 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,必须在 afterCompletionfinally 语义中清理,线程复用时遗漏清理会导致串请求。

认证与授权不要只靠一个“登录拦截器”。生产系统通常由 Spring Security 或网关验证 Token、签名和权限,Controller/Service 只处理已经验证的主体。业务数据权限仍需在查询条件和服务层校验,例如“当前用户是否可以读取该订单”,不能只根据前端传来的 userId 判断。

统一异常、响应与可观测性

@RestControllerAdvice 可集中处理异常,底层依赖 HandlerExceptionResolver。统一响应的目标是稳定 API 契约,而不是把所有异常包装成 HTTP 200。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError validation(MethodArgumentNotValidException ex) {
return ApiError.invalidArgument(ex.getBindingResult());
}

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiError> business(BusinessException ex) {
return ResponseEntity.status(ex.getHttpStatus())
.body(ApiError.of(ex.getCode(), ex.getMessage()));
}
}

建议至少区分:参数校验错误(400)、未认证(401)、无权限(403)、资源不存在(404)、业务冲突如幂等键重复或状态不允许(409)、限流(429)、依赖失败/服务异常(5xx)。系统异常记录完整堆栈、traceId、请求路径、方法、业务主键和脱敏后的参数;响应只返回稳定错误码和可理解提示。

异常处理不能吞掉异常后返回成功,也不能在 @ControllerAdvice 中直接打印 e.getMessage() 就结束。对于超时、连接池满、线程池拒绝等异常,除了返回合适状态码,还要关联下游指标、入口流量和重试量,避免重试风暴。

文件上传、下载与大响应

文件处理是 Spring MVC 常见的生产追问。MultipartFile 方便,但不代表可以把任意大文件一次读入内存。

上传流程:限制 max-file-sizemax-request-size 和文件数量;校验扩展名、MIME、魔数和业务归属;使用流式上传到对象存储或临时文件;异步做病毒扫描、转码和解析;数据库只保存对象 Key、大小、校验和、状态和权限元数据。不要信任客户端传来的文件名和 Content-Type

下载流程:小文件可由 Resource/ResponseEntity 返回;大文件使用流式输出、对象存储预签名 URL 或 Nginx/X-Accel-Redirect,避免应用 JVM 读取完整文件。支持断点续传时正确处理 RangeContent-RangeContent-Length,并在下载前校验租户和用户权限。

异步请求、超时与线程模型

Spring MVC 基于 Servlet 线程模型。普通 Controller 从容器工作线程开始处理,执行同步阻塞的数据库或 HTTP 调用时会占用该线程。CallableDeferredResultWebAsyncTaskCompletableFuture 可以将部分工作交给业务线程池,并在完成后恢复响应处理,但不会让阻塞 IO 自动变成非阻塞。

1
2
3
4
@GetMapping("/{id}/summary")
public CompletableFuture<OrderSummary> summary(@PathVariable Long id) {
return CompletableFuture.supplyAsync(() -> orderQueryService.summary(id), queryExecutor);
}

异步必须显式使用具名、有界、受监控的 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-Typeconsumes、Body 格式 JSON 未设置 application/json,或缺少对应 Converter
接口 406 Acceptproduces、返回类型 服务无法按客户端要求的媒体类型响应
参数总是 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 纳入整体治理。