知

模块二 · 请求与框架

第 3 / 11 节20 分钟阅读型

一次请求的旅程:从 HTTP 到 Spring 到数据库

把 Filter、DispatcherServlet、HandlerMapping、参数绑定、返回值处理与事务串成一条链路。这一节按请求经过的顺序讲,每一站都能挂一个面试题。

读完这一节,你能做到

  1. 按顺序说出一次请求在 Spring MVC 中的完整链路。
  2. 说清 Filter、Interceptor、AOP 三者的区别与顺序。
  3. 解释参数绑定、校验与全局异常处理的位置。
  4. 判断一次请求变慢该查哪一段。

一、容器层:Tomcat 与线程

请求先到 Tomcat,由连接器接收,交给工作线程池处理。默认线程数在几十到两百之间,队列满后会拒绝或排队。

三个关键点:

  • 一个请求占用一个线程,线程在处理期间不能被复用。
  • 阻塞操作(慢 SQL、同步 HTTP 调用)会占住线程,导致吞吐下降。
  • 异步 Servlet 与响应式编程可以把线程还回去,但复杂度上升。

所以“接口慢”不只是代码问题,也可能是线程池被打满。

二、过滤器与前端控制器

HTTP → Filter 链 → DispatcherServlet → HandlerMapping → Interceptor
→ 参数解析 → Controller → 返回值处理 → 消息转换 → 响应

Filter 属于 Servlet 规范,能拿到原始请求,适合做编码、鉴权、日志、跨域。DispatcherServlet 是前端控制器,负责把请求分发给对应的 Handler。

Filter 在 DispatcherServlet 之前,Interceptor 在之后;AOP 又在 Controller 方法调用层面。顺序是:

Filter → Interceptor.preHandle → AOP around → Controller
→ AOP after → Interceptor.postHandle → 视图/消息转换 → Filter 返回

三、路由与参数绑定

HandlerMapping 根据 URL、方法、请求头找到 Handler。参数绑定由 HandlerMethodArgumentResolver 完成:

参数解析方式
@PathVariable从 URL 模板取值
@RequestParam从 query/form 取值
@RequestBody用 HttpMessageConverter 反序列化
@RequestHeader从请求头取值
自定义对象按字段名绑定

@RequestBody 依赖 Jackson,字段名不匹配会丢值;日期格式要显式配置,否则反序列化报错。

四、校验与异常

参数校验用 @Valid + JSR-303 注解,失败抛 MethodArgumentNotValidException。全局异常处理用 @RestControllerAdvice:

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(MethodArgumentNotValidException.class)
  public Result<?> handle(MethodArgumentNotValidException e) {
    String msg = e.getBindingResult().getFieldErrors().stream()
      .map(f -> f.getField() + ":" + f.getDefaultMessage())
      .collect(Collectors.joining("; "));
    return Result.fail(400, msg);
  }
}

异常处理要分三层:参数错误(400)、业务错误(自定义码)、系统错误(500)。不要把所有异常都包成 200。

五、返回值与消息转换

Controller 返回对象时,由 HttpMessageConverter 序列化成 JSON;返回 String 且标注 @RestController 时也走消息转换,而不是视图解析。

常见问题:

  • 返回 null 时字段消失(Jackson 默认忽略 null),需要配置 NON_NULL 策略。
  • 日期序列化成时间戳,需要统一格式与时区。
  • 大字段(如 Base64 图片)会拖慢序列化,考虑分接口。

六、事务与数据库

Service 层加 @Transactional,本质是 AOP 代理:方法调用前开启事务,正常返回提交,抛异常回滚。

Controller → Service(事务代理)→ Mapper/JPA → 连接池 → 数据库

事务失效的常见原因:同类内部调用、方法不是 public、异常被吞、只读事务里写、跨线程。这些都与“代理没有生效”有关。

七、一次请求变慢怎么查

现象先查
所有接口都慢线程池、连接池、GC
单个接口慢SQL、外部调用、序列化
偶发慢锁竞争、慢查询、网络抖动
高峰慢队列积压、限流、扩容

顺序是:先看监控定位“哪一段慢”,再看日志与链路追踪,最后才改代码。

一次请求的耗时分布

段落典型耗时常见问题
网络与容器1-10ms连接复用、线程池
Filter/拦截器<1ms重复读流、同步远程调用
参数解析<1ms大 Body、复杂泛型
业务与 SQL10-500msN+1、慢查询、锁等待
序列化1-50ms大对象、循环引用

