文章目录
Spring深度学习:循环依赖及源码解析
一、序言
大家经常戏称Java工程师为Spring工程师,毫无疑问,这句话体现出Spring框架在Java开发中的重要性和普及度。
本文小豪将带大家深度学习Spring循环依赖相关知识,包括循环依赖的解决方案及Spring处理循环依赖的底层源码,学习过程中不仅仅是了解Spring处理循环依赖的设计,更重要的是学会理解框架的底层逻辑,从而提升我们的思维能力。
文章最后附有流程图,进一步帮我们梳理业务逻辑
二、问题原因
在上一篇【Spring深度学习:Bean生命周期及源码解析】中我们初步了解到,在实例化单例Bean
调用getSingleton()
方法时,Spring会先分别从一、二、三级缓存中尝试获取单例Bean
。
那具体这三级缓存分别是什么呢?
/** 一级缓存,存储所有创建好的完整单例Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 三级缓存,存储创建Bean的工厂对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** 二级缓存,存储未创建好的半成品单例Bean */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
- 一级缓存(singletonObjects):存储所有已经创建好的完整单例
Bean
,即我们常说的单例池缓存 - 二级缓存(earlySingletonObjects):存储未创建好的半成品单例
Bean
,即我们常说的提前暴露缓存 - 三级缓存(singletonFactories):存储创建
Bean
的工厂对象,即我们常说的工厂对象缓存
Spring三级缓存大致了解了,那Spring为什么需要这三级缓存,具体解决了什么问题呢?没错,解决Bean
的循环依赖问题。
循环依赖,就是存在死循环,比如我们存在两个对象A
和B
,如果对象A
、B
分别在属性注入阶段需要注入对方,那就会形成一个死循环,即A
需要B
,B
也需要A
,无线递归套娃。
具体循环依赖又分为三种类型:
A
自己依赖自己A
依赖B
、B
依赖B
A
依赖B
、B
依赖C
、C
依赖A
回到Spring中,通常我们Bean
对象进行属性注入常用三种方式:
- 基于构造方法注入:
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
xml配置文件:
<bean id="bBean" class="com.xiaohao.B"/>
<bean id="aBean" class="com.xiaohao.A">
<constructor-arg ref="bBean"/>
</bean>
- 基于set方法注入:
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
xml配置文件:
<bean id="bBean" class="com.xiaohao.B"/>
<bean id="aBean" class="com.xiaohao.A">
<property name="b" ref="bBean"/>
</bean>
- 基于属性注入:
@Component
public class A {
@Autowired
private B b;
}
上面三种属性注入情况,若产生循环依赖,实际上Spring只能解决后两者,而基于构造方法注入产生的循环依赖,Spring是处理不了的。
这里为什么处理不了基于构造方法注入产生的循环依赖呢?其实Spring解决循环依赖的本质是将实例化和初始化分离,在实例化之后产生缓存,允许其它对象引用。
而构造方法注入,在解析构造方法参数时就会去创建引用对象,而当前类自身还没有完成实例化,如果引用对象又依赖当前对象,则会产生死循环,均无法创建。
三、解决方案
1.普通Bean对象循环依赖解决
通过上面的介绍,我们大概了解到Spring循环依赖产生的场景,那我们应该如何去解决呢?
这里其实很容易想到解决方案,我们可以尝试多加个缓存去解决。
比如我们可以在对象A
完成实例化之后,将其放入一个存放不完整Bean
的缓存中,具体的执行流程变为:
- 我们在对象
A
实例化完成之后,属性注入之前,将不完整的对象A
放入缓存中 - 对象
A
进行属性注入B
时,从缓存中查找B
的实例,如果B
没有找到,则开始创建B
的实例 - 同样在对象
B
实例化完成之后,也将不完整的对象B
放入缓存中 - 对象
B
进行属性注入A
时,从缓存中查找A
的实例,这时由于A
的不完整Bean
已经放入缓存中,则可以正常获取到,之后对象B
完成后续的创建过程,生成完整实例B
并放入单例池缓存 - 在
B
的实例创建完成之后,对象A
也正常执行后续流程,完成创建,放入单例池缓存
我们发现,处理循环依赖的情况,额外加一层缓存即可解决循环依赖问题,在Spring中这个放置不完整Bean
的缓存也称为提前暴露缓存。
很多同学到这里就会迷茫了,对象
B
依赖注入A
时,注入的是不完整的对象A
,也就是对象A
并没有执行自身的依赖注入和后续的初始化流程,这样不会有问题吗?其实这里注入的只是对象
A
的引用,在JVM堆内存中对象A
的地址始终没有发生变化
2.AOP代理场景下循环依赖解决
在上面我们分析到,似乎我们只需要在单例池缓存基础上额外加一层提前暴露缓存即可。
但是这里我们忘了一点,对象A
在初始化阶段可能会产生AOP代理对象,最终放入单例池缓存的是A
的代理对象,而对象A
的属性注入阶段却在初始化阶段之前,这会造成放入提前暴露缓存的是对象A
的普通对象,然后对象B
注入的是对象A
的普通对象,并不是对象A
的代理对象,显然存在问题。
很明显,造成这样的问题是由于Bean
的生命周期,需要依次执行实例化、属性注入、初始化阶段。
可能大家也很容易想到解决方法,我们是否可以提前产生对象A
的AOP代理对象呢?答案是可以的,此时的流程变为:
大致流程和上面的相同,只不过此时在对象A
实例化完成之后,不会直接放入不完整的对象A
,而是放入一个A
的工厂对象,当检测循环依赖发生之后,根据情况通过工厂对象选择性提前生成对象A
的代理对象,赋值给对象B
,之后正常完成对象A
的初始化,不再重复生成对象A
的代理对象。
问题似乎又被我们解决了,只需要工厂对象缓存+单例池缓存即可解决存在AOP代理下的循环依赖问题,但是本文开头,我们介绍了Spring实际上使用了三级缓存,为什么Spring还需要额外多加一层缓存呢?
3.AOP代理场景下多依赖解决
其实这里我们忽略了一直情况,那就是在AOP代理下对象A
存在多依赖场景问题,如果对象A
同时依赖对象B
和C
,对象B
和C
也依赖对象A
,那么在对象B
属性注入的时候,生成了A
的代理,对象C
属性注入的时候,同样也生成了A
的代理,此时产生了两个不同的对象A
的代理,这显然违背了单例原则。
那我们应该如何解决呢,再加入一层缓存,保存工厂对象缓存生成的对象A的代理对象(提前暴露缓存),此时的业务流程为:
- 在对象
A
实例化完成之后,属性注入之前,将对象A
的工厂对象放入工厂对象缓存中 - 对象
A
进行属性注入B
时,依次从提前暴露缓存、工厂对象缓存中查找对象B
,均没有找到则开始创建B
的实例 - 同样在对象
B
实例化完成之后,也将对象B
的工厂对象放入工厂对象缓存中 - 对象
B
进行属性注入A
时,依次从提前暴露缓存、工厂对象缓存中查找对象A
,这时由于A
已经放入工厂对象缓存中,则可以正常获取到,拿到A
的工厂对象,生成对象A
的代理对象并放入提前暴露缓存,之后对象B
完成后续的创建过程,生成实例B
并放入单例缓存池 - 接着对象
A
进行属性注入C
时,重复流程②-③,进入流程④时,由于对象A
的代理对象已经被对象B
属性注入时生成并放入提前暴露缓存中,则对象C
可以直接通过提前暴露缓存获取到同一个对象A
的代理对象,正常完成后续流程,保证了对象A
是单例的 - 在注入了对象
B
、C
之后,对象A
也正常执行后续初始化流程,获取提前暴露缓存中已经生成代理对象A,放入单例池缓存中
自此,问题均已经被我们成功解决了。
小豪这里抛出一个疑问,大家可以思考思考,如果我们修改Spring Bean生命周期过程,在实例化Bean之后直接创建Bean的代理,能否只使用二级缓存解决循环依赖问题呢?
接下面我们来详细看一下在Spring底层源码中是如何实现这一过程的。
四、源码分析:
源码分析时,小豪在这里只截取了关键部分源码,便于更高效的梳理整体流程,具体详细源码过程可参考上一篇:【Spring深度学习:Bean生命周期及源码解析】。
同样在这里我们也通过一段流程模拟对象A
和B
的循环依赖。
初始缓存池:
流程①:实例化对象A后,将对象A的工厂对象放入三级缓存
首先我们进行对象A
的创建,进入AbstractAutowireCapableBeanFactory
类的doCreateBean("A")
方法,在对象A
实例化阶段完成之后,判断对象A
是否满足循环依赖条件,将对象A
放入singletonFactories
三级缓存:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
// 实例化阶段:进行实例化A对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 循环依赖:单例 && 支持循环引用 && 正在创建
// 条件成立则将对象A的工厂对象放入singletonFactories三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
//属性注入阶段:进行对象A的属性填充
populateBean(beanName, mbd, instanceWrapper);
}
我们先看一下这段代码,进入addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))
方法内部,发现逻辑比较简单,加锁之后将对象A
放入singletonFactories
三级缓存,同时从earlySingletonObjects
二级缓存中移除:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
//放入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
而getEarlyBeanReference()
方法,仔细发现它其实是使用lambda
表达式实现的匿名内部类,作为参数二传入addSingletonFactory()
方法中,具体它实现什么功能,我们先放一放,继续完成对象A
的后续执行。
此时的缓存池:
流程②:对象A依赖对象B,在缓存中查找对象B
紧接着对象A
进入属性注入阶段,发现依赖对象B
,则尝试获取对象B
,进入doGetBean("B")
方法:
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
// 检查缓存里是否有当前对象B单例(三级缓存)
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 存在则将缓存对象返回出去
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
// 如果不存在则进行实例化当前Bean
// 使用lambda表达式来实现匿名内部类,传入singletonFactory参数
sharedInstance = getSingleton(beanName, () -> {
return createBean(beanName, mbd, args);
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
这里首先从三级缓存无法获取到对象B
,后续会执行createBean()
方法创建对象B
,createBean()
方法内部其实又会调用doCreateBean()
方法,又会进入对象B
的流程①阶段。
流程③:实例化对象B后,将对象B的工厂对象放入三级缓存
// 实例化阶段:进行实例化B对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 将对象B的工厂对象放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
此时的缓存池:
流程④:对象B依赖对象A,在缓存中查找对象A
此时对象B
进入流程②阶段,执行对象B
的属性注入,发现依赖对象A
,会在缓存中获取到对象A
放入的工厂对象:
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
// 检查缓存里是否有当前对象A单例(三级缓存)
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 存在对象A,直接返回
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
}
流程⑤:通过三级缓存生成不完整的对象A,放入二级缓存
对象B
通过getSingleton("A")
方法,会依次从一、二、三级缓存中查找对象A
,最后会在三级缓存中拿到对象A
的工厂对象,调用工厂对象的getObject()
方法:
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) {
// 如果也为空,则从三级缓存中获取对象,此时可以获取到A放入的工厂对象
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用getEarlyBeanReference()的方法,生成不完整的对象A
// 这里根据条件会选择性生成对象A的普通对象或代理对象
singletonObject = singletonFactory.getObject();
// 放入二级缓存并在三级缓存中移除
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
在这里由于提前存放的是对象A
的工厂对象,调用getObject()
方法实际上会触发getEarlyBeanReference()
方法,而getEarlyBeanReference()
方法具体有什么作用呢?
想必通过上面分析的AOP代理场景下循环依赖解决,大家应该能反应过来。
没错,getEarlyBeanReference()
方法其实就是判断对象A
是否存在AOP
,如果对象A
存在AOP
,即需要提前创建代理对象A
,那么对象B
注入的就应该是代理后的对象A
,而不是普通对象A
。
我们进入getEarlyBeanReference()
方法源码看一下:
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
// 根据条件判断返回 普通对象A or 代理对象A
return wrapIfNecessary(bean, beanName, cacheKey);
}
同时将生成的对象A
的普通对象或代理对象放入二级缓存,并在三级缓存中移除:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
if (singletonFactory != null) {
// 调用getEarlyBeanReference()的方法,生成不完整的对象A
// 这里根据条件会选择性生成对象A的普通对象或代理对象
singletonObject = singletonFactory.getObject();
// 放入二级缓存并在三级缓存中移除
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
此时的缓存池:
流程⑥:完成对象B的后续创建,放入一级缓存
此时对象B
已经成功依赖注入对象A
,执行后续初始化流程,完成创建,最后放入一级缓存singletonObjects
中:
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 完整的对象B放入一级缓存(单例池)
this.singletonObjects.put(beanName, singletonObject);
// 在二、三级缓存中移除
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
此时的缓存池:
流程⑦:完成对象A的后续创建,放入一级缓存
对象B
创建完成之后,同时对象A
也可以成功拿到对象B
的完整实例,执行后续初始化流程,完成创建,最后也将对象A
放入一级缓存中,此时对象A
、B
均已创建单例完成,解决了循环依赖问题。
最后的缓存池:
五、流程图
六、后记
本文从Bean
循坏依赖问题产生原因开始,过度到思考其解决方法,同时也带大家阅读底层源码认识Spring如何处理该问题,相信大家看到这里,对Spring循环依赖有了更加清晰的认知。
显然循环依赖不是一个好的代码设计,导致Spring容器大费周章的采用三层缓存去解决它,增加了复杂性。应用在后续的开发中,我们应提前规划好类之间的组织关系,引入相应的接口或抽象类,尽量避免产生循环依赖,降低系统耦合度。
小豪在撰写这篇文章时,花费了大量的精力制作流程图,为解释Spring循环依赖问题提供直观的参考。如果大家觉得内容有价值,不妨考虑点点赞,关注关注小豪,后续小豪也会及时更新Spring底层源码其它系列文章哦~
标签:缓存,依赖,对象,Spring,beanName,源码,解析,放入 From: https://blog.csdn.net/mm1274889792/article/details/137556894