上周调一个循环依赖的 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 步:
getBean("aService")→ 实例化 A,此时aService的bService字段还是null- 执行
addSingletonFactory,把 A 的工厂放进三级缓存 - 给 A 填充属性 → 发现需要 B →
getBean("bService") - 实例化 B,填充属性 → 发现需要 A →
getSingleton("aService", true) - 一二三级缓存依次查找,命中三级缓存的工厂 → 执行它拿到 A 的早期引用 → 存入二级缓存、清掉三级
- B 拿到 A 的引用,初始化完成,进入一级缓存
- 回到 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 说明提前代理过了 → 直接返回,不再代理一次。代理对象的唯一性就是这么保住的。
一句话记忆:一级存成品,二级存早期引用,三级存"怎么造早期引用"的方法。
什么情况下三级缓存也救不了
回到开头那个构造器注入的报错。三个已知边界:
- 构造器注入的循环依赖 —— 实例化都完成不了,根本没机会调用
addSingletonFactory提前暴露 prototype作用域 —— 压根不进单例缓存,三级缓存对它无效- 手动关掉开关 ——
allowCircularReferences = false,或者 Spring Boot 2.6 之后默认禁用了循环依赖
顺带一提 @Lazy 为什么能解决:它不是打破了循环,而是把其中一个 Bean 换成了懒加载代理,让依赖在真正调用时才去解析,绕开了启动期的创建时序。治标,但确实有效。
我还没搞定的地方
诚实记录一下,避免误导:
earlySingletonObjects为什么用ConcurrentHashMap而singletonFactories用普通HashMap—— 我的猜测是工厂只在同步块内访问,但这只是猜测,没验证- 三级缓存在有父容器时的行为我还没跟,
getSingleton会先查父容器,这部分调用链我没走完
TODO:这两点搞清楚了回来补。
如果你也在啃 Spring 源码,或者发现我上面哪段理解错了,评论区告诉我——指出我写错的地方,比点赞有价值得多。
下一篇打算跟 AbstractAutowireCapableBeanFactory#doCreateBean 的完整流程,从 createBeanInstance 一路到 initializeBean。

-
最新
-
最热
只看作者