模块二 · 请求与框架
Spring 容器:一个 Bean 从创建到能被切面
Bean 的生命周期、三级缓存解决循环依赖、AOP 代理与事务失效,是 Spring 面试的四条主线。这一节按 Bean 的出生到销毁讲。
读完这一节,你能做到
- 按顺序说出 Bean 的生命周期关键节点。
- 用三级缓存解释循环依赖为什么能解决、什么情况解决不了。
- 说清 JDK 动态代理与 CGLIB 的选择。
- 判断一个
@Transactional为什么会失效。
一、Bean 的生命周期
实例化 → 属性注入 → Aware 回调 → BeanPostProcessor.before
→ 初始化(@PostConstruct / InitializingBean / init-method)
→ BeanPostProcessor.after(AOP 代理在这里生成)
→ 使用 → 销毁(@PreDestroy / DisposableBean / destroy-method)
几个容易混淆的点:
- 构造器注入发生在实例化阶段,此时其他 Bean 可能还没就绪。
@PostConstruct在属性注入之后执行,适合做校验与预热。- AOP 代理是在
BeanPostProcessor.after生成的,所以注入到其他 Bean 的是代理对象。 - 单例 Bean 在容器启动时创建(默认),原型 Bean 每次获取都新建。
二、循环依赖与三级缓存
一级缓存 singletonObjects:完整的 Bean
二级缓存 earlySingletonObjects:提前暴露的早期引用
三级缓存 singletonFactories:生成早期引用的工厂
A 依赖 B、B 依赖 A 时:
- 创建 A,实例化后把 A 的工厂放进三级缓存。
- 注入 B,发现 B 不存在,去创建 B。
- B 注入 A,从三级缓存拿到 A 的早期引用(必要时生成代理),放入二级缓存。
- B 创建完成,放入一级缓存。
- A 完成注入与初始化,放入一级缓存。
为什么需要三级而不是两级?因为 AOP 代理要在“提前暴露”时决定是否生成,工厂把这一步延迟到真正需要时。
解决不了的情况:构造器循环依赖(实例化都没完成)、原型 Bean、@Async 等后置代理。Spring Boot 2.6 之后默认禁止循环依赖,需要显式开启。
三级缓存的边界
三级缓存不是“越多越好”,而是为了在“提前暴露”与“代理生成”之间做延迟决策:
- 如果只有一级缓存,早期引用会被当成完整 Bean,可能拿到未初始化的对象。
- 如果只有两级,AOP 代理要么提前生成(可能代理错),要么永远不生成。
- 三级用工厂把“是否生成代理”推迟到真正被依赖时。
所以它解决的是“字段/Setter 注入的循环依赖”,不是所有循环依赖。设计上仍应尽量避免环。
三、AOP 与代理
| 方式 | 条件 | 特点 |
|---|---|---|
| JDK 动态代理 | 目标实现接口 | 基于接口,生成实现类 |
| CGLIB | 目标没有接口 | 生成子类,不能代理 final |
Spring Boot 默认 proxyTargetClass=true,优先 CGLIB。代理对象与目标对象是两个实例,所以:
- 同类内部调用不经过代理,事务/缓存/异步注解失效。
- 注入的是代理,
getClass()拿到的是代理类。 - final 方法与 private 方法无法被代理。
四、事务失效的六种场景
- 同类内部调用(this 调用)。
- 方法不是 public(部分代理模式下)。
- 异常被 catch 吞掉,没有抛出去。
- 抛的是检查异常,但默认只回滚 RuntimeException。
- 事务方法在另一个线程里执行。
- 数据库引擎不支持事务(如 MyISAM)。
修法:拆到另一个 Bean、用 AopContext 暴露代理、显式指定 rollbackFor、把异步放到事务外。
五、作用域与线程安全
| 作用域 | 生命周期 | 注意 |
|---|---|---|
| singleton | 容器级 | 默认,必须无状态 |
| prototype | 每次获取新建 | 销毁不归容器管 |
| request | 单次请求 | Web 环境 |
| session | 会话 | Web 环境 |
单例 Bean 如果有可变成员变量,就会有线程安全问题。Controller/Service 默认单例,不要把请求数据存到成员变量里。
六、常见坑
- 在构造器里做依赖注入后的初始化。
- 认为
@PostConstruct在构造器之前执行。 - 用 private 方法加
@Transactional。 - 循环依赖靠
@Lazy硬解,掩盖设计问题。 - 单例 Bean 持有请求级数据。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个 Bean,实现 InitializingBean 与 DisposableBean,打印生命周期顺序。
- 构造 A↔B 的字段循环依赖,观察启动日志;再改成构造器注入,看报错。
- 写一个同类调用导致事务失效的例子,用另一个 Bean 修好。
② 让 AI 写同一题
把第 2 题(Bean 生命周期 + 切面)交给 AI,补一句 说明代理是怎么创建的,以及什么情况下切面会失效。
③ 对比要盯的点
- 注入方式它用构造器还是字段?后者在单测里很难替换。
- 循环依赖的解释它是不是还停在“三级缓存”四个字?说不清是哪三级就等于没答。
- 它有没有提醒“同类方法自调用不走代理”?这是切面失效的头号原因。
④ 一句话取舍
它给的方案里,哪一处会让切面在线上悄悄失效?
八、自测
- Bean 生命周期的关键节点有哪些?
- 三级缓存分别存什么?
- 为什么需要三级而不是两级?
- JDK 代理与 CGLIB 的选择标准?
- 事务失效的六种场景?
参考答案要点:实例化/注入/Aware/BPP/初始化/使用/销毁;完整/早期/工厂;延迟决定代理;有无接口;this/public/吞异常/检查异常/线程/引擎。
附录:高频追问
问:BeanFactory 与 ApplicationContext 的区别? 后者是前者的扩展,增加了事件、国际化、资源加载与自动装配。
问:@Autowired 与 @Resource 的区别? 前者按类型(可配合 @Qualifier 按名称),后者按名称,来自 JSR-250。
问:为什么构造器注入更好? 依赖不可变、便于测试、能尽早暴露循环依赖。
问:BeanPostProcessor 与 BeanFactoryPostProcessor 的区别? 前者处理 Bean 实例,后者处理 BeanDefinition。
问:Spring 如何解决循环依赖? 三级缓存 + 提前暴露;构造器注入与原型 Bean 无法解决。
问:AOP 有哪些通知类型? Before、After、AfterReturning、AfterThrowing、Around。
问:事务的传播行为有哪些? REQUIRED、REQUIRES_NEW、NESTED、SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER。
问:为什么 @Async 会失效? 同类调用、方法非 public、线程池未配置、返回值不是 void/Future。
问:单例 Bean 如何保证线程安全? 无状态、使用局部变量、必要时加锁或用 ThreadLocal。
问:Spring Boot 自动装配的原理? @EnableAutoConfiguration + spring.factories/AutoConfiguration.imports + 条件注解。
事务与异步的边界
@Async 会把方法丢到另一个线程,事务上下文不会传播。常见错误是:在事务方法里调用异步方法,期望它参与同一个事务——实际不会,异步线程用的是新连接。
正确做法:事务提交后再触发异步(TransactionSynchronization.afterCommit),或把异步放到事务外层。
单例 Bean 的线程安全
Spring 默认单例,Controller/Service 通常是无状态的,所以安全;一旦把请求数据存进成员变量,就会互相覆盖。三种修法:改成方法局部变量、用 ThreadLocal(记得 remove)、把作用域改成 prototype/request。注入的一定是代理:同类调用不经过代理,所以事务/缓存/异步会失效。代理是边界,越界即失效;先看代理。先代理。
补遗:Spring Boot 自动装配
自动装配的本质是“条件化地注册 Bean”:
@SpringBootApplication包含@EnableAutoConfiguration。- 启动时读取
AutoConfiguration.imports(旧版是spring.factories)里的配置类。 - 每个配置类上的
@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty决定是否生效。
所以“为什么我的 Bean 没生效”通常看三件事:条件是否满足、是否被自定义 Bean 覆盖、包扫描是否覆盖到。
补遗:事务传播与隔离
| 传播行为 | 含义 | 典型场景 |
|---|---|---|
| REQUIRED | 有事务就加入,没有就新建 | 默认 |
| REQUIRES_NEW | 挂起当前,新建独立事务 | 日志、审计 |
| NESTED | 嵌套事务,可回滚到保存点 | 部分失败不影响外层 |
| SUPPORTS | 有就加入,没有就非事务 | 查询 |
| NOT_SUPPORTED | 挂起事务,非事务执行 | 远程调用 |
隔离级别默认跟随数据库(MySQL 是可重复读)。传播行为解决“事务边界”,隔离级别解决“并发读写可见性”,两者不是一回事。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解