Spring 循环依赖与三级缓存
循环依赖是面试高频题,也是线上启动失败的常见原因。很多人能背出"三级缓存"三个字,却说不清为什么必须是三级而不是两级,也说不清为什么构造器注入的循环依赖解不了。本文从可复现的报错出发,沿着 DefaultSingletonBeanRegistry 的源码,把这件事一次讲透。适合已经能熟练使用 Spring、想理解容器内部机制的后端开发阅读。
一、问题复现
两个 Service 互相持有对方:
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
这段代码在默认配置下可以正常启动。把注入方式换成构造器:
@Service
public class AService {
private final BService bService;
public AService(BService bService) { this.bService = bService; }
}
启动报错:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| aService defined in file [.../AService.class]
↑ ↓
| bService defined in file [.../BService.class]
└─────┘
Spring Boot 2.6 之后默认禁止循环引用,字段注入也会报同样的错,需要显式开启:
spring:
main:
allow-circular-references: true
Spring Boot 3.x 沿用这一默认值。也就是说,三级缓存仍然存在,但默认不再被用来解业务 Bean 的循环依赖,这是很多人版本升级后启动失败的原因。
二、为什么构造器注入解不了
Bean 的创建顺序是:实例化 → 属性填充 → 初始化。字段注入发生在属性填充阶段,此时对象已经在堆上,可以先暴露引用再赋值;构造器注入要求依赖在实例化之前就准备好,形成了不可打破的先后约束。
createBeanInstance 调用构造器时,AService 需要 BService 实例,BService 的构造又需要 AService 实例,两者都没有完成实例化,容器无对象可暴露,只能抛 BeanCurrentlyInCreationException。这不是缓存层数的问题,而是时序上根本无解。
三、三级缓存的数据结构
DefaultSingletonBeanRegistry 中定义了三个 Map:
/** 一级:完整的单例 Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 三级:ObjectFactory,用于生成早期引用(可能是代理) */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** 二级:提前暴露的早期 Bean,尚未完成属性填充与初始化 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
此外还有两个辅助集合:
/** 正在创建中的 Bean 名称 */
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
/** 已经创建完成的 Bean 名称,用于判断是否需要提前暴露 */
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
getSingleton(beanName) 的查找顺序是严格的一级 → 二级 → 三级:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
if (factory != null) {
singletonObject = factory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
注意三级取到值后会升级到二级并删除三级,所以一个 Bean 的早期引用只会被生成一次。
四、A 与 B 的创建时序
以字段注入为例,完整过程如下:
getBean("aService")→doCreateBean→createBeanInstance反射创建 A 的原始对象。- 判断允许提前暴露(单例、正在创建、且非合成 Bean),执行
addSingletonFactory,把() -> getEarlyBeanReference("aService", mbd, a)放入三级缓存。 populateBean发现需要BService,调用getBean("bService")。- B 同样实例化后把工厂放入三级缓存,
populateBean时发现需要AService。 getSingleton("aService", true)在三级缓存命中工厂,调用getEarlyBeanReference得到 A 的早期引用,放入二级缓存,删除三级缓存。- B 拿到 A 的早期引用完成注入,走完初始化,放入一级缓存。
- A 继续完成
populateBean(此时拿到的是完整的 B)、初始化,放入一级缓存,清理二级缓存。
核心代码在 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;
}
五、为什么需要第三级
如果只有两级(一级存完整对象、二级存早期对象),在第 2 步就必须立刻决定"要不要为 A 生成代理",但此时 A 还没有完成属性填充,AbstractAutoProxyCreator 判断是否需要代理依赖目标对象的类型和注解,贸然生成可能得到错误结果。
三级缓存存的是 ObjectFactory,把"是否生成代理"这个动作延迟到真正被循环引用时才执行。没有被循环引用时,工厂永远不会被调用,A 只在初始化后由 AbstractAutoProxyCreator#postProcessAfterInitialization 生成一次代理。这是"按需"与"提前"的差别。
AbstractAutoProxyCreator 用一个 earlyProxyReferences 记录已经生成过早期代理的 Bean,避免初始化后重复代理:
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
@Override
public Object postProcessAfterInitialization(@Nullable 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;
}
六、能解与不能解的情形
| 场景 | 能否解决 | 原因 |
| --- | --- | --- |
| 单例 + 字段/setter 注入 | 能 | 实例化后即可暴露早期引用 |
| 单例 + 构造器注入 | 不能 | 实例化前就需要依赖 |
| 原型 Bean 互相依赖 | 不能 | 原型不缓存,也不做提前暴露 |
| @Async 代理的循环依赖 | 不能 | 需在初始化后由 AsyncAnnotationBeanPostProcessor 生成代理,早期引用拿不到 |
| @Transactional 循环依赖 | 有条件能 | AbstractAutoProxyCreator 支持 getEarlyBeanReference |
| @Scope("prototype") 参与循环 | 不能 | 同上,无缓存可复用 |
@Async 之所以失败,是因为它依赖的 AsyncAnnotationBeanPostProcessor 只实现了 BeanPostProcessor,没有实现 SmartInstantiationAwareBeanPostProcessor,无法提供早期引用。
七、正确写法
7.1 重构,消除环
绝大多数循环依赖是职责划分不清的信号。A 依赖 B、B 依赖 A,通常意味着存在第三个被两边共享的职责,抽出来即可:
@Service
public class OrderQueryService { // 从 OrderService 抽出的只读能力
private final OrderRepository repository;
public OrderQueryService(OrderRepository repository) { this.repository = repository; }
}
@Service
public class OrderService {
private final OrderQueryService queryService;
private final PayService payService; // 单向依赖
public OrderService(OrderQueryService queryService, PayService payService) {
this.queryService = queryService;
this.payService = payService;
}
}
7.2 用 @Lazy 打破时序
无法立刻重构时,在其中一个注入点加 @Lazy,容器会注入一个延迟解析的代理:
@Service
public class AService {
private final BService bService;
public AService(@Lazy BService bService) { this.bService = bService; }
}
@Lazy 注入的是 CGLIB 代理,首次调用方法时才真正 getBean。代价是每次调用多一层代理开销,且类型判断、instanceof 会失真。
7.3 用 ApplicationContext 延迟获取
@Service
public class AService implements ApplicationContextAware {
private ApplicationContext ctx;
public void doSomething() {
ctx.getBean(BService.class).handle();
}
@Override
public void setApplicationContext(ApplicationContext ctx) { this.ctx = ctx; }
}
这种方式把编译期依赖变成运行期查找,牺牲了可测试性,只适合兜底。
八、常见坑 / 最佳实践
- 升级 Spring Boot 2.6+ 后启动失败就打开
allow-circular-references。这会把设计问题掩盖成配置问题。正确做法是先看报错里打印的依赖环,评估能否抽类重构。 - 以为加了
@Lazy就万事大吉。@Lazy生成的是代理,如果目标类是final或方法final,CGLIB 无法代理,会退化为 JDK 代理并抛类型转换异常。 - 在
@PostConstruct中调用循环依赖方的方法。此时对方可能只是早期引用,属性尚未填充,容易出现NullPointerException。 @Async与循环依赖同时出现。这类组合无法靠三级缓存解决,应把异步方法抽到独立 Bean,让调用方单向依赖它。- 原型 Bean 上尝试循环依赖。容器不缓存原型,任何"提前暴露"都无效,必须在设计上断开。
九、小结
- 三级缓存分别是
singletonObjects、earlySingletonObjects、singletonFactories,查找顺序一级 → 二级 → 三级。 - 第三级存
ObjectFactory,目的是把代理生成延迟到真正被引用时,保证普通 Bean 只代理一次。 - 构造器注入、原型 Bean、
@Async代理的循环依赖无法靠三级缓存解决。 - Spring Boot 2.6 起默认禁止循环引用,遇到报错优先重构而非改配置。
- 循环依赖多数是职责划分问题的信号,
@Lazy只是缓解手段。
- 点赞
- 收藏
- 关注作者
评论(0)