🌱 引言
在Java开发中,Spring框架以其强大的依赖注入和控制反转功能深受开发者喜爱。然而,当我们的应用变得复杂时,Bean之间可能会出现相互依赖的情况,这就是所谓的"循环依赖"问题。😵 如果处理不当,循环依赖会导致应用启动失败,给开发者带来困扰。
幸运的是,Spring框架通过一种巧妙的机制——三级缓存,优雅地解决了大部分循环依赖问题。本文将深入探讨Spring是如何通过三级缓存机制处理循环依赖的,带你揭开这一技术奥秘的面纱。💡
阅读本文,你将收获:
•深入理解循环依赖的本质和危害
•掌握Spring三级缓存的工作原理
•通过源码分析了解Spring如何处理循环依赖
•学习不同场景下循环依赖的解决方案
•获取实际开发中的最佳实践建议
让我们开始这段探索Spring内部机制的奇妙旅程吧!🚀
🔄 什么是循环依赖?
循环依赖的基本概念
循环依赖是指两个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了一个环形调用。简单来说,就是A依赖B,B又依赖A,形成了一个闭环。🔄
如上图所示,Class A依赖Class B,同时Class B又依赖Class A,这就形成了最简单的循环依赖。在代码中,这种情况通常表现为:
@Component public class A { @Autowired private B b; }
@Component public class B { @Autowired private A a; }
为什么会出现循环依赖?
循环依赖通常与实际业务逻辑密切相关。在某些场景下,两个服务或组件确实需要相互协作,例如:
•用户服务需要调用订单服务获取用户的订单信息,而订单服务需要调用用户服务获取下单用户的详细信息
•父子关系的实体类,父实体需要知道所有子实体,子实体需要引用父实体
虽然从设计角度看,我们应该尽量避免循环依赖,但在某些业务场景下,循环依赖可能是不可避免的。😕
循环依赖的危害
如果没有适当的机制处理循环依赖,可能会导致以下问题:
•启动失败:应用无法完成初始化过程
•死锁:相互等待对方初始化完成,导致永远无法完成初始化
•内存溢出:在递归创建过程中可能导致栈溢出
幸运的是,Spring框架为我们提供了解决循环依赖的机制,让我们来看看Spring是如何应对这一挑战的。👇
🧩 Spring三级缓存机制概述
什么是三级缓存?
Spring通过三级缓存的设计巧妙地解决了单例模式下的属性循环依赖问题。这三级缓存分别是:
1.一级缓存(singletonObjects):存放完全初始化好的Bean,即所谓的"成品"Bean
2.二级缓存(earlySingletonObjects):存放原始的Bean对象(尚未填充属性),即"半成品"Bean
3.三级缓存(singletonFactories):存放Bean工厂对象,用于生成原始Bean对象或其代理对象
这三级缓存的定义可以在Spring源码的DefaultSingletonBeanRegistry类中找到:
/** 一级缓存:存放完全初始化好的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:存放原始的Bean对象(尚未填充属性) */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 三级缓存:存放Bean工厂对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
各级缓存的作用
🔑 一级缓存(singletonObjects):
•作为单例池,存放所有已经完全初始化好的单例Bean
•当我们从容器中获取Bean时,首先会从一级缓存中获取
•所有单例Bean最终都会存放在这里
🔑 二级缓存(earlySingletonObjects):
•存放已实例化但尚未属性赋值、未执行初始化方法的Bean
•用于解决循环依赖,存放"半成品"Bean
•这是一个临时缓存,Bean最终会被移动到一级缓存
🔑 三级缓存(singletonFactories):
•存放Bean工厂对象(ObjectFactory)
•可以在需要时生成原始Bean对象或其代理对象(如果Bean被AOP切面代理)
•这也是一个临时缓存,获取对象后会从三级缓存移除,并放入二级缓存
为什么需要三级而非两级?
这是一个很多开发者都会思考的问题:既然二级缓存已经可以存放早期的Bean引用,为什么还需要三级缓存呢?🤔
核心原因在于AOP代理。在Spring中,如果一个Bean被AOP切面代理,那么最终注入的应该是代理对象而非原始对象。如果只有两级缓存:
1.在没有循环依赖的情况下,Spring会在Bean完全初始化后再创建代理对象
2.但在循环依赖的情况下,Bean还未完全初始化就需要被其他Bean引用
这就导致了一个问题:如果在循环依赖的情况下提前曝光原始对象,而最终需要的是代理对象,就会出现注入类型不一致的问题。
三级缓存的设计巧妙地解决了这个问题:
•三级缓存中存放的是Bean工厂对象,可以在需要时才创建代理对象
•只有在真正发生循环依赖时,才会触发代理对象的创建
•这样既保证了循环依赖的解决,又确保了注入的一致性
🔍 源码深度解析
关键类和方法
要理解Spring如何处理循环依赖,我们需要关注以下关键类和方法:
•DefaultSingletonBeanRegistry:定义了三级缓存,实现了单例Bean的注册和获取逻辑
•AbstractAutowireCapableBeanFactory:负责创建Bean实例、填充属性和初始化Bean
•getSingleton():从缓存中获取单例Bean的方法
•doCreateBean():创建Bean实例的核心方法
•addSingletonFactory():将Bean工厂添加到三级缓存的方法
getSingleton方法分析
getSingleton()方法是解决循环依赖的关键,它定义了如何从三级缓存中获取Bean:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 首先从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
// 如果一级缓存没有,且当前Bean正在创建中
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;
}
这个方法的执行流程是:
1.先从一级缓存(singletonObjects)查找
2.如果没找到且Bean正在创建中,再从二级缓存(earlySingletonObjects)查找
3.如果还没找到且允许早期引用,则从三级缓存(singletonFactories)获取工厂对象
4.通过工厂对象创建Bean,放入二级缓存,并从三级缓存移除
添加到三级缓存的时机
在Bean创建过程中,Spring会在实例化后、属性填充前将Bean工厂添加到三级缓存:
// 添加到三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
这个getEarlyBeanReference方法非常关键,它可能会返回原始Bean对象,也可能返回代理对象:
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;
}
💻 代码实战:不同场景下的循环依赖
场景一:setter注入的循环依赖(可解决)
Spring可以解决单例模式下通过setter方法注入的循环依赖:
@Component
public class A {
private B b;
public B getB() {
return b;
}
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public A getA() {
return a;
}
@Autowired
public void setA(A a) {
this.a = a;
}
}
这种情况下,Spring会:
1.完成A的实例化,并将其工厂添加到三级缓存
2.尝试填充A的属性,发现依赖B
3.开始创建B,完成实例化,并将其工厂添加到三级缓存
4.尝试填充B的属性,发现依赖A
5.此时可以从三级缓存中获取A的早期引用
6.完成B的初始化,并将B放入一级缓存
7.继续A的属性填充,注入B,完成A的初始化
8.将A放入一级缓存
场景二:构造器注入的循环依赖(无法解决)
Spring无法解决构造器注入的循环依赖:
@Component
public class A {
private B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public B(A a) {
this.a = a;
}
}
这种情况下会抛出BeanCurrentlyInCreationException异常,原因是:
•构造器注入是在实例化阶段就需要依赖对象
•而Spring的三级缓存机制是在实例化后才生效
•当创建A时,需要先有B的实例,但创建B又需要先有A的实例,形成死锁
场景三:原型作用域的循环依赖(无法解决)
Spring也无法解决原型(prototype)作用域的循环依赖:
@Component
@Scope("prototype")
public class A {
@Autowired
private B b;
}
@Component
@Scope("prototype")
public class B {
@Autowired
private A a;
}
原因是:
•原型Bean每次获取都会创建新实例
•三级缓存机制只对单例Bean生效
•原型Bean不会被缓存,每次都是重新创建
场景四:AOP代理下的循环依赖处理
当Bean被AOP代理时,循环依赖的处理会更加复杂:
@Component
public class A {
@Autowired
private B b;
@Transactional
public void methodA() {
System.out.println("A's method");
}
}
@Component
public class B {
@Autowired
private A a;
public void methodB() {
a.methodA(); // 调用A的事务方法
}
}
在这种情况下:
1.A因为有@Transactional注解,需要被AOP代理
2.当B依赖A时,注入的应该是A的代理对象而非原始对象
3.三级缓存中的工厂会在getEarlyBeanReference方法中创建代理对象
4.这确保了即使在循环依赖的情况下,B注入的也是A的代理对象
🔄 循环依赖解决流程图解
下面是Spring解决循环依赖的完整流程图,展示了从Bean创建到循环依赖解决的全过程:
这个流程与Bean的生命周期密切相关,循环依赖的解决发生在Bean的实例化和属性填充阶段:
⚠️ 循环依赖的限制与陷阱
Spring无法解决的循环依赖场景
虽然Spring的三级缓存机制很强大,但仍有一些循环依赖场景是无法解决的:
1.构造器注入的循环依赖:如前所述,因为实例化阶段就需要依赖对象
2.原型作用域的循环依赖:因为原型Bean不会被缓存
3.使用@DependsOn导致的循环依赖:显式指定了初始化顺序
4.使用@Async的方法循环调用:可能导致死锁
常见错误与解决方案
当遇到Spring无法解决的循环依赖时,可以考虑以下解决方案:
1.将构造器注入改为setter注入:这是最简单有效的方法
2.使用@Lazy注解延迟依赖注入:
3.重新设计避免循环依赖:
•引入中间层打破循环
•合并有循环依赖的类
•使用事件机制替代直接依赖
🚀 实践建议与最佳实践
如何避免循环依赖
虽然Spring能够解决大部分循环依赖问题,但从设计角度看,避免循环依赖通常是更好的选择:
1.遵循单一职责原则:确保每个类只有一个职责,减少相互依赖
2.使用依赖倒置原则:依赖抽象而非具体实现
3.引入中间层:通过中间服务或事件机制解耦
4.使用观察者模式:替代直接的双向依赖
合理使用依赖注入
在实际开发中,可以遵循以下建议:
1.优先使用构造器注入:虽然无法解决循环依赖,但可以明确依赖关系,有助于发现潜在设计问题
2.必要时使用setter注入:在确实需要处理循环依赖时使用
3.慎用@DependsOn:避免创建显式的依赖顺序
4.合理规划Bean的作用域:尽量使用单例作用域
📝 总结与延伸
核心要点回顾
通过本文,我们深入了解了Spring处理循环依赖的机制:
1.Spring通过三级缓存机制解决单例Bean的循环依赖问题
2.三级缓存分别存放完成品Bean、半成品Bean和Bean工厂
3.三级缓存的设计主要是为了处理AOP代理对象的创建时机
4.Spring无法解决构造器注入和原型作用域的循环依赖
5.良好的设计应当尽量避免循环依赖
Spring设计哲学思考
Spring框架的三级缓存机制体现了其设计哲学:
•开闭原则:通过扩展而非修改来适应新需求
•单一职责:每个缓存有明确的职责
•延迟加载:只在必要时才创建代理对象
•优雅降级:尽可能解决问题,无法解决时给出明确错误
这种精心设计使Spring能够在保持简洁API的同时,内部处理复杂的依赖关系,为开发者提供便利。
相关知识推荐
如果你对Spring的内部机制感兴趣,可以进一步探索以下主题:
•Spring Bean的完整生命周期
•Spring AOP的实现原理
•Spring IoC容器的设计与实现
•Spring事务管理机制
通过深入理解这些核心机制,你将能够更加高效地使用Spring框架,并在遇到复杂问题时找到合适的解决方案。🌟
希望本文能帮助你理解Spring循环依赖的处理机制,如有疑问或建议,欢迎交流讨论!💬