Skip to content
Go back

Spring循环依赖——三级缓存的精妙设计

Spring 循环依赖:三级缓存的精妙设计

一句话结论(30s)

Spring 用三级缓存解决「两个单例互相引用」的循环依赖,本质是先暴露半成品打破创建循环,再延迟决策保证代理唯一性。一级 singletonObjects 存成品、二级 earlySingletonObjects 存半成品打破循环,三级 singletonFactoriesObjectFactory 把「是否需要 AOP 代理」的决策推迟到第一次被引用时,再用 earlyProxyReferences 标记保证 after 阶段不重复代理。但它解决不了构造器注入的循环依赖,因为构造器调用发生在实例化阶段、还没有任何缓存暴露,只能靠 @Lazy 打破。

核心原理(2min)

主流程:A 实例化后(new A())不是直接放二级缓存,而是往三级缓存 singletonFactories 放一个 ObjectFactory(内部是 getEarlyBeanReference)。当 B 创建时需要 A,触发 getBean(A),从三级缓存取出并执行 ObjectFactory.getObject(),此时按需决定:A 需要 AOP 就提前生成早期代理、不需要就返回原始对象,结果存入二级缓存 earlySingletonObjects;B 拿到 A 的引用完成创建,A 再注入已完成的 B,最终 A 完整后放入一级缓存 singletonObjects。关键机制在于:代理本来在 postProcessAfterInitialization 阶段生成,但那时 B 可能早已拿走原始 A,导致一级缓存里是代理、B 手里是原始对象、单例语义被破坏;三级缓存的延迟决策配合 earlyProxyReferences 标记,让一级缓存与 B 持有的引用是同一个对象。

底层深入(5-10min)

什么是循环依赖

@Component
class A {
    @Autowired B b;
}

@Component
class B {
    @Autowired A a;
}

A 创建时需要 B → B 创建时需要 A → 互相等 → 死循环。如果没有 Spring 的三级缓存,容器启动会 StackOverflow

三级缓存的数据结构

DefaultSingletonBeanRegistry 中三级缓存就是三个 Map(Spring 源码逐字):

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Creation-time registry of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

三个 Map 分工明确:

思考一下:为什么非要三级,两级不行吗? 如果只留「成品 + 半成品」两级,那「要不要给 A 生成代理」就必须在 A 实例化后、暴露半成品的那一刻拍板。但此刻 A 还没走完初始化,我们根本不知道它最终会不会被 AOP 代理——提前生成可能白生成,不生成又怕 B 拿走原始对象导致代理不唯一。三级缓存里放的不是 Bean 而是 ObjectFactory,等于把「决策」也延迟了:等真有人来取 A 的引用时,再现场决定返回原始对象还是早期代理。所以三级的分工其实是——前两级解决「能不能拿到引用」,第三级解决「拿到的是不是正确的引用」

getSingleton:查三级缓存

getBean("A") 最终会走到这里,按「一级 → 二级 → 三级」顺序查找(源码逐字):

protected @Nullable Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock.
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            if (!this.singletonLock.tryLock()) {
                // Avoid early singleton inference outside of original creation thread.
                return null;
            }
            try {
                // Consistent creation of early reference within full singleton lock.
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            // Singleton could have been added or removed in the meantime.
                            if (this.singletonFactories.remove(beanName) != null) {
                                this.earlySingletonObjects.put(beanName, singletonObject);
                            }
                            else {
                                singletonObject = this.singletonObjects.get(beanName);
                            }
                        }
                    }
                }
            }
            finally {
                this.singletonLock.unlock();
            }
        }
    }
    return singletonObject;
}

关键点:只有在「Bean 正在创建中」(isSingletonCurrentlyInCreation)时才会查二级、三级缓存,避免误拿别的未完成对象;一旦从三级取出早期引用,就立刻把工厂从三级移除、把结果升级进二级,保证同一个早期引用只生成一次。而 singletonFactory.getObject() 这一步正是触发 getEarlyBeanReference 的地方,也就是「延迟决策」的落点。

