三级缓存解决循环依赖问题

三级缓存解决循环依赖问题

前言

目前我们的 Spring 可以完成一个基本功能。但是如果遇到 A、B 两个 Bean 对象相互依赖,就会报出 java.lang.StackOverflowError 错误。因为创建 A 时需要 B,而 B 的创建又依赖于 A 创建,死循环

循环依赖是 Spring 经典的场景。需要解决的主要是以下三种情况:

  • 自身依赖
  • 循环依赖
  • 多组依赖

image-20230218114108130

按照 Spring 框架的设计,用于解决循环依赖需要用到三个缓存 Map,这三个缓存分别存放了成品对象、半成品对象(未填充属性值)、代理对象,分阶段存放对象内容,来解决循环依赖问题。

  • singletonObjects:成品对象
  • earlySingletonObjects:半成品对象
  • singletonFactories:工厂对象(代理对象)

这里我们需要知道一个核心的原理,就是用于解决循环依赖就必须是三级缓存呢,二级行吗?一级可以不?其实都能解决,只不过 Spring 框架的实现要保证几个事情,如只有一级缓存处理流程没法拆分,复杂度也会增加,同时半成品对象可能会有空指针异常。而将半成品与成品对象分开,处理起来也更加优雅、简单、易扩展。另外 Spring 的两大特性中不仅有 IOC 还有 AOP,也就是基于字节码增强后的方法,该存放到哪,而三级缓存最主要,要解决的循环依赖就是对 AOP 的处理,但如果把 AOP 代理对象的创建提前,那么二级缓存也一样可以解决。但是就没法为 AOP 所创建的代理对象注入属性了

半成品容易导致空指针现象

半成品对象是指在对象的构造函数中,由于某些原因导致对象尚未完全初始化,即存在某些属性或者状态未被正确初始化的情况,这样的对象也被称作是“不完整的对象”。在使用这些对象的时候,由于其状态不完整,很容易引发 NullPointerException 等异常。

半成品对象引起空指针异常的原因通常有以下几个方面:

  1. 对象的构造函数中发生了异常,导致对象没有被正确初始化。例如,构造函数中可能会调用其他方法或者访问其他属性,如果这些方法或属性返回了 null 值或者抛出了异常,就有可能导致对象的状态不完整。
  2. 多线程情况下,可能会出现并发访问半成品对象的情况。由于半成品对象尚未完全初始化,如果多个线程同时访问该对象,就有可能引发线程安全问题,例如出现竞态条件等情况。
  3. 对象中存在循环依赖的情况。如果两个或多个对象之间存在循环依赖关系,就有可能导致其中一个对象尚未完全初始化,从而出现半成品对象的情况。

因此,在编写程序时,需要注意避免出现半成品对象的情况,通常的做法是在构造函数中完成所有属性的初始化,并且避免在构造函数中调用其他方法或者访问其他属性,同时也需要注意多线程访问和循环依赖的问题。

三级缓存的具体流程如下

三级缓存架构图

image-20230218185123867

在 Spring 框架中,半成品对象容易引发空指针的问题,通常是由于对象之间的复杂依赖关系和循环依赖问题导致的。

为了解决这个问题,Spring 框架采用了 “三级缓存” 的机制,以确保对象能够正确地初始化和注入依赖。

在 Spring 中,对象的创建和管理是由 BeanFactory 和 ApplicationContext 完成的。当 Spring 容器创建一个 Bean 的实例时,会先将其创建成一个原型对象,然后检查该对象是否已经被创建过。如果没有被创建过,那么会将其添加到一级缓存(singletonObjects)中,即完全初始化完成的单例对象缓存中。如果该对象存在依赖关系,那么 Spring 会在创建过程中将其添加到二级缓存(earlySingletonObjects)中,即尚未完全初始化完成的单例对象缓存中,以避免循环依赖的问题。

当对象的所有依赖项都被注入后,Spring 会将对象从二级缓存中移除,并将其添加到三级缓存(singletonFactories)中,即创建单例对象的工厂实例缓存中。通过这个工厂实例,Spring 可以对对象进行一些扩展,例如使用 AOP 进行代理,以实现对对象的增强。

当下次需要获取该对象时,Spring 会从三级缓存中获取工厂实例,然后通过工厂实例创建一个新的对象。这个新的对象在创建的过程中,会使用已经存在的对象实例进行注入,确保对象的依赖关系得以正确处理。

通过这样的机制,Spring 可以保证对象的创建和初始化的正确性,从而避免了半成品对象容易引发空指针的问题。