循环依赖定义
Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成
Spring有四种循环依赖场景:
1/2: 构造器的循环依赖(singleton 和 prototype)
3/4: 属性的循环依赖(singleton 和 prototype)
spring目前只支持singleton类型的属性循环依赖
构造器循环依赖
构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入。
@Autowired public ConstructorB(@Lazy ConstructorA constructorA) { this.constructorA = constructorA; }
属性的循环依赖
Spring Bean 的生命周期
- 实例化,createBeanInstance,就是 new 了个对象
- 属性注入,populateBean, 就是 set 一些属性值
- 初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等
首先我们明确下不同阶段概念上的区别:
实例化
:调用构造函数将对象创建出来的阶段
属性赋值
:实例化之后,给对象的属性赋值阶段
三级缓存
Spring 中,singleton Bean 在创建后会被放入 IoC 容器的缓存池中,并触发 Spring 对该 Bean 的生命周期管理
单例模式下,在第一次使用 Bean 时,会创建一个 Bean 对象并放入 IoC 容器的缓存池中。后续再使用该 Bean 对象时,会直接从缓存池中获取。
Spring中保存单例模式 Bean 的缓存池,采用了三级缓存设计,如下代码所示
/** 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);
三级缓存介绍
- 一级缓存(singletonObjects), 存放的是
单例对象缓存池,存放的 Bean 已经实例化、属性赋值、完全初始化好(成品)
- 二级缓存(earlySingletonObjects),存放的是
早期单例对象缓存池,存放的 Bean 已经实例化但尚未属性赋值、未执行 init 方法(半成品), 用于解决循环依赖
- 三级缓存(singletonFactories),存放
单例工厂的缓存, 用于解决循环依赖
三级缓存的使用过程如下
- Spring 会先从一级缓存 singletonObjects 中尝试获取 Bean。
- 若是获取不到,而且对象正在建立中,就会尝试从二级缓存 earlySingletonObjects 中获取 Bean。
- 若还是获取不到,且允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象,就会尝试从三级缓存 singletonFactories 中获取 Bean。
- 若是在三级缓存中获取到了 Bean,会将该 Bean 存放到二级缓存中。
第三级缓存为什么可以解决循环依赖
Spring 解决循环依赖的诀窍就在于 singletonFactories 这个三级缓存。 三级缓存中使用到了ObjectFactory 接口,定义如下
public interface ObjectFactory<T> { T getObject() throws BeansException; }
在 Bean 建立过程当中,有两处比较重要的匿名内部类实现了该接口。一处是 Spring 利用其建立 Bean 的时候,另外一处就是在 addSingletonFactory 方法中,如下代码所示
addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { return getEarlyBeanReference(beanName, mbd, bean); } });
此处就是解决循环依赖的关键,这段代码发生在 createBeanInstance 以后
-
此时,单例
Bean 对象已经实例化
(可以通过对象引用定位到堆中的对象),
但尚未属性赋值和初始化
。 - Spring 会将该状态下的 Bean 存放到三级缓存中,提早曝光给 IoC 容器(“提早”指的是不必等对象完成属性赋值和初始化后再交给 IoC 容器)。也就是说,可以在三级缓存 singletonFactories 中找到该状态下的 Bean 对象。
解决循环依赖示例分析
public class DependencyDemoV3 { private static final Map<String, Object> singletonObjects = new HashMap<>(); private static final Map<String, Object> earlySingletonObjects = new HashMap<>(); private static final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(); @SneakyThrows public static <T> T getBean(Class<T> beanClass) { String beanName = beanClass.getSimpleName(); if (singletonObjects.containsKey(beanName)) { return (T) singletonObjects.get(beanName); } if (earlySingletonObjects.containsKey(beanName)) { return (T) earlySingletonObjects.get(beanName); } ObjectFactory<?> singletonFactory = singletonFactories.get(beanName); if (singletonFactory != null) { // 这里会在被循环引入时被调用,调用的是下面 从三级缓存获取代理对象,且将代理对象放入二级缓存; // 也就是说代理对象的初始化,在循环依赖的场景下被提前了。 // 不然非循环依赖的话,第一次获取该对象结束后,他就已经被放到一级缓存里使用了 return (T) singletonFactory.getObject(); } // 实例化bean,放入三级缓存 Object object = beanClass.getDeclaredConstructor().newInstance(); // 这里代表第一次获取,只是往三级缓存放进了一个方法(返回自身代理对象的方法,添加进二级缓存,删除三级缓存内容) singletonFactories.put(beanName, () -> { Object proxy = createProxy(object); singletonFactories.remove(beanName); earlySingletonObjects.put(beanName, proxy); return proxy; }); // 开始初始化bean,即填充属性 Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); // 获取需要注入字段的class Class<?> fieldClass = field.getType(); field.set(object, getBean(fieldClass)); } createProxy(object); singletonObjects.put(beanName, object); singletonFactories.remove(beanName); earlySingletonObjects.remove(beanName); return (T) object; } public static Object createProxy(Object object) { // 因为这个方法有可能被执行2次,所以这里应该有个判断 // 如果之前提前进行过aop操作则直接返回需要aop的话则返回代理对象,否则返回传入的对象 return object; } }
- 创建对象 A,完成生命周期的第一步,即实例化(Instantiation),在调用 createBeanInstance 方法后,会调用 addSingletonFactory 方法,将已实例化但未属性赋值未初始化的对象 A 放入三级缓存 singletonFactories 中。即将对象 A 提早曝光给 IoC 容器。
- 继续,执行对象 A 生命周期的第二步,即属性赋值(Populate)。此时,发现对象 A 依赖对象,所以就会尝试去获取对象 B。
- 继续,发现 B 尚未创建,所以会执行创建对象 B 的过程。
- 在创建对象 B 的过程中,执行实例化(Instantiation)和属性赋值(Populate)操作。此时发现,对象 B 依赖对象 A。
- 继续,尝试在缓存中查找对象 A。先查找一级缓存,发现一级缓存中没有对象 A(因为对象 A 还未初始化完成);转而查找二级缓存,二级缓存中也没有对象 A(因为对象 A 还未属性赋值);转而查找三级缓存 singletonFactories,对象 B 可以通过 ObjectFactory.getObject 拿到对象 A。
- 继续,对象 B 在获取到对象 A 后,继续执行属性赋值(Populate)和初始化(Initialization)操作。对象 B 完成初始化操作后,会被存放到一级缓存中。
- 继续,转到「对象 A 执行属性赋值过程并发现依赖了对象 B」的场景。此时,对象 A 可以从一级缓存中获取到对象 B,所以可以顺利执行属性赋值操作。
- 继续,对象 A 执行初始化(Initialization)操作,完成后,会被存放到一级缓存中。
为什么要包装一个ObjectFactory对象
如果创建的Bean有对应的aop代理,那其他对象注入时,注入的应该是对应的代理对象;
「但是Spring无法提前知道这个对象是不是有循环依赖的情况」
,而正常情况下(没有循环依赖情况),Spring都是在对象初始化后才创建对应的代理。这时候Spring有两个选择:
-
不管有没有循环依赖,实例化后就直接创建好代理对象
,并将代理对象放入缓存
,出现循环依赖时,其他对象直接就可以取到代理对象并注入(
只需要2级缓存
,singletonObjects和earlySingletonObjects即可) -
「不提前创建好代理对象,
在出现循环依赖被其他对象注入时,才提前生成代理对象(此时只完成了实例化
)。这样在没有循环依赖的情况下,Bean还是在初始化完成才生成代理对象」
(需要3级缓存)
因此,3级缓存主要是为了正常情况下,代理对象能在初始化完成后生成,而不用提前生成」
为什么需要二级缓存
一般来讲,只用了一个map(即
single
tonObjects,一级缓存
)就可以实现循环依赖,但这种实现有个问题,
在某一时刻
singletonObjects中的类有可能只是完成了实例化,并没有完成初始化。
解释一下
singletonObjects中存在未初始化对象的问题:因为某一个时刻我从
singletonObjects拿到的如果是未初始化的对象,然后我(可以是框架,或者其他业务方)需要对其进行操作,那么很可能会出错。 所以是不能容忍的。
而在spring中singletonObjects中的类都完成了初始化
,因为我们取单例Bean的时候都是从singletonObjects中取的,不可能让我们获取到没有初始化完成的对象。
所以需要我们提供另一级缓存,用来区分对象是否完成了初始化,可以提供给用户使用。
Spring为何不能解决非单例Bean的循环依赖
这个问题可以细分为下面几个问题
- 为什么不能解决构造器的循环依赖?
- 为什么不能解决 prototype 作用域循环依赖?
- 为什么不能解决多例的循环依赖?
Spring 为什么不能解决构造器的循环依赖
对象的构造函数是在实例化阶段调用的。
首先明确下,在 Spring 中创建 Bean 分三步:
- 实例化,createBeanInstance,就是 new 了个对象
- 属性注入,populateBean, 就是 set 一些属性值
- 初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等
明确了上面这三点,再来来理一下。
如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B 。
但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。
为什么找不到?因为 A 还没 new 完呢,所以找到不完整的 A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖。
同时提一下:
一个set注入,一个构造器注入一定能成功?
- 如果循环依赖都是构造器注入,则失败
- 如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟BeanName的字母序有关系。
为什么需要三级缓存,二级缓存不行嘛
三级缓存的意义
测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存,使用三级缓存呢?
如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理
。