死磕Spring之IoC篇 - 单例 Bean 的循环依赖处理


该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读

Spring 版本:5.1.14.RELEASE

开始阅读这一系列文章之前,建议先查看《深入了解 Spring IoC(面试题)》这一篇文章

该系列其他文章请查看:《死磕 Spring 之 IoC 篇 - 文章导读》

单例 Bean 的循环依赖处理

我们先回到《Bean 的创建过程》中的“从缓存中获取单例 Bean”小节,当加载一个 Bean 时,会尝试从缓存(三个 Map)中获取对象,如果未命中则进入后面创建 Bean 的过程。再来看到《Bean 的创建过程》中的“提前暴露当前 Bean”小节,当获取到一个实例对象(还未设置属性和初始化)后,会将这个“早期对象”放入前面的缓存中(第三个 Map),这里暴露的对象实际是一个 ObjectFactory,可以通过它获取“早期对象”。这样一来,在后面设置属性的过程中,如果需要依赖注入其他 Bean,且存在循环依赖,那么上面的缓存就避免了这个问题。接下来,将会分析 Spring 处理循环依赖的相关过程。

这里的循环依赖是什么?

循环依赖,其实就是循环引用,就是两个或者两个以上的 Bean 互相引用对方,最终形成一个闭环,如 A 依赖 B,B 依赖 C,C 依赖 A。

例如定义下面两个对象:

学生类

public class Student {

    private Long id;

    private String name;

    @Autowired
    private ClassRoom classRoom;

    // 省略 getter、setter
}

教室类

public class ClassRoom {

    private String name;

    @Autowired
    private Collection<Student> students;
    
    // 省略 getter、setter
}

当加载 Student 这个对象时,需要注入一个 ClassRoom 对象,就需要去加载 ClassRoom 这个对象,此时又要去依赖注入所有的 Student 对象,这里的 Student 和 ClassRoom 就存在循环依赖,那么一直这样循环下去,除非有终结条件

Spring 只处理单例 Bean 的循环依赖,原型模式的 Bean 如果存在循环依赖直接抛出异常单例 Bean 的循环依赖的场景有两种:

  • 构造器注入出现循环依赖
  • 字段(或 Setter)注入出现循环依赖

对于构造器注入出现缓存依赖,Spring 是无法解决的,因为当前 Bean 还未实例化,无法提前暴露对象,所以只能抛出异常,接下来我们分析的都是字段(或 Setter)注入出现循环依赖的处理

循环依赖的处理

1. 尝试从缓存中获取单例 Bean

可以先回到《Bean 的创建过程》中的“从缓存中获取单例 Bean”小节,在获取一个 Bean 过程中,首先会从缓存中尝试获取对象,对应代码段:

// AbstractBeanFactory#doGetBean(...) 方法
Object sharedInstance = getSingleton(beanName);

// DefaultSingletonBeanRegistry.java
public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}
// DefaultSingletonBeanRegistry.java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // <1> **【一级 Map】**从单例缓存 `singletonObjects` 中获取 beanName 对应的 Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // <2> 如果**一级 Map**中不存在,且当前 beanName 正在创建
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // <2.1> 对 `singletonObjects` 加锁
        synchronized (this.singletonObjects) {
            // <2.2> **【二级 Map】**从 `earlySingletonObjects` 集合中获取,里面会保存从 **三级 Map** 获取到的正在初始化的 Bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            // <2.3> 如果**二级 Map** 中不存在,且允许提前创建
            if (singletonObject == null && allowEarlyReference) {
                // <2.3.1> **【三级 Map】**从 `singletonFactories` 中获取对应的 ObjectFactory 实现类
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                // 如果从**三级 Map** 中存在对应的对象,则进行下面的处理
                if (singletonFactory != null) {
                    // <2.3.2> 调用 ObjectFactory#getOject() 方法,获取目标 Bean 对象(早期半成品)
                    singletonObject = singletonFactory.getObject();
                    // <2.3.3> 将目标对象放入**二级 Map**
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // <2.3.4> 从**三级 Map**移除 `beanName`
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    // <3> 返回从缓存中获取的对象
    return singletonObject;
}

这里的缓存指的就是上面三个 Map 对象:

  • singletonObjects(一级 Map):里面保存了所有已经初始化好的单例 Bean,也就是会保存 Spring IoC 容器中所有单例的 Spring Bean
  • earlySingletonObjects(二级 Map),里面会保存从 三级 Map 获取到的正在初始化的 Bean
  • singletonFactories(三级 Map),里面保存了正在初始化的 Bean 对应的 ObjectFactory 实现类,调用其 getObject() 方法返回正在初始化的 Bean 对象(仅实例化还没完全初始化好)

过程如下:

  1. 【一级 Map】从单例缓存 singletonObjects 中获取 beanName 对应的 Bean
  2. 如果一级 Map中不存在,且当前 beanName 正在创建
    1. singletonObjects 加锁
    2. 【二级 Map】earlySingletonObjects 集合中获取,里面会保存从 三级 Map 获取到的正在初始化的 Bean
    3. 如果二级 Map 中不存在,且允许提前创建
      1. 【三级 Map】singletonFactories 中获取对应的 ObjectFactory 实现类,如果从三级 Map 中存在对应的对象,则进行下面的处理
      2. 调用 ObjectFactory#getOject() 方法,获取目标 Bean 对象(早期半成品)
      3. 将目标对象放入二级 Map
      4. 三级 Map移除 beanName
  3. 返回从缓存中获取的对象

2. 提前暴露当前 Bean

回到《Bean 的创建过程》中的“提前暴露当前 Bean”小节,在获取到实例对象后,如果是单例模式,则提前暴露这个实例对象,对应代码段:

// AbstractAutowireCapableBeanFactory#doCreateBean(...) 方法
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// <3> 提前暴露这个 `bean`,如果可以的话,目的是解决单例模式 Bean 的循环依赖注入
// <3.1> 判断是否可以提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() // 单例模式
        && this.allowCircularReferences // 允许循环依赖,默认为 true
        && isSingletonCurrentlyInCreation(beanName)); // 当前单例 bean 正在被创建,在前面已经标记过
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    /**
     * <3.2>
     * 创建一个 ObjectFactory 实现类,用于返回当前正在被创建的 `bean`,提前暴露,保存在 `singletonFactories` (**三级 Map**)缓存中
     *
     * 可以回到前面的 {@link AbstractBeanFactory#doGetBean#getSingleton(String)} 方法
     * 加载 Bean 的过程会先从缓存中获取单例 Bean,可以避免单例模式 Bean 循环依赖注入的问题
     */
    addSingletonFactory(beanName,
            // ObjectFactory 实现类
            () -> getEarlyBeanReference(beanName, mbd, bean));
}