思考一下:为什么取到早期引用后要「移除三级 + 升级二级」? 如果工厂一直留在三级缓存,每次 getBean(A) 都会重新执行 ObjectFactory.getObject(),可能产出多个不同的早期引用——B 拿一个、C 又拿一个新对象,单例就名存实亡。把它升级进二级并删掉工厂,本质是一个**「懒加载 + 一次性缓存」**:第一次被引用时才真正产出早期对象,之后所有人都从二级缓存拿同一个引用。

addSingletonFactory:提前暴露半成品

Bean 实例化完成后、属性注入之前,容器把工厂塞进三级缓存(源码逐字):

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    this.singletonFactories.put(beanName, singletonFactory);
    this.earlySingletonObjects.remove(beanName);
    this.registeredSingletons.add(beanName);
}

注意放进三级缓存的是 ObjectFactory 而非 Bean 本身——此刻 Bean 还没走 populateBean 注入属性、更没走初始化,只是先把「能产出它引用」的工厂挂出去,别的 Bean 就能随时拿到这个半成品的引用来打破循环。

doCreateBean:提前暴露发生在哪一步

真正调用 addSingletonFactory 的地方在 AbstractAutowireCapableBeanFactory.doCreateBean(源码逐字):

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

三个条件必须同时成立——「单例 + 允许循环引用 + 当前 Bean 正在创建中」——才会提前暴露,且暴露发生在 populateBean(属性注入)之前。这就是字段/Setter 注入能解循环依赖的根本原因:实例化(new)和属性注入是两步,注入前引用已经挂出去了。

思考一下:这三个条件缺一个会怎样? 去掉「单例」——prototype 本来就不进缓存,提前暴露没有意义;去掉「允许循环引用」——Spring 2.6+ 默认就是关闭的,直接抛异常逼你重构;去掉「正在创建中」——那「提前引用」的判断就失去了前提。这三个条件其实在回答同一个问题:这个 Bean 此刻处于「能被别人提前借用」的状态吗?

为什么需要三级缓存?

如果 A 需要被 AOP 代理(如 @Transactional 注解在 A 上),B 拿到的应该是 A 的代理对象而不是原始对象。

问题:代理是在 Bean 初始化完成后的 postProcessAfterInitialization 阶段生成的——而此时 B 可能在 A 初始化完成之前就拿了二级缓存中的原始 A 对象。

后果:最终一级缓存中的 A 是代理对象,但 B 持有的是原始 A 对象——同一个 Bean 有两个不同引用的单例,单例语义被破坏。 三级缓存就是用来解决这个「代理唯一性」问题的。

getEarlyBeanReference:延迟决定是否代理

三级缓存工厂里真正执行的逻辑是 getEarlyBeanReference(源码逐字):

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

它本身不做代理判断,而是把「要不要提前生成代理」交给 SmartInstantiationAwareBeanPostProcessor(典型实现是 AOP 的 AbstractAutoProxyCreator)。如果 Bean 需要 AOP,它会在这里提前 wrap 出早期代理并记录到 earlyProxyReferencespostProcessAfterInitialization 阶段检测到该标记就跳过、不再重复代理;否则直接返回原始对象。结果被升级进二级缓存后,所有人都拿同一个引用,一级缓存与 B 手里持的是同一个对象,单例语义得以保全。

为什么构造器注入解决不了

构造器注入:A 的构造器需要 B,B 的构造器需要 A。构造器调用发生在实例化阶段createBeanInstance),而提前暴露发生在实例化之后——也就是说连对象都还没 new 出来,三级缓存里没有任何东西可查,双方都无法提前拿到对方引用,只能互相死等。

A 实例化: new A(b) → 需要 B
B 实例化: new B(a) → 需要 A
→ 谁都无法被 new 出来 → 死循环 → BeanCurrentlyInCreationException

