知

模块二 · 请求与框架

第 4 / 11 节20 分钟阅读型

Spring 容器:一个 Bean 从创建到能被切面

Bean 的生命周期、三级缓存解决循环依赖、AOP 代理与事务失效,是 Spring 面试的四条主线。这一节按 Bean 的出生到销毁讲。

读完这一节,你能做到

  1. 按顺序说出 Bean 的生命周期关键节点。
  2. 用三级缓存解释循环依赖为什么能解决、什么情况解决不了。
  3. 说清 JDK 动态代理与 CGLIB 的选择。
  4. 判断一个 @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 时:

  1. 创建 A,实例化后把 A 的工厂放进三级缓存。
  2. 注入 B,发现 B 不存在,去创建 B。
  3. B 注入 A,从三级缓存拿到 A 的早期引用(必要时生成代理),放入二级缓存。
  4. B 创建完成,放入一级缓存。
  5. A 完成注入与初始化,放入一级缓存。

为什么需要三级而不是两级?因为 AOP 代理要在“提前暴露”时决定是否生成,工厂把这一步延迟到真正需要时。

解决不了的情况:构造器循环依赖(实例化都没完成)、原型 Bean、@Async 等后置代理。Spring Boot 2.6 之后默认禁止循环依赖,需要显式开启。

三级缓存的边界

三级缓存不是“越多越好”,而是为了在“提前暴露”与“代理生成”之间做延迟决策:

  • 如果只有一级缓存,早期引用会被当成完整 Bean,可能拿到未初始化的对象。
  • 如果只有两级,AOP 代理要么提前生成(可能代理错),要么永远不生成。
  • 三级用工厂把“是否生成代理”推迟到真正被依赖时。

所以它解决的是“字段/Setter 注入的循环依赖”,不是所有循环依赖。设计上仍应尽量避免环。

三、AOP 与代理

方式条件特点
JDK 动态代理目标实现接口基于接口,生成实现类
CGLIB目标没有接口生成子类,不能代理 final

Spring Boot 默认 proxyTargetClass=true,优先 CGLIB。代理对象与目标对象是两个实例,所以:

  • 同类内部调用不经过代理,事务/缓存/异步注解失效。
  • 注入的是代理,getClass() 拿到的是代理类。
  • final 方法与 private 方法无法被代理。

四、事务失效的六种场景

  1. 同类内部调用(this 调用)。
  2. 方法不是 public(部分代理模式下)。
  3. 异常被 catch 吞掉,没有抛出去。
  4. 抛的是检查异常,但默认只回滚 RuntimeException。
  5. 事务方法在另一个线程里执行。
  6. 数据库引擎不支持事务(如 MyISAM)。

修法:拆到另一个 Bean、用 AopContext 暴露代理、显式指定 rollbackFor、把异步放到事务外。

五、作用域与线程安全

作用域生命周期注意
singleton容器级默认,必须无状态
prototype每次获取新建销毁不归容器管
request单次请求Web 环境
session会话Web 环境

单例 Bean 如果有可变成员变量,就会有线程安全问题。Controller/Service 默认单例,不要把请求数据存到成员变量里。

六、常见坑

  1. 在构造器里做依赖注入后的初始化。
  2. 认为 @PostConstruct 在构造器之前执行。
  3. 用 private 方法加 @Transactional。
  4. 循环依赖靠 @Lazy 硬解,掩盖设计问题。
  5. 单例 Bean 持有请求级数据。

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

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

① 自己写

  1. 写一个 Bean,实现 InitializingBean 与 DisposableBean,打印生命周期顺序。
  2. 构造 A↔B 的字段循环依赖,观察启动日志;再改成构造器注入,看报错。
  3. 写一个同类调用导致事务失效的例子,用另一个 Bean 修好。

② 让 AI 写同一题

把第 2 题(Bean 生命周期 + 切面)交给 AI,补一句 说明代理是怎么创建的,以及什么情况下切面会失效。

③ 对比要盯的点

  • 注入方式它用构造器还是字段?后者在单测里很难替换。
  • 循环依赖的解释它是不是还停在“三级缓存”四个字?说不清是哪三级就等于没答。
  • 它有没有提醒“同类方法自调用不走代理”?这是切面失效的头号原因。

④ 一句话取舍

它给的方案里,哪一处会让切面在线上悄悄失效?

八、自测

  1. Bean 生命周期的关键节点有哪些?
  2. 三级缓存分别存什么?
  3. 为什么需要三级而不是两级?
  4. JDK 代理与 CGLIB 的选择标准?
  5. 事务失效的六种场景?

参考答案要点:实例化/注入/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”:

  1. @SpringBootApplication 包含 @EnableAutoConfiguration。
  2. 启动时读取 AutoConfiguration.imports(旧版是 spring.factories)里的配置类。
  3. 每个配置类上的 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 决定是否生效。

所以“为什么我的 Bean 没生效”通常看三件事:条件是否满足、是否被自定义 Bean 覆盖、包扫描是否覆盖到。

补遗:事务传播与隔离

传播行为含义典型场景
REQUIRED有事务就加入,没有就新建默认
REQUIRES_NEW挂起当前,新建独立事务日志、审计
NESTED嵌套事务,可回滚到保存点部分失败不影响外层
SUPPORTS有就加入,没有就非事务查询
NOT_SUPPORTED挂起事务,非事务执行远程调用

隔离级别默认跟随数据库(MySQL 是可重复读)。传播行为解决“事务边界”,隔离级别解决“并发读写可见性”,两者不是一回事。

配套题目

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

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