循环依赖和三级缓存
本文参考:
再谈谈 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 | <!--一个Boss类--> |
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 | public class DefaultSingletonBeanRegistry ... { |
单例池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 | protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
:pill: 兼顾实例化的getSingleton方法
同时你会注意到,在DefaultSingletonBeanRegistry类中还有一个getSingleton()方法:
1 | public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
这个方法兼顾新建Bean的功能,在Spring容器创建后,加载配置文件(.xml文件),就会调用到这个方法来新建对象。
知道了Spring存在三级缓存读取机制,接下来再来讲Spring具体是如何使用三级缓存机制解决问题的。
:o: Bean的生命周期:
如果要搞清楚Spring解决循环依赖的全过程和三级缓存的实现源码,就必须要先了解 Spring 创建一个 Bean 的全过程。下图是发生循环依赖时,从Spring容器加载配置文件起,程序的方法调用流程图:
上图假设有两个Bean,A和B,它们之间形成循环依赖。
在初始化A的过程中需要注入B,因此进行了B的实例化和初始化(图中1到3部分),B的实例化过程中又需要注入A(图中4到5部分),此时从三级缓存中获取到A,并把A从三级缓存移到二级缓存,也就是图中第六部分标红的方法。
然后方法调用结束,开始回溯,继续B的其他初始化阶段。(图中的5到4部分)
B完全构建完成,将B从三级缓存移到一级缓存(单例池)中。然后回溯到A的初始化部分,(图中的3部分)
A完全构建完毕,将A放到一级缓存,并把三级和二级缓存中的A删除(如果有的话)
需要注意的是,在Bean刚被实例化(也就是new出来,在内存空间具有地址后),Bean就会被放到三级缓存中。具体可以参看doGetBean方法
可以debug查看方法调用情况:
:pill: 跟着方法调用的脚步
前面提到,在程序刚开始为Spring容器中加载Bean时,会调用兼顾实例化的getSingleton方法,这个方法中就会对未找到的Bean构建实例化。
1 | //兼顾实例化的getSingleton方法的调用 ———— 在AbstractBeanFactory 的doGetBean方法中 |
为了程序的健壮性,有时候不得不像这样层次嵌套,get里有个doGet,create里有个doCreate,之间做的很多工作都是健壮性和拓展性,然后将具体的功能抽象成不同的方法。
:pill: doCreatBean方法
在AbstractAutowireCapableBeanFactory这个类中有doCreateBean方法,也就是前面方法调用分析最后调用的那个doCreateBean方法。这个方法大致交代了Bean的一生(生命周期)
doCreateBean方法解析:
doCreateBean方法主要做了四件事:
- 实例化Bean
- Bean字段填充
- 初始化Bean
- 添加销毁方法
大致可以分为几大模块
1 | /** |
在这一切结束之后,Bean会被放入singleObjects 单例池中。
doCreateBean方法实际上包括于几乎是一个Bean的生命周期的全流程,这里有一个关于Bean生命周期的图:
它实际上还包含了Aware接口,后处理器等一系列使Spring更灵活的接口的生效时机,但这不是本文所关注的。
:o: 写在最后
思考一个问题:
本篇的依赖注入的方法是通过setter注入,如果是构造器注入,还能跳出依赖循环吗?如果它们的实例类型scope都设置为prototype(不使用单例而是每次getBean都重新实例化一个)、如果是自己依赖自己的场景呢?
见参考中的第一篇文章:参考文章
本篇笔记为本人学习Spring时关于Spring循环依赖的一部分思考和总结,可能存在一些错误,希望指正。
————————————
再谈谈 Spring 中的循环依赖 - spring 中文网 (springdoc.cn)
一文告诉你Spring是如何利用”三级缓存”巧妙解决Bean的循环依赖问题的 - 腾讯云开发者社区-腾讯云 (tencent.com)