本文参考:

再谈谈 Spring 中的循环依赖 - spring 中文网 (springdoc.cn)

一文告诉你Spring是如何利用”三级缓存”巧妙解决Bean的循环依赖问题的 - 腾讯云开发者社区-腾讯云 (tencent.com)

:o: 什么是循环依赖

循环依赖的场景来自于你以前开发时可能遇到过的情况:有这样两个类,A和B,A中存在一个B的引用,同时B中也存在一个A的引用,如果不适用Spring的依赖注入,在正常的开发中或许不会有太大的问题,但在一个自动注入的场景,很有可能发生当实例化A时,需要实例化B,而跑去实例化B时,又需要实例化一个A,陷入一个循环,很快就内存溢出了。

我在学习JavaSE的时候写一个小游戏就遇到过类似的场景,这是由我的类的设计存在问题。

我的游戏具有一个登录窗口,我使用一个LoginFrame;然后是一个GameFrame窗口,为了实现在这两个窗口之间能够实现转换(登陆成功后呼出GameFrame、退出登陆时呼出LoginFrame),我很愚蠢的在LoginFrame中加上GameFrame字段、同时在GameFrame中加入LoginFrame字段。每次呼出另一个窗口时都新建一个窗口,并将当前窗口设为不可见。

当然我现在意识到我只需要将这两个Frame实例化后交给一个类管理,并通过这个中间类设置双方的可见性即可

在谈论什么是循环依赖之前,先了解一下什么依赖注入的概念。

:pill: 依赖注入

在Spring中,可以在xml文件中配置一个bean标签进行实例化,默认会将这个类的实例对象放到**单例池 ** singletonObjects中。你还可以为这个类的每一个字段设置指定的值,只要你为这个字段提供一个set方法。

假设有一个人叫张三,他是一名工人,他的老板叫老王,现在需要借助依赖注入实例化张三和老王

1
2
3
4
5
6
7
8
9
<!--一个Boss类-->
<bean id="Boss"class="com.example.entity.Boss">
<property name="name" value="老王"></property>
</bean>
<!--一个Employee类,它有一个Boss属性,表示它的Boss是谁-->
<bean id="Employee"class="com.example.entity.Employee">
<property name="name" value="张三"></property>
<property name="Boss" ref="Boss"></property>
</bean>

ref 是 reference 的缩写形式,表示引用其他Bean的id。

在创建(new)Employee后,由于需要对它进行依赖注入,Spring会去寻找单例池(singletonObjects)中是否存在Boss这个实例,如果不存在,先创建这个实例,放入单例池中,回溯到依赖注入的地方,再为Employee注入Boss对象,然后再将Employee放入单例池中。

:pill: ​循环依赖

前面提到,创建Employee后,要对它的Boss字段进行注入。会先到单例池中找是否有Boss对象,没有的话,会创建Boss对象,思考一下,如果Boss中同样存在Employee的实例,且同样需要通过Spring注入。坏了,创建Employee,需要先创建Boss,但创建Boss,又需要给他注入一个Employee,这下陷入死循环了。

这样的场景就叫做循环依赖。

:o: 如何解决

##:pill: Java地址传递特性

既然Employee和Boss已经采取单例模式了,那么是否可以在创建Employee后,还没有注入Boss之前,将Employee记录下来,接下来继续之前所讲的,注入Boss需要新建Boss,等到新建 Boss 注入Employee时,将之前记录的Employee直接给Boss,这样就能够跳出循环而不用新建一个Employee了。

而且由于Java引用类型的地址传递特性,Employee和Boss之间的互相指向的是对方的地址,这意味着当Boss完成它的所有内容的自动注入时,Employee中的Boss就是这个完全体的Boss。

:o: 三级缓存

在 DefaultListableBeanFactory 向上四级父类DefaultSingletonBeanRegistry中有三个容器:

Spring就是使用这三个Map解决缓存依赖问题的。

1
2
3
4
5
6
7
8
public class DefaultSingletonBeanRegistry ... {
//1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"
Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"
Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
//3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"
Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}
  • 单例池singletonObjects用来存放完全构建好的实例对象

  • earlySingletonObjects 当这个对象还没有完全创建,而它已经被其他对象引用时,就会先放在这个二级缓存中

    • 举个例子:Boss老王可能不止张三一个雇工,在新建第二个雇工时,Boss老王可能还没完成构建好,但已经被Employee张三引用了,这时老王就在第earlySingletonObjects池中
  • singletonFactories 每一个新建对象都会被先放到这个池子中,如果完全构建好,才从这个池子中挪到一级缓存;如果被其他对象引用了,就放到二级缓存中去

补充:

1
2
3
4
5
6
7
8
9
/** Names of beans that are currently in creation. */
// bean创建过程中都会存放在这个Map中
// 它在Bean开始创建时放值,创建完成时将其移出
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

/** Names of beans that have already been created at least once. */
// 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
// 至少被创建了一次的 都会放进这里
private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));

:pill: getSingleton方法

三级缓存 singletonFactories 就是用来存放未完全实例化的类的。

在当前类(DefaultSingletonBeanRegistry)中存在getSingleton()方法用来获取池子中的对象。这个方法会在创建实例时需要为他进行依赖注入时调用,用来找那个需要注入的字段。它会先在第一季缓存singletonObjects中找需要注入的的bean,然后是第二级缓存earlySingletonObjects,最后是第三级缓存singletonFactories。如果发现存在于第三级缓存中,程序将这个bean从第三级缓存中取出放入第二级缓存。当然,在这段过程中,他会判断这个Bean是否正在新建,如果没有,这个方法不会往下执行,直接返回去新建这个Bean。

