模块二 · 请求与框架
一次请求的旅程:从 HTTP 到 Spring 到数据库
把 Filter、DispatcherServlet、HandlerMapping、参数绑定、返回值处理与事务串成一条链路。这一节按请求经过的顺序讲,每一站都能挂一个面试题。
读完这一节,你能做到
- 按顺序说出一次请求在 Spring MVC 中的完整链路。
- 说清 Filter、Interceptor、AOP 三者的区别与顺序。
- 解释参数绑定、校验与全局异常处理的位置。
- 判断一次请求变慢该查哪一段。
一、容器层: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、复杂泛型 |
| 业务与 SQL | 10-500ms | N+1、慢查询、锁等待 |
| 序列化 | 1-50ms | 大对象、循环引用 |
先量出每段的占比,再决定优化哪里。没有分布数据就改代码,通常是在猜。
八、常见坑
- 在 Filter 里读
@RequestBody,导致 Controller 读不到。 - Interceptor 里抛异常,没有被全局异常处理捕获。
@Transactional加在 private 方法上,代理不生效。- 全局异常把业务异常也包成 500,前端无法区分。
- 返回大对象导致序列化耗时超过 SQL。
九、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个 Filter 与 Interceptor,打印同一次请求的执行顺序。
- 用
@RestControllerAdvice统一处理参数校验与业务异常。 - 用 Arthas 或日志耗时打点,定位一次慢请求的耗时分布。
② 让 AI 写同一题
把第 2 题(一条写接口)交给 AI,补一句 事务边界要明确,事务内不许调外部 HTTP。
③ 对比要盯的点
@Transactional是同类自调用吗?那样代理不生效,事务根本没开。- 它把 HTTP 调用或消息发送放在事务里了吗?长事务会拖死连接池。
- 查询它有没有写出 N+1?循环里逐条查是最常见的产物。
④ 一句话取舍
事务回滚时,它那句“已发送的消息”会怎样?
十、自测
- 一次请求在 Spring MVC 中的完整链路?
- Filter、Interceptor、AOP 的执行顺序?
@RequestBody与@RequestParam的区别?- 全局异常处理应该放在哪一层?
@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 或大小做限制。
拦截器的三个方法
| 方法 | 时机 | 返回值 |
|---|---|---|
| preHandle | Controller 前 | false 中断请求 |
| postHandle | Controller 后、视图前 | 无,可改 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 过多通常是短连接太多,用连接池/长连接解决。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ Spring Boot 自动配置的原理是什么?
- ★★★☆☆ Spring Boot 的配置加载优先级是怎样的?
- ★★★★★ Spring Bean 的生命周期是怎样的?
- ★★★★☆ 五种 IO 模型是什么?epoll 为什么比 select 高效?
提示:读完可以顺手做几道对应题目,检验是否真的理解