在Java开发中,Spring框架的依赖注入机制极大地简化了对象间的协作关系。但当我们遇到两个或多个Bean相互引用时,就形成了所谓的"循环依赖"困境。想象一下两个紧密合作的同事,彼此都需要对方先完成工作才能开始自己的任务——这就是循环依赖在代码世界的真实写照。
Spring通过独创的三级缓存架构优雅地解决了这个难题。这套机制的精妙之处在于:它允许Bean在尚未完全"成熟"(初始化完成)时,就能以"半成品"状态被其他Bean引用。这就好比建筑工地上的预制件,虽然还未最终组装完成,但已经具备基本形态可供其他工序使用。
Spring使用三个不同层级的缓存来管理Bean生命周期:
java复制// 一级缓存:成品仓库
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:半成品展示区
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:零件加工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
这种分层设计体现了"空间换时间"的思想。一级缓存相当于成品仓库,存放完全初始化好的Bean;二级缓存是半成品展示区,存放已实例化但未初始化的Bean;三级缓存则是零件加工厂,保存着能生产Bean早期引用的工厂对象。
让我们通过A→B→A的典型循环依赖场景,看看Spring如何施展魔法:
创建A的实例
java复制// 1. 调用构造函数创建A的原始对象
A aInstance = createBeanInstance("a");
// 2. 将A的工厂放入三级缓存(关键步骤!)
addSingletonFactory("a", () -> getEarlyBeanReference("a", aInstance));
填充A的属性时发现需要B
java复制// 3. 开始解析依赖项B
B bDependency = getBean("b");
创建B的实例
java复制// 4. 构造B的原始对象
B bInstance = createBeanInstance("b");
// 5. 同样将B的工厂放入三级缓存
addSingletonFactory("b", () -> getEarlyBeanReference("b", bInstance));
填充B的属性时又需要A
java复制// 6. 再次尝试获取A
A aReference = getBean("a");
// 7. 从三级缓存获取A的工厂并创建早期引用
ObjectFactory<?> aFactory = singletonFactories.get("a");
A earlyA = aFactory.getObject();
// 8. 将早期A升级到二级缓存
earlySingletonObjects.put("a", earlyA);
singletonFactories.remove("a");
关键理解点:此时A对象虽然还未完成初始化,但已经可以作为"早期引用"被B使用。这种"未完成但可用"的状态是打破循环依赖的关键。
java复制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;
}
这段代码展示了Spring如何通过三级缓存的协同工作来解决循环依赖。值得注意的是其中的同步控制块——这是保证线程安全的关键设计。
很多开发者会有疑问:既然二级缓存已经能存储早期引用,为什么还需要三级缓存?这主要与Spring AOP的代理机制有关:
代理对象的延迟创建:如果Bean需要被代理(如通过AOP),代理对象的创建时机很关键。三级缓存保存的是ObjectFactory,可以延迟决定是否需要创建代理。
一致性保证:通过工厂模式,确保无论有多少个依赖方,获取到的都是同一个早期引用对象。
java复制protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// 如果有必要,在这里创建代理对象
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp =
(SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
构造器注入循环
java复制@Component
public class ServiceA {
private final ServiceB b;
public ServiceA(ServiceB b) { // 构造器注入
this.b = b;
}
}
这种情况下,Spring无法在构造ServiceA之前获得ServiceB的实例,导致失败。
原型(prototype)作用域的Bean
java复制@Scope("prototype")
@Component
public class PrototypeBean { ... }
Spring不会缓存原型Bean,因此无法提供早期引用。
@Async注解的方法
java复制@Async
public void asyncMethod() { ... }
异步方法会导致代理创建时机的问题,可能破坏循环依赖解决机制。
java复制@Component
public class ServiceA {
private final ServiceB b;
public ServiceA(@Lazy ServiceB b) {
this.b = b; // 实际使用时才会初始化
}
}
java复制@Component
public class ServiceA {
@Autowired
private ServiceB b; // 字段注入
@Autowired
public void setB(ServiceB b) { // setter注入
this.b = b;
}
}
java复制public interface IServiceB {
void commonMethod();
}
@Component
public class ServiceA {
@Autowired
private IServiceB b; // 依赖抽象
}
java复制@Component
public class ServiceA implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
}
public void businessMethod() {
ServiceB b = context.getBean(ServiceB.class);
// 使用b
}
}
建议在开发阶段启用Spring的循环依赖检测:
properties复制# application.properties
spring.main.allow-circular-references=false
这样可以在启动时就发现潜在的循环依赖问题,而不是等到运行时才暴露。
三级缓存机制虽然强大,但也带来一定的性能开销:
对于性能敏感的应用,可以考虑:
@Lazy减少初始化开销@DependsOn明确初始化顺序从长期维护的角度,循环依赖往往是设计上的"代码异味"。建议:
| 异常现象 | 可能原因 | 解决方案 |
|---|---|---|
| BeanCurrentlyInCreationException | 构造器循环依赖 | 改用setter注入或@Lazy |
| NoSuchBeanDefinitionException | 原型Bean循环依赖 | 重新设计避免循环 |
| NullPointerException | @Async导致代理问题 | 调整代理创建顺序 |
启用Spring调试日志:
properties复制logging.level.org.springframework.beans=DEBUG
使用BeanPostProcessor打印生命周期:
java复制@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("Before init: " + beanName);
return bean;
}
}
断点位置建议:
当循环依赖涉及AOP代理时,Spring的处理流程会更加复杂:
事务注解也会创建代理,需要注意:
如果使用@Inject而非@Autowired:
在WebFlux等响应式编程模型中:
当将应用编译为原生镜像时:
可以考虑以下模式替代循环依赖:
在实际项目中,理解Spring解决循环依赖的机制不仅有助于调试复杂问题,更能引导我们思考更优雅的架构设计。三级缓存方案展现了框架设计者面对复杂问题时的创造性思维,这种"半成品"暴露的思路在分布式系统设计等领域也有类似应用。