Spring处理循环依赖为什么使用三级缓存而不是二级缓存,以及二级缓存的作用?

  • Post author:
  • Post category:其他


循环依赖定义


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 的生命周期

  1. 实例化,createBeanInstance,就是 new 了个对象
  2. 属性注入,populateBean, 就是 set 一些属性值
  3. 初始化,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);

三级缓存介绍

  1. 一级缓存(singletonObjects), 存放的是

单例对象缓存池,存放的 Bean 已经实例化、属性赋值、完全初始化好(成品)

  1. 二级缓存(earlySingletonObjects),存放的是

早期单例对象缓存池,存放的 Bean 已经实例化但尚未属性赋值、未执行 init 方法(半成品), 用于解决循环依赖

  1. 三级缓存(singletonFactories),存放

单例工厂的缓存, 用于解决循环依赖

三级缓存的使用过程如下

  1. Spring 会先从一级缓存 singletonObjects 中尝试获取 Bean。
  2. 若是获取不到,而且对象正在建立中,就会尝试从二级缓存 earlySingletonObjects 中获取 Bean。
  3. 若还是获取不到,且允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象,就会尝试从三级缓存 singletonFactories 中获取 Bean。
  4. 若是在三级缓存中获取到了 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 以后

  1. 此时,单例

    Bean 对象已经实例化

    (可以通过对象引用定位到堆中的对象),

    但尚未属性赋值和初始化

  2. 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;
    }

}

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

为什么要包装一个ObjectFactory对象

如果创建的Bean有对应的aop代理,那其他对象注入时,注入的应该是对应的代理对象;


「但是Spring无法提前知道这个对象是不是有循环依赖的情况」

,而正常情况下(没有循环依赖情况),Spring都是在对象初始化后才创建对应的代理。这时候Spring有两个选择:


  1. 不管有没有循环依赖,实例化后就直接创建好代理对象


    ,并将代理对象放入缓存

    ,出现循环依赖时,其他对象直接就可以取到代理对象并注入(

    只需要2级缓存

    ,singletonObjects和earlySingletonObjects即可)

  2. 「不提前创建好代理对象,


    在出现循环依赖被其他对象注入时,才提前生成代理对象(此时只完成了实例化


    )。这样在没有循环依赖的情况下,Bean还是在初始化完成才生成代理对象」

    (需要3级缓存)


因此,3级缓存主要是为了正常情况下,代理对象能在初始化完成后生成,而不用提前生成」

为什么需要二级缓存

一般来讲,只用了一个map(即

single


tonObjects,一级缓存

)就可以实现循环依赖,但这种实现有个问题,

在某一时刻

singletonObjects中的类有可能只是完成了实例化,并没有完成初始化。

解释一下

singletonObjects中存在未初始化对象的问题:因为某一个时刻我从

singletonObjects拿到的如果是未初始化的对象,然后我(可以是框架,或者其他业务方)需要对其进行操作,那么很可能会出错。 所以是不能容忍的。


而在spring中singletonObjects中的类都完成了初始化

,因为我们取单例Bean的时候都是从singletonObjects中取的,不可能让我们获取到没有初始化完成的对象。

所以需要我们提供另一级缓存,用来区分对象是否完成了初始化,可以提供给用户使用。

Spring为何不能解决非单例Bean的循环依赖

这个问题可以细分为下面几个问题

  1. 为什么不能解决构造器的循环依赖?
  2. 为什么不能解决 prototype 作用域循环依赖?
  3. 为什么不能解决多例的循环依赖?

Spring 为什么不能解决构造器的循环依赖

对象的构造函数是在实例化阶段调用的。

首先明确下,在 Spring 中创建 Bean 分三步:

  1. 实例化,createBeanInstance,就是 new 了个对象
  2. 属性注入,populateBean, 就是 set 一些属性值
  3. 初始化,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 初始化完成之后才为其创建代理





版权声明:本文为zekser原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。