Skip to content

为什么 Spring 循环依赖需要三级缓存,二级不够吗? ​

  • 标签:后端、Spring

🏛 业务架构师 ​

三级缓存各自的角色:一级放成品、二级放提前曝光的早期引用、三级放能生产早期引用的工厂。

二级不够的根本原因在 AOP 代理的生成时机。Spring 的设计原则是:代理应由 BeanPostProcessor 在 bean 初始化完成后统一生成。如果没有第三级:

  • 二级直接存"实例化后的半成品"→ B 注入的是没有代理的原始 A → @Transactional/@Async 对 A 全部失效 → 运行期诡异 bug(事务不回滚、异步不生效);
  • 为了解决代理,就得实例化后立刻生成代理放进二级 → 所有 bean 都提前代理化,破坏了 Spring 生命周期设计(初始化回调、代理生成顺序的语义全乱),为了让少数循环依赖场景工作,牺牲全部 bean 的正常流程。

三级缓存的工厂是精确的折中:正常路径零成本(工厂只是个占位);只有真的发生循环依赖(B 来找我)时才调用工厂,把代理"按需提前"生成——特例特办,主流程不动。

架构视角的两层体会:

  1. 这是"为少数异常场景做隔离性设计"的范本——类似思想:熔断(为故障路径单独设计快速失败)、读写分离(为慢查询单独设计路由)。不要让异常路径的复杂度污染主路径;
  2. 回到业务:循环依赖能不写就不写——框架给了你活路,但依赖成环通常意味着职责划分出了问题(该抽的事件没抽、该下沉的服务没下沉)。SpringBoot 2.6 已默认禁止循环依赖,官方态度也是"这是设计坏味道"。

一句话:三级缓存是框架的精妙兜底,欣赏它可以,依赖它不可以。

🏗 系统架构师 ​

先说二级缓存为什么不够,关键是AOP 代理的生成时机:

场景:A 循环依赖 B,且 A 需要 AOP 代理(@Transactional 等)。只用两级缓存的话,要么在 A 实例化后立刻生成代理放进二级缓存,要么等 A 初始化完成再生成。

  • 立刻生成的问题:违反 Spring 设计原则——代理应在 Bean 生命周期初始化之后(BeanPostProcessor 的 postProcessAfterInitialization)按需创建。实例化就生成代理,等于强制所有 Bean 提前定形,生命周期语义(@PostConstruct 之后再代理)被破坏;而且大多数 Bean 不参与循环依赖,为它们提前创建代理纯属浪费;
  • 三级缓存的解法:存 ObjectFactory(工厂/lambda)而不是成品。只有真的发生循环依赖(有别的 Bean 来取我)时,才调用工厂按需生成早期引用(有 AOP 则 getEarlyBeanReference 生成代理,否则原对象),放入二级缓存。绝大多数正常创建的 Bean 工厂从未被调用,代理仍按正常时机生成——两全。

一句话:三级缓存的第三级是"延迟决策"——用工厂把"要不要提前暴露代理"这个决定推迟到真正需要的那一刻,兼顾循环依赖支持与 Bean 生命周期的正确性。这也是"延迟求值"思想解决时序冲突的经典案例,比背"防循环依赖"的表象答案深一层。

💻 全栈开发专家 ​

二级不是不行,而是要以"破坏 AOP 代理的正常生命周期"为代价;三级缓存是为了兼容没有循环依赖时的正常路径。

推演一下只有二级缓存会怎样(成品 map + 早期引用 map):

  • 要支持循环依赖,B 来取 A 时必须能从二级拿到 A 的早期引用;
  • 如果 A 需要 AOP 代理,那这个早期引用必须是代理对象(B 注入代理,调用才走切面);
  • 于是 Spring 必须在 A 实例化后立刻决定:要不要代理、生成代理放进二级——
  • 但 Spring 的设计是代理在初始化完成后(BeanPostProcessor 后置处理)才生成(AnnotationAwareAspectJAutoProxyCreator 在初始化后执行)。

结果二选一:

  1. 二级缓存 + 实例化后马上生成代理 → 所有 Bean 的代理创建时机都被提前,没循环依赖的也受害,AOP 相关生命周期被整体打乱;
  2. 三级缓存存 ObjectFactory(lambda)→ 只有真的发生循环依赖、别人来 getSingleton(A) 时才调用工厂,工厂内部"该不该提前代理、怎么代理"延迟决定:
java
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 只有循环依赖触发 getEarlyBeanReference,正常 Bean 永远不执行这个 lambda

一句话:三级缓存 = 用"工厂延迟决策"保住无循环依赖 Bean 的标准生命周期,只在确实发生循环时才提前生成早期引用(含代理)。代价是如果提前暴露了代理,初始化后的代理生成步骤会跳过并校验一致性,保证同一引用。

能讲到"代理生成时机"这层,这道题的深度就到位了。