← 返回博客
2026-10-20 08:00:01

三级缓存为什么能解开循环依赖

三级缓存为什么能解开循环依赖

两个 Service 互相注入,Spring 能启动成功;换成构造器注入,直接报错。差别在哪?这篇把三级缓存拆开看。

手把手实操

先造一个能复现的场景。两个类互相持有对方:

@Service
public class AService {
    @Autowired private BService bService;
}
@Service
public class BService {
    @Autowired private AService aService;
}

启动能过。把 @Autowired 换成构造器注入,启动直接抛 BeanCurrentlyInCreationException。这是第一个要记住的边界:三级缓存只救 setter/字段注入,救不了构造器注入。原因后面讲。

再做一个能看见"提前曝光"的实验。给 AService 加一个 @PostConstruct,里面打印 bService 是否为空:

@PostConstruct
public void init() {
    System.out.println("A 初始化时 bService = " + bService);
}

你会看到打印出的是 BService 的代理对象或实例,不是 null。说明 A 在还没走完初始化时,B 已经被塞进去了。

想看得更细,打开 debug 日志:logging.level.org.springframework.beans.factory=DEBUG,日志里会出现 Creating shared instance of singleton bean 和 Eagerly caching bean 这类行,顺序能对上后面讲的调用链。

看源码及解析

入口在 DefaultSingletonBeanRegistry,三个字段就是三级缓存:

getSingleton(String, boolean) 的查找顺序是一级 → 二级 → 三级。三级命中时调 factory.getObject() 拿到对象,同时把它挪进二级并删掉三级。这一步是设计核心:同一个 Bean 只允许被提前曝光一次,之后复用二级里的引用,保证多轮循环依赖拿到的是同一个对象。

创建流程在 AbstractAutowireCapableBeanFactory#doCreateBean。关键分支:如果当前 Bean 允许循环依赖(isSingletonCurrentlyInCreation 相关判断),在属性填充之前调 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))。注意传的是 lambda,不是 bean 本身——这就是"延迟"的含义:真正调用 getObject() 时才决定返回原对象还是代理。

getEarlyBeanReference 里会走 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference,AOP 的 AbstractAutoProxyCreator 实现它,所以需要代理的 Bean 在这里就生成代理,而不是等初始化后。这解决了一个坑:如果提前曝光的是原始对象,后来 AOP 又生成代理,注入到别人手里的就是两个不同对象。

为什么非要三级,两级不行?如果只有二级,就得在 doCreateBean 一开始就把对象放进二级,但那时对象还没填充属性,更没走 BeanPostProcessor。三级缓存存的是工厂,把"要不要提前曝光、曝光成什么样"推迟到真正被依赖的那一刻。没有循环依赖的 Bean,这个工厂从头到尾不会被调用,AOP 代理也只在正常初始化后生成一次。

构造器注入为什么不行:对象还没 new 出来,addSingletonFactory 根本没机会执行,getSingleton 三级全空,只能抛异常。这是设计上的硬边界,不是 bug。

验证方法

三个自测点。一,把 AService 改成构造器注入,确认启动报 BeanCurrentlyInCreationException,说明你理解了边界。二,在 getEarlyBeanReference 打断点,观察只有存在循环依赖时才命中。三,给 AService 加 @Async 或切面,检查 BService 里拿到的 A 和容器最终 getBean 拿到的 A 是不是同一个引用——== 比较,true 才算过。这一条最容易翻车,代理生成时机不对就会出现两个对象。

面试速答

核心收获:三级缓存的本质是把"曝光什么"推迟到"被依赖时"。下一步,把 AService 加上 @Async 再跑一遍引用比较,看代理时机怎么影响结果。

本文关键词:三级缓存、循环依赖、ObjectFactory、提前曝光、BeanCurrentlyInCreationException