spring解决循环依赖探究
Kale

了解了一下Spring中解决循环依赖的手段,记录一下。

循环依赖

首先需要解释一下什么是循环依赖,例如有两个对象A和B,A依赖B,B依赖A,这就构成了循环依赖,循环依赖其实是个错误的设计,在实际工程中肯定是需要避免的,例如在go中如果产生了循环依赖,直接就会编译报错,但是在spring中,针对循环依赖提出了解决方案,虽然循环依赖是错误设计,但这种解决方案却很精妙。

解决方案

我们首先预设两个对象A和B,其中A依赖B,B依赖A。在spring中,一个比较核心的方法就是refresh()方法,该方法会完成容器的创建工作并且做好一切准备工作,所以创建bean也是在该方法中完成的。而在该方法的finishBeanFactoryInitialization(beanFactory)中,最后的beanFactory.preInstantiateSingletons()方法会实例化所有未实例化的且没有设置为懒加载的bean。该方法会遍历beanDefinitionNames,最先遍历到的就是"a",会调用getBean(beanName)方法,继而调用doGetBean()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// doGetBean,代码省去了一些细节
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {

String beanName = transformedBeanName(name);
Object beanInstance;
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
try {
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
catch (BeansException ex) {
throw ex;
}
finally {
beanCreation.end();
}
}
return adaptBeanInstance(name, beanInstance, requiredType);
}

主要关注第15行,调用了getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法,该方法内会调用第二个参数,而也就是第17行的createBean()方法,在该方法中会再去调用doCreateBean()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// doCreateBean,代码省去了一些细节
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object bean = instanceWrapper.getWrappedInstance();
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
}
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
}
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
}
return exposedObject;
}

在第六行,对A对象进行了实例化,注意此时只是实例化了对象,还没有进行属性填充,然后在第12行通过调用addSingletonFactory()方法将对象放到一个map中,也就是singletonFactories,即俗称的三级缓存中,注意放入的也是一个lambda表达式,接着在第16行,要完成属性填充,调用链是populateBean(beanName, mbd, instanceWrapper) --> applyPropertyValues(beanName, mbd, bw, pvs),此时发现在我们预设的环境下,A对象依赖B对象,那么,会调用valueResolver.resolveValueIfNecessary(pv, originalValue)方法,如果传入的originalValueRuntimeBeanReference类型,则会调用resolveReference(argName, ref)方法,而这个方法里,就会走到getBean()的那一套流程,从而创建出B对象。

在实例化B对象后,同样也会进入到属性填充流程,也会进入populateBean(beanName, mbd, instanceWrapper) --> applyPropertyValues(beanName, mbd, bw, pvs)调用链,在调用valueResolver.resolveValueIfNecessary(pv, originalValue)方法时,又会进入到getBean()的流程,但此时不同的是,getSingleton()方法可以返回对象了,因为在前面创建A对象时,已经将早期的A对象加入到singletonFactories中,也就是三级缓存中,而此时,会走到三级缓存处singletonObject = singletonFactory.getObject(),调用了lambda表达式,也就是getEarlyBeanReference(beanName, mbd, bean)方法:

1
2
3
4
5
6
7
8
9
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;
}

这个方法非常关键,会判断是否需要进行aop,如果需要,则会在此时进行aop,然后返回增强后的对象。

1
2
3
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

拿到对象后,可以看到会将对象放入到earlySingletonObjects中,也就是俗称的二级缓存中,然后将三级缓存中对应的值删除。

此时B对象的属性填充环节完毕,再进行初始化工作,最后调用addSingleton()方法将对象放入singletonObjects中,也就是俗称的一级缓存,存放完整的单例对象,然后将对象从三级缓存以及二级缓存中移除。

1
2
3
4
5
6
7
8
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}

此时B对象创建流程结束,A对象拿到完整的B对象,进行赋值,放入单例池。自此,循环依赖得到解决。

一些思考

可以看到,spring通过三级缓存,解决了循环依赖问题,那么,为什么需要三级缓存呢,网上很多回答解释说是有可能对象需要进行aop,所以需要三级缓存来解决,但其实只需要一级缓存也能解决循环依赖问题,并且可以实现aop。

