【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()

盖棺 问:Spring 是如何解决循环依赖的?
答:支持提前暴露当前正在创建的单例,这个单例可以是未完全初始化的。
问:三级缓存是必要的吗?
答:非必要,两个缓存可以支持,Spring 2.5.3 版本中,只使用两个缓存来解决循环依赖。
show you the code 欢迎移步地址:三级缓存非必要 进行 clone 和 debug。
说明:
以上示例,实际依赖 Spring 版本:2.5.3,该版本适用 Java7,所以需要先调整一个属性以绕过 Spring 框架的检查。
如果想要进行源码 debug,可以如下点击:Download Source and Document 然后断点调试.
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

【【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()】如果想要切换三级缓存实现,可以直接修改 pom.xml 文件中 Spring 版本,直接提升版本到 2.5.4 ,然后重新进行 Maven ReloadDownload Source and Document 即可。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

定论 循环依赖,表现形式很简单,就是 A -> B(A 引用了 B)B -> A(B 引用了 A),在其他技术的应用场景可能会因此出现常见的死锁问题。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

在讨论 Spring 是如何解决循环依赖前,需要先认识两个概念:
1. 普通对象与代理对象

普通对象:指常见的通过有参/无参构造函数 newInstance 实例化后,由 Spring 进行属性赋值并执行其他初始化操作后得到的对象,与普通的 new 操作得到的对象接近;
代理对象:是 Spring 通过代理技术,如内置的 Jdk动态代理、CGLIB 代理生成的 为原始实例增加额外行为的对象
这两者有什么关系呢?
从类层级来说,代理对象类型总是低于等于(子类化)普通对象类型(或相同的 Class 类型),另外,代理对象,实际上是对普通对象加了一层包裹,代理行为在外,实际对象在内(可以理解为,Spring中,实际存在两个相同类型的对象,其中代理对象内部引用普通对象)。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

Spring 代理对象给循环依赖造成的棘手问题也因此而解,也就是不论最终依赖的是普通对象还是代理对象,属性的类型都足以支持进行引用赋值(可以赋值为普通对象引用,也可以赋值为代理对象引用)。
2. Spring 的 Bean 创建过程
Spring 创建 Bean 分为三个阶段:
实例化:简单理解为,Spring 擅自使用 newInstance 操作,为我们调用无参构造函数,创建出一个普通对象,此时,拥有普通对象的引用但内部属性未赋值
属性填充:拥有了对象后,需要给对象内的属性赋值,如常见的 @Autowired 和 @Value 等,而注入的依赖对象可能导致循环引用
初始化:填充完内部属性后,为对象执行其他的一系列操作,此时可能在原对象的基础上,为其套上一层壳(额外行为),即为代理对象(即正常的代理对象生成阶段),最后完成对象构建过程
也就是说:在实例化、属性填充阶段,对象是不完全的,而在初始化阶段后,无论是否生成代理对象,此阶段后的 Bean 就是最终形态。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

3. 收尾
理解完上述两点,继续往下走。
破坏死锁的方法有许多种,而 Spring 的解决方式主要是:支持提前暴露当前正在创建的单例,这个实例可以是未完全构建的。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

可能有人会疑惑,如果 B对象提前加载了A的代理对象,即 B引用了代理对象 A'后完成了 B 对象本身的构建,而 A 的普通对象此时正处于填充属性阶段,可能存在其他属性需要在填充 B引用 之后进行,他们是如何关联上的?
这就回到刚刚的第一点,就是:代理对象实际是对普通对象的一个包裹,属性填充、初始化的其他操作最后都会在原始对象上进行。
所以,原始对象只有一个,而最终属性依赖到的对象,是 代理对象 A' 还是 普通对象 A,只取决于它有没有被套一层壳,即使有,也因为代理技术上的支持,使得 代理对象 A' 完全可以被被赋值到 属性 A 上。
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

加铆钉 在 Spring2.5.3 的源码中,开发者只使用了两个缓存来解决循环依赖,并且翻看源码,可以看到:
【Spring浅析】七、给二三级缓存盖棺定论,面试官你答案对吗()
文章图片

注释中,明确了允许提前引用一个当前正在创建的单例对象用于解决循环依赖
再然后,查看 Spring2.5.4 源码、代码注释和 changelog,你也找不到任何关于新增出三级缓存的描述,在我看来,新缓存不过是作者为了更加清晰的代码流程以及为 Spring 提速而新增的属性,如果是 bug,至少会在 changelog 中提一嘴,但实际却不值一提。
回到文章开头的问题:
问:Spring 是如何解决循环依赖的?
答:支持提前暴露当前正在创建的单例,这个单例可以是未完全初始化的。Spring2.5.3 使用了二级缓存,Spring2.5.4 及之后,使用了三级缓存。实际上,解决循环依赖,甚至只需要一个缓存就可以,二级缓存也不是必须的。
你能接受这个答案吗?
The End DefaultSingletonBeanRegistry:我所被给予的,不过是一个小小的集合属性,而我所赋予的,是 Java八股界的一次狂欢。
三级缓存博文何时开始是不确定的了,但泛滥在互联网中是确定的,希望每个纠结三级缓存的面试官,或即将成为面试官的 Javaer,都能够读到此文,否则,在座各位阅完有所收获的读者,恐怕只会徒增面试被涮的风险^_^|。
由于 Spring 目前已被迁移至 github,仓库最早的源码版本是:3.0.0。在此附上 spring2.5.3spring2.5.4 完整源码文件及 changelog 下载链接(密码: othl),有兴趣可自行获取。

    推荐阅读