对比字段注入:实例化与注入是两步,实例化后能先暴露引用再注入;构造器注入的依赖在 new 的那一刻就必须就绪,没有「先挂出去」的机会,所以三级缓存无能为力。

解法:@Lazy 延迟注入——Bean 属性注入一个代理占位符,首次调用时才去获取真正的 Bean,打破构造器阶段的循环。

思考一下:@Lazy 为什么能解构造器循环,它到底做了什么? 它并没有真正「解决」循环,而是把「立刻要」改成了「将来再要」——注入的是一个代理占位符,构造器只拿到空壳引用就顺利 new 出来了,真正的 Bean 等首次调用时才去容器取。所以它绕开了「构造器阶段就必须就绪」的死结,代价是真正的循环问题被推迟到运行期才暴露。

总结

缓存存储内容作用
一级 singletonObjects完整就绪的 Bean最终结果
二级 earlySingletonObjects早期引用(原始或代理)打破循环
三级 singletonFactoriesObjectFactory(延迟判断)保证代理唯一性

Spring 2.6+ 默认禁止循环依赖,需要在配置中显式开启。官方推荐重构设计而非依赖三级缓存——循环依赖通常是设计问题,不是实现问题。

章末提问

1. 为什么非要三级缓存,两级不行吗?

结论先行:两级能解循环依赖,但解不了「AOP 代理唯一性」。

因为:二级缓存里存的是实例化后、初始化前的「半成品」,此时 AOP 代理还没生成(代理在 postProcessAfterInitialization 阶段才 wrap)。如果 B 在这个窗口期拿走了原始 A,等 A 初始化完生成代理放回一级缓存,就会出现「一级是代理、B 手里是原始对象」两个引用的单例。第三级用 ObjectFactory 把「是否提前生成代理」的决策推迟到第一次被引用那一刻,配合 earlyProxyReferences 标记,保证所有人拿到同一个对象。

2. 构造器注入的循环依赖为什么三级缓存救不了?

结论先行:因为构造器调用发生在实例化阶段,早于任何缓存暴露。

因为:三级缓存暴露半成品的时机是 new 之后、populateBean 之前,而构造器注入的依赖在 new A(b) 那一刻就必须就绪——连对象都没 new 出来,缓存里自然空无一物,双方互相死等,最终抛 BeanCurrentlyInCreationException

3. earlyProxyReferences 到底防的是什么?

结论先行:防的是 AOP 代理被生成两次。

因为:提前暴露时如果 A 需要代理,getEarlyBeanReference 会提前 wrap 出早期代理并记录到 earlyProxyReferences;如果 after 阶段不检查这个标记,postProcessAfterInitialization 会再 wrap 一次,一级缓存里就放进了和 B 手里不一样的代理,单例语义再次被破坏。标记就是「这个人我已经提前代理过了」的备忘。

4. 为什么 Spring 2.6+ 反而默认禁止循环依赖?

结论先行:因为循环依赖绝大多数是设计问题,而不是实现问题。

因为:三级缓存只是「兜底机制」,它让坏设计也能跑起来,但代价是复杂度极高——三级缓存、早期引用、代理提前生成都是为它服务的。官方更希望你在设计阶段就理清依赖方向(如拆类、抽接口、用 @Lazy),而不是让框架默默替你擦屁股。

5. ObjectFactory 在这里为什么比直接存 Bean 更好?

结论先行:因为它把「要不要代理」从「创建时决定」变成了「使用时决定」。

因为:直接存 Bean,就得在 addSingletonFactory 的那一刻(实例化刚完成、尚未初始化)就确定存原始对象还是代理对象,而此刻 AOP 决策所需的信息还不完整;包一层 ObjectFactory 后,真正调用 getObject() 时才执行 getEarlyBeanReference,决策被推迟到「第一次被引用」这个信息最充分的时刻。


Share this post on:

Previous Post
Spring Boot自动装配——@SpringBootApplication背后的秘密
Next Post
Spring IoC容器——BeanDefinition到Bean实例的完整旅程