DispatcherServlet 请求总链:一次调用如何找到方法并提交响应
一个请求最终进入 Controller,并不意味着它只经历了一次方法反射。真正的执行单元是由映射、拦截器、适配器、参数解析、返回值处理和异常解析共同组成的策略链。若不知道响应在哪一层提交,排查 404、重复写响应和异常未捕获就只能靠猜。
Spring MVC 建立在 Servlet 容器之上。理解它时应把“Servlet 分派类型、MVC 策略链、Controller 方法调用、响应提交”分成四层;Spring Boot 负责装配这些基础设施,却没有改变 DispatcherServlet 的核心分发语义。机制入口可对照 Spring Framework 官方的 DispatcherServlet 与 注解式 Controller 参考。
先看 Spring MVC 请求链路
Spring MVC 的核心是 DispatcherServlet。所有请求先进入它,再由它找到 Controller 方法、解析参数、执行方法、处理返回值。
这张图排障时很有用。
如果接口 404,优先查 HandlerMapping 有没有匹配。
如果 400,优先查参数绑定和校验。
如果 415,优先查 Content-Type 和消息转换器。
如果日志 traceId 缺失,优先查 Filter 是否执行。
如果 Interceptor 没进来,优先查请求有没有到 DispatcherServlet。
把图拆成源码对象,大致是这条链:
| 环节 | 关键对象 | 主要职责 | 常见现象 |
|---|---|---|---|
| Servlet 入口 | DispatcherServlet | 前端控制器,统一执行请求分发算法 | 404、异常没有被统一处理、静态资源被误拦 |
| 找方法 | RequestMappingHandlerMapping | 把请求路径、HTTP 方法、consumes、produces、版本信息匹配到 HandlerMethod | 404、405、接口版本不命中 |
| 拦截链 | HandlerExecutionChain | 持有 handler 和匹配到的 HandlerInterceptor 列表 | Interceptor 没执行、顺序不对 |
| 调方法 | RequestMappingHandlerAdapter | 调用注解 Controller 方法,协调参数解析、校验和返回值处理 | 400、415、406、返回值序列化失败 |
| 解析参数 | HandlerMethodArgumentResolverComposite | 按参数类型和注解选择具体解析器 | query/path/header/body 取值混乱 |
| 绑定校验 | WebDataBinderFactory、WebDataBinder、ConversionService、Validator | 类型转换、对象绑定、Bean Validation | 类型转换失败、校验没生效 |
| 写响应 | HandlerMethodReturnValueHandlerComposite、HttpMessageConverter | 处理 ResponseEntity、@ResponseBody、JSON、字符串、文件流 | 406、中文乱码、JSON 字段异常 |
| 处理异常 | HandlerExceptionResolverComposite | 把 MVC 异常和业务异常转换成 HTTP 响应 | 错误码失真、ProblemDetail 不生效 |
Spring MVC 启动时会先把这些策略对象准备好。RequestMappingHandlerMapping 会扫描 Controller,把 @RequestMapping、@GetMapping 等方法整理成 RequestMappingInfo -> HandlerMethod 的映射;RequestMappingHandlerAdapter 会准备参数解析器、返回值处理器、@ControllerAdvice、@InitBinder、RequestBodyAdvice 和 ResponseBodyAdvice。请求真正进来时,不是临时再反射全项目,而是沿着这些已经初始化好的映射和处理器往下走。
这里最容易被忽略的是:Spring MVC 的很多“魔法”其实是按顺序遍历策略列表。参数解析器、返回值处理器、异常解析器都是这样。线上出现“明明有注解却不生效”时,不要只盯 Controller,先判断请求到底卡在匹配、绑定、校验、转换、返回还是异常解析哪一层。
排障手册:按状态码走
线上排障不要一上来就改代码。先把请求方法、URI、HTTP 状态码、业务 code、响应头里的 traceId 和应用日志对齐,再决定查映射、绑定、校验、鉴权、业务状态还是下游。
这张图的用法很简单:非 2xx 先按 HTTP 状态码分流,再读业务 code 做细分。设计评审时要检查每个分支是否都有可复现命令、稳定错误体、日志级别和调用方动作;如果只能看到“系统异常”但没有 traceId,排障链路就是断的。
404 Not Found
请求:
curl -i http://127.0.0.1:8080/api/v1/order/1001检查:
路径是否写错
Controller 是否被扫描
类上和方法上的 @RequestMapping 是否拼接正确
是否配置了 server.servlet.context-path
是否被 Nginx 或网关改写路径命令:
curl -i http://127.0.0.1:8080/actuator/mappings/actuator/mappings 可能暴露接口结构,生产不要公网开放。
405 Method Not Allowed
现象:接口存在,但 HTTP 方法错了。
curl -i -X DELETE http://127.0.0.1:8080/api/v1/orders/1001检查 Controller 是否只写了 @GetMapping 或 @PostMapping。不要用一个 @RequestMapping 不写 method 接住所有请求,后期很难治理。
400 Bad Request
常见原因:
query 参数类型转换失败
path 变量类型转换失败
JSON 格式错误
校验失败
缺少必填参数命令:
curl -i 'http://127.0.0.1:8080/api/v1/orders?page=abc'看日志时要找具体异常:
MethodArgumentTypeMismatchException
HttpMessageNotReadableException
MethodArgumentNotValidException
HandlerMethodValidationException415 Unsupported Media Type
现象:POST JSON 时没加 Content-Type。
curl -i -X POST http://127.0.0.1:8080/api/v1/orders \
-d '{"skuCode":"SKU-001","amount":99.00}'修复:
curl -i -X POST http://127.0.0.1:8080/api/v1/orders \
-H 'Content-Type: application/json' \
-d '{"skuCode":"SKU-001","amount":99.00}'500 Internal Server Error
500 不是前端问题。先找 traceId,再查应用日志。
curl -i http://127.0.0.1:8080/api/v1/orders/1001 -H 'X-Request-Id: debug-001'
grep 'debug-001' logs/order-service.log如果日志没有 traceId,先修 Filter 和日志 pattern。
Interceptor 没执行
检查:
请求路径是否命中 addPathPatterns
是否被 excludePathPatterns 排除
请求是否被 Filter 或 Security 提前拦截
是否访问的是静态资源或 Actuator
是否手动加了 @EnableWebMvc 导致配置变化校验没生效
检查:
是否引入 spring-boot-starter-validation
@RequestBody 前是否加 @Valid
约束注解是否来自 jakarta.validation
方法参数约束是否被当前 Spring MVC 版本支持
全局异常是否吞掉校验异常命令:
mvn -q dependency:tree | grep validation用可运行模型固定边界
两个模型不依赖 Spring 运行时,而是把策略选择和状态迁移压缩成 Java 17 程序。它们不是框架源码替身;价值在于先固定不变量,再回到真实应用观察哪个扩展点破坏了不变量。
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-mvc/dispatcher-chain/DispatcherChainDemo.java examples/backend-development/spring-mvc/dispatcher-chain/ExceptionResolutionDemo.java
java -cp examples/backend-development/spring-mvc/dispatcher-chain DispatcherChainDemo
java -cp examples/backend-development/spring-mvc/dispatcher-chain ExceptionResolutionDemophases=[mapping, adapter, preHandle, handler, returnValue, afterCompletion] responseCommitted=true
handlerException=OrderMissing resolver=ExceptionHandler responseCommitted=false第一条输出验证正常路径,第二条刻意暴露分支或失败路径。把这两条输出放进持续集成,能防止重构只保持“接口能返回”,却改变匹配优先级、清理时机或错误契约。
状态图强调“选择先于执行、提交限制补救、所有分支最终清理”。真实框架包含更多策略对象,但任何自定义扩展都不应打破这三个约束。
把故障定位到第一个发生偏差的阶段
排障记录至少保留请求方法、规范化路径、Content-Type、Accept、命中的 handler、参数解析器或转换器、异常解析器、最终状态码以及响应是否已经提交。不要从最终状态码倒推唯一原因:同一个 400 可能来自类型转换、缺失参数、消息体不可读或校验失败;同一个 500 也可能发生在 Controller、序列化或异步完成阶段。先寻找第一个偏差,后续异常往往只是连锁反应。
生产观测要控制基数。handler 模板可以作为指标标签,原始 URL、用户 ID、游标和异常消息不能直接成为标签。细节进入带采样和脱敏的日志或 trace;指标只回答哪一阶段、哪类结果、耗时分布是否偏离基线。
建立可回归的完成标准
完成标准不是“本地 curl 成功”,而是正常、拒绝、异常、超时和清理路径都有证据:映射结果可解释,输入边界不可绕过,错误契约稳定,响应提交后不再尝试改写,异步工作在取消后收敛,指标和日志既能关联又不泄露敏感数据。单元模型固定算法不变量,MockMvc 或 RestTestClient 固定 MVC 语义,少量真实容器测试固定 Filter、dispatcher type、网络与提交行为。
当一个问题出现时,先判断它属于 Servlet 入口、MVC 策略选择、业务 handler 还是响应输出,再选择证据和修复点。这样扩展 Spring MVC 时增加的是明确策略,而不是更多互相覆盖的全局钩子。
