2004年,Rod Johnson在《Expert One-on-One J2EE Development without EJB》一书中首次提出了Spring框架的核心设计理念。其中,依赖注入(Dependency Injection)作为实现控制反转(IoC)的主要手段,彻底改变了Java企业级开发的范式。然而,依赖注入的便利性也带来了一个棘手的问题:当两个或多个Bean相互依赖时,容器该如何处理?
这个问题被称为循环依赖(Circular Dependency)。看似简单,却触及了对象创建与依赖管理的本质矛盾。Spring给出的答案是一套精巧的三级缓存机制——这套机制的设计哲学远比表面看起来更加深邃,它不仅关乎循环依赖的解决,更涉及AOP代理、对象生命周期管理、线程安全等多个维度的权衡。
循环依赖:一个先有鸡还是先有蛋的问题
循环依赖的本质是一个"先有鸡还是先有蛋"的悖论。假设有两个单例Bean A和B:
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
当Spring容器启动时,需要创建这两个Bean。如果按照正常的创建流程:
- 创建ServiceA → 发现需要ServiceB → 创建ServiceB
- 创建ServiceB → 发现需要ServiceA → 但ServiceA还没创建完成…
这就是死锁的典型场景。在构造器注入的情况下,这种死锁是无法打破的,因为构造函数执行时对象还未创建,无法提前暴露引用。
但Spring选择支持Setter注入和字段注入的循环依赖,核心思路是:在对象实例化完成后、属性注入完成前,就将该对象的引用提前暴露出去。这样,当另一个Bean需要引用它时,即使它还未完全初始化,也能获取到同一个对象引用。
一级缓存的困境:线程安全与性能的两难
最朴素的想法是使用一个缓存来存储所有Bean。但这里有一个关键问题:什么时候将Bean放入缓存?
如果等Bean完全初始化后再放入,循环依赖无法解决(因为另一个Bean在初始化时找不到它)。如果在实例化后立即放入,又会有新的问题:缓存中同时存在完整Bean和半成品Bean,其他线程可能获取到不完整的对象。
解决方案只能是加锁。但全局锁意味着每次获取Bean都需要竞争锁,在容器启动后的大量getBean()调用中,性能会严重下降。这是一个典型的"正确性与性能不可兼得"的困境。
二级缓存:分离完整对象与早期对象
Spring的解决方案是引入两级缓存:
- 一级缓存(singletonObjects):存储完全初始化好的单例Bean
- 二级缓存(earlySingletonObjects):存储实例化完成但未完成属性注入的早期对象
// DefaultSingletonBeanRegistry中的核心数据结构
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
获取Bean时,先从一级缓存找,找不到再从二级缓存找。由于一级缓存中只有完整对象,正常情况下不需要加锁就能安全获取。
这套方案看似完美,直到AOP登场。
当循环依赖遇上AOP:代理对象的创建时机
Spring AOP通过动态代理(JDK动态代理或CGLIB)为目标对象创建代理。按照正常的设计,代理对象应该在Bean初始化完成后创建,由BeanPostProcessor的postProcessAfterInitialization()方法负责。
现在考虑这个场景:ServiceA需要被AOP代理(比如加了@Transactional注解),同时ServiceA和ServiceB相互依赖。
如果使用二级缓存:
- 实例化ServiceA → 放入二级缓存(此时是原始对象)
- 属性注入ServiceB → 创建ServiceB
- ServiceB需要注入ServiceA → 从二级缓存获取ServiceA(原始对象)
- ServiceB创建完成 → 回到ServiceA的初始化
- 初始化ServiceA → 创建代理对象 → 放入一级缓存
问题出现了:ServiceB持有的是ServiceA的原始对象引用,而容器中存储的是代理对象。这两个不是同一个对象!
这会导致严重的问题:事务注解、日志切面等功能全部失效,因为ServiceB调用的是原始对象而非代理对象。
三级缓存:延迟代理创建的优雅解法
Spring的解决方案是引入第三级缓存:
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
三级缓存存储的不是对象本身,而是一个ObjectFactory——一个函数式接口,其getObject()方法可以在需要时才执行代理创建逻辑。
// 在doCreateBean方法中,实例化后立即注册到三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
getEarlyBeanReference()方法会调用所有SmartInstantiationAwareBeanPostProcessor的实现,其中AbstractAutoProxyCreator(AOP代理创建的核心类)会在此处判断是否需要创建代理对象。
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;
}
三级缓存的精妙之处在于延迟执行:只有在真正发生循环依赖时,才会调用ObjectFactory.getObject()创建代理对象。如果没有循环依赖,这个工厂函数永远不会被执行,代理对象会在正常的初始化后阶段创建。
这解释了为什么不能只用二级缓存:如果提前在实例化阶段就创建代理对象并放入二级缓存,那么所有Bean(无论是否有循环依赖)都会在实例化阶段就创建代理,这违背了Spring的设计初衷——代理创建应该在Bean完全初始化后进行。
完整的循环依赖解决流程
让我们用一个具体例子来梳理完整流程。ServiceA和ServiceB相互依赖,且ServiceA需要被AOP代理:
sequenceDiagram
participant Container as Spring容器
participant Cache as 三级缓存系统
participant A as ServiceA
participant B as ServiceB
Container->>Cache: 创建ServiceA
Cache->>A: 实例化ServiceA(原始对象)
Cache->>Cache: 注册ObjectFactory到三级缓存
Note over Cache: singletonFactories.put("serviceA", factory)
Container->>Cache: 属性注入ServiceB
Container->>Cache: 创建ServiceB
Cache->>B: 实例化ServiceB
Cache->>Cache: 注册ObjectFactory到三级缓存
Container->>Cache: ServiceB需要注入ServiceA
Cache->>Cache: getSingleton("serviceA")
Note over Cache: 一级缓存未命中
Note over Cache: 二级缓存未命中
Note over Cache: 三级缓存命中ObjectFactory
Cache->>Cache: 调用getObject()创建代理
Note over Cache: 移动到二级缓存
Cache-->>B: 返回ServiceA的代理对象
Cache->>Cache: ServiceB创建完成
Cache->>Cache: ServiceA继续初始化
Note over Cache: 从二级缓存获取已创建的代理
Cache->>Cache: 放入一级缓存
核心代码在DefaultSingletonBeanRegistry.getSingleton()方法中:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 一级缓存没有,从二级缓存获取
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) {
// 3. 从三级缓存获取ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 执行工厂方法,可能创建代理对象
singletonObject = singletonFactory.getObject();
// 移入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存移除
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
Spring无法解决的循环依赖场景
三级缓存机制很强大,但并非万能。以下场景Spring无法自动解决循环依赖:
构造器注入
构造器注入要求在对象创建时就传入所有依赖。当两个Bean都使用构造器注入相互依赖时:
@Component
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
创建ServiceA时,构造函数需要ServiceB,但ServiceB还未创建;创建ServiceB时,构造函数又需要ServiceA——这是一个无法打破的死循环。对象在构造函数执行完成前根本不存在,自然无法提前暴露引用。
解决方案是使用@Lazy注解:
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
@Lazy会告诉Spring注入一个代理对象,只有在真正调用方法时才会创建实际对象。
原型作用域
三级缓存机制只适用于单例(Singleton)作用域的Bean。对于原型(Prototype)作用域的Bean,Spring不缓存实例,每次请求都会创建新对象,循环依赖自然无法解决。
@Async注解
这是一个容易被忽视的陷阱。@Async注解使用AsyncAnnotationBeanPostProcessor创建代理,但这个后置处理器没有实现SmartInstantiationAwareBeanPostProcessor接口。
这意味着当发生循环依赖时,getEarlyBeanReference()方法不会被调用,@Async的代理对象无法在早期暴露阶段创建。最终结果是:早期暴露的对象和最终创建的对象不一致,Spring会抛出BeanCurrentlyInCreationException。
解决方案包括:
- 在循环依赖的字段上添加
@Lazy注解 - 将异步方法提取到独立的Bean中,避免循环依赖
Spring Boot 2.6:默认禁用循环依赖
2021年12月发布的Spring Boot 2.6做出了一个重要决定:默认禁止循环引用。
Spring Boot 2.6的发布说明中明确写道:
Circular references between beans are now prohibited by default. If your application fails to start due to a
BeanCurrentlyInCreationExceptionyou are strongly encouraged to update your configuration to break the dependency cycle.
这一变化反映了Spring团队的态度:循环依赖往往是设计问题的信号。它通常意味着职责划分不清晰,违反了单一职责原则。
如果确实需要启用循环依赖支持,可以设置:
spring.main.allow-circular-references=true
但这只是一个过渡方案,最佳实践是重构代码结构,消除循环依赖。
设计启示:权衡的艺术
Spring的三级缓存设计展示了软件工程中权衡的艺术:
为什么不用一级缓存? 需要全局锁,性能太差。
为什么不用二级缓存? 无法优雅处理AOP代理,会导致提前创建代理对象。
为什么三级缓存可以? 延迟执行的工厂模式,只在真正需要时才创建代理,既解决了循环依赖,又保持了代理创建的正常时机。
这套机制的本质是:用空间换时间,用延迟换简单。三级缓存看似复杂,实则是多种约束条件下最优的工程解法。
参考文献:
- Spring Framework Documentation - The IoC Container: https://docs.spring.io/spring-framework/reference/core/beans/
- Spring Boot 2.6 Release Notes: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
- Baeldung - Circular Dependencies in Spring: https://www.baeldung.com/circular-dependencies-in-spring
- Spring源码分析系列-循环依赖和三级缓存: https://zhuanlan.zhihu.com/p/375308988
- DefaultSingletonBeanRegistry Source Code: https://docs.spring.io/spring-framework/docs/3.2.0.M2_to_3.2.0.RC1/Spring%20Framework%203.2.0.RC1/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.html