循环依赖

152 阅读8分钟

1.循环依赖的定义与解决方案:

循环依赖指的是多个Bean互相依赖,如A依赖B,B依赖A,A的属性注入时需要用到Bean B,这时去创建Bean B却又发现Bean B的属性注入需要用到Bean A,但是Bean A还在创建过程中,导致Bean B无法创建成功,从而导致一个死循环,Bean A和Bean B都处于实例化后阶段而无法继续进行属性注入。Spring对于循环依赖的解决方案是三级缓存(只针对于set方法注入的单例Bean的循环依赖)。

2.三级缓存:

  • 一级缓存:完全创建好的单例Bean的缓存
  • 二级缓存:早期暴露的Bean的缓存(指的是已经实例化但是还没有属性注入的正在创建中的Bean的缓存)
  • 三级缓存:FactoryBean的缓存,有一个getObject方法,用于获得早期暴露的Bean,获得后放入二级缓存(说一下FactoryBean,首先它是一个Bean,但又不仅仅是一个Bean。它是一个能生产或修饰对象生成的工厂Bean,类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候生产一个对象,且不仅仅限于它自身,它能返回任何Bean的实例。主要方法有getObject、getObjectType、isSingleton)
/** Cache of singleton objects: bean name --> bean instance */
/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256); 
/** Cache of early singleton objects: bean name --> bean instance */
/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */ 
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 
/** Cache of singleton factories: bean name --> ObjectFactory */ 
/** 三级级缓存:存放工厂 bean 对象,用于解决循环依赖 */ 
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

3.三级依赖解决循环依赖的逻辑:

简单来讲Bean的创建过程分为:Bean的实例化、Bean的属性注入、Bean初始化、Bean的销毁。

对于单例的Bean,在完全创建好也就是初始化后,会将其放入一级缓存中(用concurrentHashMap实现的),在需要使用的时候,会先去缓存中查找,找到直接调用(使用getSingleton()方法),找不到说明还未创建成功,就去创建一个。

分析 getSingleton() 的整个过程,可知三级缓存的使用过程如下

  1. Spring 会先从一级缓存 singletonObjects 中尝试获取 Bean。获取不到表示Bean未创建或正在创建中。
  2. 若是获取不到,而且对象正在建立中,就会尝试从二级缓存 earlySingletonObjects 中获取 Bean。
  3. 若还是获取不到,且允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象,就会尝试从三级缓存 singletonFactories 中获取 Bean。
  4. 若是在三级缓存中获取到了 Bean,会将该 Bean 存放到二级缓存中。

Spring的三级缓存解决循环依赖的逻辑就是先将正在创建中的Bean A,也就是实例化但未初始化的Bean A通过三级缓存的singletonFactories来获取到并缓存到二级缓存当中,这样在对Bean A进行属性注入的时候,去创建Bean B,在Bean B的属性注入时就可以顺利的将早期的Bean A注入到Bean B当中,从而创建好Bean B,并注入到Bean A当中,至此循环依赖得以解决。

4.具体流程:

  1. 使用Bean A,先调用getSingleton方法依次去三级缓存中找,发现缓存中不存在,则调用creatBean方法开始创建Bean A。
  2. 创建对象 A,完成实例化(Instantiation),在调用 createBeanInstance 方法实例化A后,会调用 addSingletonFactory 方法,将已实例化但未属性赋值未初始化的对象 A 放入三级缓存 singletonFactories 中。即将对象 A 提早曝光给 IoC 容器。
  3. 继续,执行对象 A 生命周期的第二步,即属性赋值(Populate)。此时,发现对象 A 依赖对象B,所以就会尝试去获取对象 B。
  4. 继续,发现 B 尚未创建,所以会执行创建对象 B 的过程。
  5. 在创建对象 B 的过程中,执行实例化(Instantiation)后将早期的B放到第三级缓存中,然后开始属性赋值,此时发现,对象 B 依赖对象 A。
  6. 继续,尝试在缓存中查找对象 A。先查找一级缓存,发现一级缓存中没有对象 A(因为对象 A 还未初始化完成);转而查找二级缓存,二级缓存中也没有对象 A(因为对象 A 还未属性赋值);转而查找三级缓存 singletonFactories,对象 B 可以通过 ObjectFactory.getObject 拿到对象 A,并将早期暴露的A放入二级缓存并移除三级缓存。然后将其返回给B。
  7. 继续,对象 B 在获取到对象 A 后,继续执行set方法属性赋值(Populate)和初始化(Initialization)操作。对象 B 完成初始化操作后,会被存放到一级缓存中并从移出二级和三级缓存中移除。
  8. 继续,转到「对象 A 执行属性赋值过程并发现依赖了对象 B」的场景。此时,对象 A 可以从一级缓存中获取到对象 B,所以可以顺利执行属性赋值操作。
  9. 继续,对象 A 执行初始化(Initialization)操作,完成后,会被存放到一级缓存中,并从二级和三级缓存中移除。

5.三级缓存无法解决以下的情况:

  1. 对于构造器注入的Bean 的循环依赖:对象的构造函数是在实例化阶段调用的。而三级缓存方案是在对象已经实例化但未属性注入时将其放入三级缓存。如new A(B),new B(A),这种情况是无解的,因为A和B都尚未实例化。所以三级依赖只能解决set方法注入的Bean的循环依赖。
  2. 对于prototype的Bean的循环依赖:Spring IoC 容器只会管理单例 Bean 的生命周期,并将单例 Bean 存放到缓存池中(三级缓存)。Spring 并不会管理 prototype 作用域的 Bean,也不会缓存该作用域的 Bean,而 Spring 中循环依赖的解决正是通过缓存来实现的。prototype的Bean是交由调用者管理的。非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

6.非单例Bean的循环依赖如何解决

  • 对于构造器注入产生的循环依赖,可以使用 @Lazy 注解,延迟加载。
  • 对于多例 Bean 和 prototype 作用域产生的循环依赖,可以尝试改为单例 Bean。

7.为什么一定要三级缓存,二级依赖可以吗?

第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。既然目的只是延迟的话,那么我们可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。

使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于 IOC 的考虑,而是出于 AOP 的考虑,即若使用二级缓存,在 AOP 情形注入到其他 Bean的,不是最终的代理对象,而是原始对象。

若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样的,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的和最终的bean不是同一个bean的,将产生意向不到的效果,而三级缓存就可以发现这个问题,会报错。