2.2 DispatcherServlet的分发逻辑


2.2 DispatcherServlet 的分发逻辑

本节摘要:DispatcherServlet 是 Spring MVC 的总调度,它自己不处理业务,而是协调一组协作组件完成"找到方法、调用方法、处理结果、兜底异常"四件事。本节按它的实际执行步骤走一遍,并解释每一步出问题时你在页面上看到的症状。

为什么需要一个总调度

假如没有前端控制器,每个 Servlet 各自处理各自的路径,跨路径的公共事务——视图解析、文件上传、异常兜底——就得在每个 Servlet 里重复实现。DispatcherServlet 把这些公共事务集中到一处,控制器因此可以瘦成"一个方法对应一次调用"。这是典型的门面加调度的组合:控制器只关心业务语义,其余一切由总调度负责。

分发的六步

一个请求进入分发器后,大致经历六步:

  1. 定位处理器:询问 HandlerMapping,得到路径与方法加拦截器链的组合
  2. 适配调用:通过 HandlerAdapter 屏蔽控制器实现差异,注解控制器、函数式端点各有适配器
  3. 参数解析与绑定:路径变量、请求参数、请求体逐个解析并转换类型
  4. 调用方法:业务代码执行,返回一个结果对象
  5. 结果处理:返回值经消息转换器写为 JSON,或交给视图解析器渲染页面
  6. 异常兜底:任何一步抛出异常,交给异常解析器转成友好的响应

第 3 步是最容易出问题的环节,值得看一段绑定失败的实例。控制器声明:

@GetMapping("/orders/{id}") public OrderVO detail(@PathVariable Long id) { ... }

若路径传来一段文字,类型转换失败,默认返回 400。更好的做法是把校验错误收拢到一处:

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<String> badParam(MethodArgumentTypeMismatchException e) { return ResponseEntity.badRequest() .body("参数 " + e.getName() + " 类型不正确"); } }

@RestControllerAdvice 生效的位置就在第 6 步:分发器捕获异常后遍历注册的解析器,找到能处理该异常类型的处理方法。异常处理之所以能全局收口,正是因为所有请求都经过同一个分发器。

图 2-2 分发六步与协作组件

图 2-2 分发六步与协作组件

从症状反推断点

排错时先定位旅程断在哪一步,比通读代码快得多:404 断在定位处理器(路径没映射或被拦截器吞掉);400 多半断在参数绑定;500 且日志无业务栈,常是异常解析缺位或序列化失败(循环引用是常客)。养成"先判站、再查因"的习惯。

实战:把六步日志全部打开做一次诊断

背景:一个线上偶发的 500,仅凭响应体无从下手,需要看到分发器内部每一步的裁决过程。操作:把分发器所在包的日志级别调到最细:

logging: level: org.springframework.web.servlet: DEBUG org.springframework.http.converter: DEBUG # 观察结果处理与序列化

发一次问题请求,日志会依次出现:映射查找与命中记录、适配器选择、参数绑定细节(含类型转换)、返回值处理器选择、消息转换器写出的内容类型。结果:一次成功的请求日志约十几行,逐行对应六步;问题请求则在某一行之后戛然而止——断点站就此暴露。

解读一段真实形态的日志:若日志停在"命中映射"之后、没有任何绑定记录,问题在进入方法之前(拦截器拒绝或适配异常);若有绑定记录但没有返回值处理,方法内部抛了异常且没有兜底解析器,此时 500 的根因要看更早的应用日志。变式:把全局异常处理类临时移除再重放请求,对比日志与响应体的差别,能直观看到第六步兜底的价值——没有它,用户看到的是容器默认错误页,有了它才是业务友好的错误信息。

六步之外的两个延伸问题

其一,分发器只有一个,它会不会成为性能瓶颈?不会成为计算瓶颈,因为它的职责是查表与转交,两步都是微秒级;真正的耗时永远在业务方法与数据库那一段。理解这一点有助于把优化精力放对地方——调优分发器配置的收益,远小于优化一条慢查询。其二,函数式端点与注解控制器会被同一套分发步骤处理吗?会。第 2 步的适配器设计正是为此存在:不同风格的处理器各有对应适配器,分发器本体不关心处理器是什么写法。这也解释了为什么"换一种控制器写法"不影响旅程前两站的行为,过滤器与安全链照常生效。把六步当成排错的坐标系,比当成背诵清单有用得多——每一次异常响应,都可以先问"断在第几步"。

本节要点回顾

  • 总调度集中公共事务:控制器因此可以保持极薄
  • 六步分发:定位、适配、绑定、调用、结果、异常
  • 全局异常收口依赖统一入口: advice 组件在第六步被调用
  • 按症状判站:404、400、500 分别对应三类断点位置

读完自测

合上书回答三问:适配器这层存在的理由是什么;参数绑定失败默认响应什么状态码;全局异常处理为什么必须依赖"所有请求过同一入口"这一前提。三问全对,本节过关。

再补一段容易被忽略的细节:分发器对每次请求都会准备一个属性袋,过滤器、拦截器、控制器通过它传递中间数据——2.3 节限流示例里暂存起始时间的做法就是标准用法。它随请求生灭,天然线程安全,是入口层各组件协作的便签板。理解了它,过滤器与拦截器之间怎么传话的问题就有了答案,也解释了为什么这些组件之间不靠全局变量通信。
顺带一提,属性袋里还能存放本次请求的追踪标识,网关或最外层过滤器写入后,日志框架可以统一取出拼进每行日志——第 8 章的链路追踪在单进程内正是靠这个机制把同一请求的所有日志串成一条线,这一点提前记住,后面会豁然开朗。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U