我们设计这样一个场景,A对象依赖B对象,B对象依赖A对象和C对象,C对象依赖A对象,并且A对象还需要进行aop,即存入单例池的应该是A的代理对象。

  • 首先创建A对象,发现需要增强,则增强后放入单例池
  • 然后进行属性填充,发现依赖B对象,而单例池中没有找到B对象,则开始创建B对象
  • 实例化B对象后将B对象放入单例池,然后进行属性填充,发现依赖A对象,在单例池中找到A对象(代理对象),进行赋值,又发现依赖C对象,而单例池中没有找到C对象,则开始创建C对象
  • 实例化C对象后将C对象放入单例池,然后进行属性填充,发现依赖A对象,在单例池中找到A对象(代理对象),进行赋值,结束
  • 返回C对象给B对象,B对象进行属性填充,结束
  • 返回B对象给A对象,A对象进行属性填充,结束

可以发现上述流程只用到了一级缓存,并且预设场景中存在多级循环依赖以及需要aop的对象,下面写了一个demo实现了上述流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class MyBeanFactory {

private Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

public MyBeanFactory () {
// 创建A时发现需要进行增强
AProxy aProxy = new AProxy();
A a = aProxy.cglibFactory();
singletonObjects.put("a", a);

// A populate阶段,发现缺少B,并且单例池中没有B,则创建B对象
if (!singletonObjects.containsKey("b")) {
B b = new B();
singletonObjects.put("b", b);
// B populate阶段
if (!singletonObjects.containsKey("a")) {
// 如果不存在A对象,则进行创建,但此时已经能找到A对象
event("创建A对象,并放入单例池");
}
b.setA((A) singletonObjects.get("a"));
if (!singletonObjects.containsKey("c")) {
// 如果不存在C对象,则进行创建,此时很明显找不到C对象
C c = new C();
singletonObjects.put("c", c);
// C populate阶段
if (!singletonObjects.containsKey("b")) {
// 如果不存在A对象,则进行创建,但此时已经能找到A对象
event("创建a对象,并放入单例池");
}
c.setA((A) singletonObjects.get("a"));
}
b.setC((C) singletonObjects.get("c"));
}
a.setB((B) singletonObjects.get("b"));
}

public void event (String name) {
}
}

从结果中可以看到,单例池中的对象都是完整对象,并且需要aop的对象也成功对应到了代理对象:

由此可见,一级缓存就足以解决这个问题,那么为什么需要三级缓存呢。

其实三级缓存解决并不是唯一解,解决问题的方法有很多,而spring选择了这一种,考虑一下上面流程中提到的一级缓存解决的方案,必须在实例化对象完毕后就立即检查是否需要进行aop,这个才是问题的关键,因为这样做破坏了原本的bean生命周期,在正常流程中,aop是在initializeBean()方法中通过postProcessAfterInitialization后置处理器去触发,而在发生循环依赖时,则是相当于在populateBean()方法进行了aop,相比正常流程稍微提前了一点。循环依赖本就是需要避免的内容,肯定是不希望因为为了解决循环依赖而去改动原始的流程,所以引入三级缓存的方式来进行解决。

另外,观察可以发现,二级缓存和三级缓存是配套使用的,三级缓存中的lambda被执行后立即将对象放入二级缓存,并从三级缓存中删除,这是为了避免多级依赖的情况下,反复执行三级缓存中的lambda表达式从而生成不一样的对象,这肯定是不可以接受的,所以需要二级缓存作为中继,二级缓存中存储的一定是经过增强的(如果需要增强)对象。


看了很久源码,spring的源码太难啃了…很多设计也非常巧妙,比如这个循环依赖问题的解决,另外其实我觉得对三级缓存这个名词不太恰当,所谓的二级缓存和三级缓存其实只是在bean的创建过程中短暂用到,后续就不会再用到,只是一个临时存放对象的map而已。

  • 本文标题:spring解决循环依赖探究
  • 本文作者:Kale
  • 创建时间:2022-09-13 23:20:08
  • 本文链接:https://kalew515.com/2022/09/13/spring解决循环依赖探究/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!