Spring 三级缓存

Spring 三级缓存

Spring 三级缓存

上周调一个循环依赖的 bug,同事甩给我一句"加个 @Lazy 就好了"。

问题确实解决了,但我说不清为什么好——更准确地说,我说不清 Spring 到底是怎么让两个互相依赖的 Bean 都活下来的。于是我把 getBean() 的调用链从头跟了一遍,断点打了十几个。

这篇文章是这次的笔记。不是教程,是我自己跟源码的过程记录。

先说结论,它跟大多数人想的不一样

三级缓存不是为了"解决"循环依赖,而是为了在解决循环依赖的同时,不破坏 AOP 代理对象的唯一性。

如果单纯为了打破循环,二级缓存就够了。第三级存在的意义,是让"要不要提前创建代理"这件事延迟到真正需要时才决定。

下面所有源码基于 Spring 5.3.x,类都是 org.springframework.beans.factory 包下的。

先复现一下问题

最小可复现代码,两个 Service 互相注入:

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

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

启动不报错。但把 @Autowired 换成构造器注入:

@Service
public class AService {
    private final BService bService;
    public AService(BService bService) { this.bService = bService; }
}

立刻抛 BeanCurrentlyInCreationException。这个差异本身就是一条线索——它说明 Spring 的能力是有边界的,不是什么循环都能解。

三个缓存的物理位置

都在 DefaultSingletonBeanRegistry 里,就是三个 Map:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry
        implements SingletonBeanRegistry {

    /** 一级缓存:已完成初始化的成品单例 */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /** 三级缓存:能生产早期引用的工厂 */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    /** 二级缓存:提前暴露的早期引用(半成品) */
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}

注意声明顺序:源码里二级和三级的声明顺序是反的,看变量名别看行号。

A 依赖 B、B 依赖 A 时的调用链

我把断点跟出来的顺序整理成 7 步:

  1. getBean("aService") → 实例化 A,此时 aService 的 bService 字段还是 null
  2. 执行 addSingletonFactory,把 A 的工厂放进三级缓存
  3. 给 A 填充属性 → 发现需要 B → getBean("bService")
  4. 实例化 B,填充属性 → 发现需要 A → getSingleton("aService", true)
  5. 一二三级缓存依次查找,命中三级缓存的工厂 → 执行它拿到 A 的早期引用 → 存入二级缓存、清掉三级
  6. B 拿到 A 的引用,初始化完成,进入一级缓存
  7. 回到 A,A 拿到完整的 B,初始化完成

第 5 步的查找逻辑就是这段:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);      // 查一级
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName); // 查二级
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {                        // 查三级
                    singletonObject = singletonFactory.getObject();    // 执行工厂
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

关键在 allowEarlyReference 这个开关:只有允许早期引用时才会去碰三级缓存。

工厂里到底做了什么

第 5 步执行的那个工厂,是在 AbstractAutowireCapableBeanFactory#doCreateBean 里塞进去的:

boolean earlySingletonExposure = (mbd.isSingleton()
        && this.allowCircularReferences
        && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

它调的是 getEarlyBeanReference:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorFactories()) {
            // 如果需要 AOP 代理,这里会提前返回代理对象
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

这就是第三级的全部意义:延迟决策。

没有循环依赖 → 工厂永远不执行 → AOP 代理照常在初始化后由 postProcessAfterInitialization 创建。
发生循环依赖 → 工厂被执行 → 提前创建代理。

两条路径互斥,不会重复代理。

为什么两级真的不行

这是我最想搞清楚的一节。假设砍掉第三级,只剩二级缓存,那么 Bean 在实例化后必须立刻决定往二级缓存里放什么:

  • 放原始对象 → B 拿到的是原始 A,但 A 最终会被代理 → B 持有的引用和容器里的 A 不是同一个对象,事务、缓存注解全部失效
  • 放代理对象 → 每个 Bean 都要在实例化后立刻走一遍代理判断,哪怕它根本没被循环依赖;而且此时 Bean 的属性还没填充,代理基于的是一个不完整的状态

三级缓存的 ObjectFactory 把"要不要代理"这个问题推迟到了真正被需要的那一刻,同时用 earlyProxyReferences 这个 Map 记账:

// AbstractAutoProxyCreator
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);   // 记一笔:这个已经提前代理过了
    return wrapIfNecessary(bean, beanName, cacheKey);
}

public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);  // 没提前代理过,才在这里代理
        }
    }
    return bean;
}

remove() 返回 bean 说明提前代理过了 → 直接返回,不再代理一次。代理对象的唯一性就是这么保住的。

一句话记忆:一级存成品,二级存早期引用,三级存"怎么造早期引用"的方法。

什么情况下三级缓存也救不了

回到开头那个构造器注入的报错。三个已知边界:

  1. 构造器注入的循环依赖 —— 实例化都完成不了,根本没机会调用 addSingletonFactory 提前暴露
  2. prototype 作用域 —— 压根不进单例缓存,三级缓存对它无效
  3. 手动关掉开关 —— allowCircularReferences = false,或者 Spring Boot 2.6 之后默认禁用了循环依赖

顺带一提 @Lazy 为什么能解决:它不是打破了循环,而是把其中一个 Bean 换成了懒加载代理,让依赖在真正调用时才去解析,绕开了启动期的创建时序。治标,但确实有效。

我还没搞定的地方

诚实记录一下,避免误导:

  • earlySingletonObjects 为什么用 ConcurrentHashMap 而 singletonFactories 用普通 HashMap —— 我的猜测是工厂只在同步块内访问,但这只是猜测,没验证
  • 三级缓存在有父容器时的行为我还没跟,getSingleton 会先查父容器,这部分调用链我没走完

TODO:这两点搞清楚了回来补。


如果你也在啃 Spring 源码,或者发现我上面哪段理解错了,评论区告诉我——指出我写错的地方,比点赞有价值得多。

下一篇打算跟 AbstractAutowireCapableBeanFactory#doCreateBean 的完整流程,从 createBeanInstance 一路到 initializeBean。

©版权声明
THE END
喜欢就支持一下吧
点赞13 分享
admin的头像-OsAtNb
评论 共2条

请登录后发表评论