先量出每段的占比,再决定优化哪里。没有分布数据就改代码,通常是在猜。

八、常见坑

  1. 在 Filter 里读 @RequestBody,导致 Controller 读不到。
  2. Interceptor 里抛异常,没有被全局异常处理捕获。
  3. @Transactional 加在 private 方法上,代理不生效。
  4. 全局异常把业务异常也包成 500,前端无法区分。
  5. 返回大对象导致序列化耗时超过 SQL。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个 Filter 与 Interceptor,打印同一次请求的执行顺序。
  2. 用 @RestControllerAdvice 统一处理参数校验与业务异常。
  3. 用 Arthas 或日志耗时打点,定位一次慢请求的耗时分布。

② 让 AI 写同一题

把第 2 题(一条写接口)交给 AI,补一句 事务边界要明确,事务内不许调外部 HTTP。

③ 对比要盯的点

  • @Transactional 是同类自调用吗?那样代理不生效,事务根本没开。
  • 它把 HTTP 调用或消息发送放在事务里了吗?长事务会拖死连接池。
  • 查询它有没有写出 N+1?循环里逐条查是最常见的产物。

④ 一句话取舍

事务回滚时,它那句“已发送的消息”会怎样?

十、自测

  1. 一次请求在 Spring MVC 中的完整链路?
  2. Filter、Interceptor、AOP 的执行顺序?
  3. @RequestBody 与 @RequestParam 的区别?
  4. 全局异常处理应该放在哪一层?
  5. @Transactional 失效的常见原因?

参考答案要点:Filter→DS→Mapping→Interceptor→Controller→转换;由外到内;体 vs 参数;ControllerAdvice;代理失效。

附录:高频追问

问:Filter 和 Interceptor 的区别? Filter 属于 Servlet 容器,能改请求/响应;Interceptor 属于 Spring MVC,能拿到 Handler 与方法信息。

问:DispatcherServlet 做了什么? 前端控制器:路由查找、拦截器调用、参数解析、返回值处理、异常分派。

问:@RequestBody 只能读一次吗? 默认流只能读一次;Filter 里读了要包装成可重复读的 Request。

问:参数校验失败返回什么状态码? 通常 400;业务校验失败用 200 + 业务码,看团队规范。

问:事务和 AOP 的关系? @Transactional 通过 AOP 代理实现,代理没生效事务就不生效。

问:为什么同类调用事务失效? 调用走的是 this,不经过代理对象。

问:接口幂等怎么做? 唯一索引、令牌、状态机、去重表;按业务代价选择。

问:Tomcat 线程池满了会怎样? 先排队,队列满后拒绝;表现为超时或连接被拒。

问:如何定位慢请求? 链路追踪 + 分段耗时(Filter、Service、SQL、外部调用)。

问:为什么不要在 Controller 里写业务? 难复用、难测试、事务边界不清;Controller 只做参数与协议适配。

Filter 里读 Body 的正确做法

原始 InputStream 只能读一次,Filter 里读了,Controller 就拿不到。做法是用 ContentCachingRequestWrapper 或自定义 Wrapper 把字节缓存下来,重写 getInputStream,让后续可以重复读。

还要注意大文件上传:不要把所有请求体都缓存进内存,按 Content-Type 或大小做限制。

拦截器的三个方法

方法时机返回值
preHandleController 前false 中断请求
postHandleController 后、视图前无,可改 ModelAndView
afterCompletion请求完成无,可记录异常与耗时

注意:preHandle 返回 false 时,已执行过的拦截器仍会走 afterCompletion,适合做资源清理。

过滤器与拦截器的选择

需要改请求/响应本身(编码、包装流、跨域)用 Filter;需要知道 Controller 与方法信息(鉴权、日志、限流)用 Interceptor;需要围绕方法调用做增强(事务、缓存、重试)用 AOP。先选层,再写逻辑。先分层。

补遗:HTTP 与 TCP 基础

HTTP/1.1 默认长连接,但有队头阻塞;HTTP/2 多路复用、头部压缩;HTTP/3 基于 QUIC(UDP),减少握手延迟。

HTTPS = HTTP + TLS:非对称握手协商密钥,对称加密传输;证书链要可信。

TCP 三次握手(SYN/SYN-ACK/ACK)建连接,四次挥手断连接;TIME_WAIT 过多通常是短连接太多,用连接池/长连接解决。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解