如果是单例模式允许循环依赖(默认为 true)、当前单例 Bean 正在被创建(前面已经标记过),则提前暴露

这里会先通过 Lambda 表达式创建一个 ObjectFactory 实现类,如下:

// AbstractAutowireCapableBeanFactory.java
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() // RootBeanDefinition 不是用户定义的(由 Spring 解析出来的)
        && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

入参 bean 为当前 Bean 的实例对象(未初始化),这个实现类允许通过 SmartInstantiationAwareBeanPostProcessor 对这个提前暴露的对象进行处理,最终会返回这个提前暴露的对象。注意,这里也可以返回一个代理对象。

有了这个 ObjectFactory 实现类后,就需要往缓存中存放了,如下:

// DefaultSingletonBeanRegistry.java
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

可以看到会将这个 ObjectFactory 往 singletonFactories (三级 Map)中存放,到这里对于 Spring 对单例 Bean 循环依赖的处理是不是就非常清晰了

3. 缓存单例 Bean

在完全初始化好一个单例 Bean 后,会缓存起来,如下:

// DefaultSingletonBeanRegistry.java
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

singletonObjects(一级 Map)存放当前单例 Bean,同时从 singletonFactories(三级 Map)和 earlySingletonObjects(二级 Map)中移除

总结

Spring 只处理单例 Bean 的字段(或 Setter)注入出现循环依赖,对于构造器注入出现的循环依赖会直接抛出异常。还有就是如果是通过 denpends-on 配置的依赖出现了循环,也会抛出异常,所以我觉得这里的“循环依赖”换做“循环依赖注入”是不是更合适一点

Spring 处理循环依赖的解决方案如下:

  • Spring 在创建 Bean 的过程中,获取到实例对象后会提前暴露出去,生成一个 ObjectFactory 对象,放入 singletonFactories(三级 Map)中
  • 在后续设置属性过程中,如果出现循环,则可以通过 singletonFactories(三级 Map)中对应的 ObjectFactory#getObject() 获取这个早期对象,避免再次初始化

问题一:为什么需要上面的 二级 Map

因为通过 三级 Map获取 Bean 会有相关 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 的处理,避免重复处理,处理后返回的可能是一个代理对象

例如在循环依赖中一个 Bean 可能被多个 Bean 依赖, A -> B(也依赖 A) -> C -> A,当你获取 A 这个 Bean 时,后续 B 和 C 都要注入 A,没有上面的 二级 Map的话,三级 Map 保存的 ObjectFactory 实现类会被调用两次,会重复处理,可能出现问题,这样做在性能上也有所提升

问题二:为什么不直接调用这个 ObjectFactory#getObject() 方法放入 二级Map中,而需要 三级 Map

对于没有不涉及到 AOP 的 Bean 确实可以不需要 singletonFactories(三级 Map),但是 Spring AOP 就是 Spring 体系中的一员,如果没有singletonFactories(三级 Map),意味着 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 的设计原则。Spring 是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器在完全创建好 Bean 后来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。如果出现了循环依赖,那没有办法,只有给 Bean 先创建代理对象,但是在没有出现循环依赖的情况下,设计之初就是让 Bean 在完全创建好后才完成 AOP 代理。

本站声明:网站内容来源于网络,如有侵权,请联系我们,我们将及时处理。

  • 分享:
评论
还没有评论
    发表评论 说点什么