代码如下:

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
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName); //在第一季缓存中找
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName); //在第二级缓存中找
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); //在第三级缓存中找
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}

:pill: 兼顾实例化的getSingleton方法

同时你会注意到,在DefaultSingletonBeanRegistry类中还有一个getSingleton()方法:

1
2
3
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
...
}

这个方法兼顾新建Bean的功能,在Spring容器创建后,加载配置文件(.xml文件),就会调用到这个方法来新建对象。

知道了Spring存在三级缓存读取机制,接下来再来讲Spring具体是如何使用三级缓存机制解决问题的。

:o: Bean的生命周期:

如果要搞清楚Spring解决循环依赖的全过程和三级缓存的实现源码,就必须要先了解 Spring 创建一个 Bean 的全过程。下图是发生循环依赖时,从Spring容器加载配置文件起,程序的方法调用流程图:

三级缓存源码剖析流程_page-0001

上图假设有两个Bean,A和B,它们之间形成循环依赖。

  1. 初始化A的过程中需要注入B,因此进行了B的实例化和初始化(图中1到3部分),B的实例化过程中又需要注入A(图中4到5部分),此时从三级缓存中获取到A,并把A从三级缓存移到二级缓存,也就是图中第六部分标红的方法。

  2. 然后方法调用结束,开始回溯,继续B的其他初始化阶段。(图中的5到4部分)

  3. B完全构建完成,将B从三级缓存移到一级缓存(单例池)中。然后回溯到A的初始化部分,(图中的3部分)

  4. A完全构建完毕,将A放到一级缓存,并把三级和二级缓存中的A删除(如果有的话)

需要注意的是,在Bean刚被实例化(也就是new出来,在内存空间具有地址后),Bean就会被放到三级缓存中。具体可以参看doGetBean方法

可以debug查看方法调用情况:

Spring容器加载Bean的栈轨迹

:pill: ​跟着方法调用的脚步

前面提到,在程序刚开始为Spring容器中加载Bean时,会调用兼顾实例化的getSingleton方法,这个方法中就会对未找到的Bean构建实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//兼顾实例化的getSingleton方法的调用 ———— 在AbstractBeanFactory 的doGetBean方法中
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}

//getSingleton方法的定义
//其中的ObjectFactory是一个函数式接口,它只有一个getObject()方法。
//前面调用getSingleton的lambda表达式就是在重写这个方法
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
...
singletonObject = singletonFactory.getObject();//在调用getObject()方法时相当于在调用createBean(beanName, mbd, args)
...
}


//createBean方法中又调用了一个doCreateBean方法
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
...
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
...
}

为了程序的健壮性,有时候不得不像这样层次嵌套,get里有个doGet,create里有个doCreate,之间做的很多工作都是健壮性和拓展性,然后将具体的功能抽象成不同的方法。

:pill: doCreatBean方法

在AbstractAutowireCapableBeanFactory这个类中有doCreateBean方法,也就是前面方法调用分析最后调用的那个doCreateBean方法。这个方法大致交代了Bean的一生(生命周期

doCreateBean方法解析:

doCreateBean方法主要做了四件事:

  • 实例化Bean
  • Bean字段填充
  • 初始化Bean
  • 添加销毁方法

大致可以分为几大模块

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
/**
beanName: Bean的名字
mbd: Spring容器中维护了一个BeanDefinitionMap用于注册所有需要实例化的类的注册信息,其中存放的BeanDefinition维护了很多这个类的实例化相关的信息信息。而RootBeanDefinition是BeanDefinition的一个子类。
args: 创建实例时需要的参数
*/

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
...
Object bean = instanceWrapper.getWrappedInstance(); //通过反射实例化Bean

...
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); //调用BeanProcessorBean后置处理器

...
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); //实例化后放入三级缓存区, 即SingletonFactory

...
populateBean(beanName, mbd, instanceWrapper); //填充Bean的字段,当涉及到循环依赖时

... //循环依赖校验,这里主要用于处理发生了动态代理的情况,即对Bean进行了增强,导致最终放到二级缓存中的Bean不是原来的Bean,而是一个Proxy代理对象的情况

registerDisposableBeanIfNecessary(beanName, bean, mbd); //添加销毁方法,在bean的生命周期走到尽头---销毁阶段时,会调用它的销毁方法

}

在这一切结束之后,Bean会被放入singleObjects 单例池中。

doCreateBean方法实际上包括于几乎是一个Bean的生命周期的全流程,这里有一个关于Bean生命周期的图:

Spring构建bean时的方法调用

它实际上还包含了Aware接口,后处理器等一系列使Spring更灵活的接口的生效时机,但这不是本文所关注的。

:o: 写在最后

思考一个问题:

本篇的依赖注入的方法是通过setter注入,如果是构造器注入,还能跳出依赖循环吗?如果它们的实例类型scope都设置为prototype(不使用单例而是每次getBean都重新实例化一个)、如果是自己依赖自己的场景呢?

见参考中的第一篇文章:参考文章

本篇笔记为本人学习Spring时关于Spring循环依赖的一部分思考和总结,可能存在一些错误,希望指正。

————————————

参考:

再谈谈 Spring 中的循环依赖 - spring 中文网 (springdoc.cn)

一文告诉你Spring是如何利用”三级缓存”巧妙解决Bean的循环依赖问题的 - 腾讯云开发者社区-腾讯云 (tencent.com)