Spring如何解决循环依赖——Spring IOC系列(三)

170 阅读10分钟

一、问题

本文我们讨论下常见的八股文,Spring如何解决循环依赖。其实我们之前的两篇文章已经解释过了,本文进一步详细探讨一下。

🤔什么是循环依赖?

比如我们有两个类A/B,A对B有依赖,也就是A有个属性为B,而B对A也有依赖,这就是循环依赖。也可以更复杂,比如A -> B -> C -> A这种三个类的,当然也可以更多类形成这种“套圈”的依赖,都属于循环依赖。

🤔有什么问题

回顾一下Spring Bean的创建流程,大概为:

  1. 实例化(createBeanInstance)
  2. 依赖注入(populateBean)
  3. 初始化(initializeBean)
  4. 注册销毁方法(registerDisposableBeanIfNecessary)

如果Spring没有解决循环依赖,那么假设A/B循环依赖后:

  1. A实例化完成后,进行依赖注入,从BeanFactory中查找B,触发B的创建
  2. B实例化,进行依赖注入,从BeanFactory中查找A,触发A的创建
  3. 触发B创建,实例化。。
  4. 触发A创建,实例化。。
  5. 。。。

完犊子了,死循环了。

🤔如何解决?

如果是手动编码,该如何解决呢,以最简单的A/B互相依赖为例,我们可以这样解决。

A a = new A();
B b = new B();
a.setB(b);
b.setA(a);

Spring的解决方案也类似,实例化A对象后,先把这个还没有创建完成的“半成品”(也就是还没有执行属性注入,初始化等后续流程)放到map中缓存起来。这样,创建B之后,在B注入属性时,就可以先把这个“半成品”,注入到B中,也就破除了死循环。不过因为Spring的动态代理问题,实际实现上,多使用了一个map。

二、源码

在阅读源码前,我们先梳理一下骨架。Spring使用了三个map管理单例的bean,也就是著名的三级缓存。分别是 (出自DefaultSingletonBeanRegistry) :

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
  • singletonObjects,存放完整单实例Bean的map,也就是所谓的“一级缓存”。
  • singletonFactories,半成品的单实例对象工厂的map,也就是所谓的“三级缓存”。
  • earlySingletonObjects,提前暴露的半成品单实例bean,这里的提前(early),就是因为循环依赖而提前暴露的bean,会从singletonFactories获取工厂,调用工厂方法获取bean,再放到这个map中。这个map,就是所谓的“二级缓存”。

🤔什么是半成品bean?

还没有完成属性注入和初始化等创建流程的bean,详细的bean创建流程,可以看第一章。

假设A/B出现了循环依赖,并且A先初始化,那么在Spring中,创建的流程为:

  1. A实例化,实例化后,使用半程品的A对象,构造工厂,放入工厂map(singletonFactories)中。
  2. A依赖注入,触发B的创建。
    1. B实例化,使用半程品的B对象,构造工厂,放入工厂map(singletonFactories)中。
    2. B依赖注入,触发A的创建。A创建前,会去工厂map检查,是否存在
      1. 存在,获取工厂,执行工厂方法,获取提前暴露的bean。
      2. 工厂方法执行过程中,会调用SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法,比如AOP,会在这里进行提前代理。
      3. 把提前暴露的bean,放入到提前暴露的map(earlySingletonObjects)中。
    3. 获取到A的提前暴露的对象,注入B中。
    4. B执行后续的初始化等流程,完成完整的bean创建流程后,把完整对象放到单实例map(singletonObjects)中,并在另外两个map中移除对象。
  3. A成功获取到B,进行依赖注入以及后续初始化流程。
  4. 创建完成后,把完整对象放到单实例map(singletonObjects)中,并在另外两个map中移除对象。

以上就是Spring解决循环依赖的大致流程,下面我们看下源码。

2.1 缓存半成品工厂

在Bean创建完成后,会有这样一段代码(出自AbstractAutowireCapableBeanFactory#doCreateBean):

// 单例 && 允许循环依赖 && 该单例正在创建,则使用该单例,构造一个ObjectFactory<?>,放入工厂map中。
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    if (logger.isTraceEnabled()) {
        logger.trace("Eagerly caching bean '" + beanName +
                "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

addSingletonFactory源码,也很简单,不再赘述。

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);
        }
    }
}

相对的,在bean注入属性,初始化完成后,会把该半成品移除掉,同时把完整的bean放入的singletonObjects中。(出自DefaultSingletonBeanRegistry#addSingleton

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);
    }
}

调用链为:getBean -> doGetBean -> getSingleton -> addSingleton。

2.2 发现循环依赖

注入属性时,会通过BeanFactory的getBean方法,获取依赖,如果是单例,会尝试先获取单例缓存。最终调用getSingleton方法(出自DefaultSingletonBeanRegistry)。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 尝试在完整的单例map中获取bean,成功直接返回
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        //没有完整bean获取到,并且该bean正在创建,则尝试从提前暴露的半成品map中获取bean
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            //没有获取到提前暴露的半成品,则从工厂map中获取工厂
            synchronized (this.singletonObjects) {
                // 经典的并发double check,尽量减少加锁可能
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        //拿到了工厂,则调用工厂方法
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

梳理一下:

  1. 从三个map中依次查找,比较简单。
  2. 经典的double check方式加锁,尽量减少加锁可能。

2.3 触发getEarlyBeanReference

如果从工厂map中获取到了工厂,则调用工厂方法获取对象,这个工厂方法,即上一步中放进来的那个lamba表达式:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

也就是调用了这个方法,执行SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法。

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

这里就解释了,为什么需要三个map了,需要在存在循环依赖的场景下,bean被提前暴露时,提供一个扩展能力。也就是,在此种场景下,会执行这个SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法。

那么这个扩展能力谁实现了呢,对,就是Spring AOP功能(在上一篇文章中介绍过)。代码出自AbstractAutoProxyCreator。

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

简单来说,就是如果发生了循环依赖,需要提前暴露,那么AOP,需要提前做动态代理。否则注入到其他bean的,就是一个未代理对象。

三、扩展

3.1 构造器构造

3.1.1 都是构造器依赖

之前,都采用属性方式注入,使用上述方式解决循环依赖。那么假如是构造器方式依赖呢,也就是这样:

@Component
public class A {

    private B b;

    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {

    private A a;

    public B(A a) {
        this.a = a;
    }
}

这种似乎无解,手动构造都无法成功。额,其实还真有解,使用@Lazy,像这样,注意一定是放在构造方法的参数里,而不是放在类上。

@Component
public class A {

    private B b;

    public A(@Lazy B b) {
        this.b = b;
    }
}

在这种情况下,构造A的时候,并没有真正实例化B,而是使用代理类包装起来,用到的时候,才会实例化。也就是破坏了循环依赖,构造时只有B依赖A,而A没有真正的依赖B。

3.1.2 一个构造器,一个非构造器

还有种场景,一个是构造器,另一个不是:

@Component
public class A {

    private B b;

    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {
    @Resource
    private A a;
}

在不指定两者的构造顺序情况下,可能成功,也可能失败。既然不指定,那么Spring并没有保证,哪个bean会先初始化。

如果是先A,后B,那么:

  1. 实例化A,仅有一个构造器,那么使用该构造器,寻找依赖,也就是B,触发B的创建。
  2. 实例化B
    1. 实例化完成后,构造工厂,放到工厂map中。
    2. 依赖注入,触发创建A,然而,A还没有实例化完成。。。

这时,你会收到一个异常,总之,就是发现了循环依赖,无法解决。

Exception encountered during context initialization - cancelling refresh attempt: 
    org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a' defined in class path resource [com/hellohu/spring/bean/annotation/A.class]: Unsatisfied dependency expressed through constructor parameter 0; 
        nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

反之,如果先B后A,那么:

  1. 实例化B,实例化后包装成工厂,放入工厂map中。
  2. 依赖注入,寻找依赖,也就是A,触发A的创建。
  3. 实例化A
    1. 仅有一个构造器,那么使用该构造器,寻找依赖,也就是B,触发B的创建。
      1. 从工厂map中获取工厂,提前暴露。。。

不再向下推演,你也可以发现,循环依赖解决了。由于Spring并不会保证初始化顺序,你极有可能本来运行好好的,突然有一天,你增改了某些无关的代码,就突然完犊子了。

🤔该怎么解决呢?

很简单

  1. 不采用构造器的方式依赖,使用属性或者setter方法依赖。
  2. 固定创建顺序,比如使用@DependsOn注解,指定A依赖B,以达到先初始化A的目的。
  3. 也可以使用@Lazy,破坏循环依赖

使用@DependsOn

@Component
@DependsOn("b")
public class A {
    ...
}

使用@Lazy

@Component
public class A {
    public A(@Lazy B b) {
        this.b = b;
    }
}

3.2 多例

之前,都是单例的场景,其实从源码上你也能看出来,所谓的三级缓存,都是存储单例对象(非常合理,多例也不能缓存啊)。

3.2.1 多例互相依赖

如果A/B都是多例的呢,也就是这样:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class A {

    @Resource
    private B b;
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class B {

    @Resource
    private A a;
}

很显然,正常情况下,Spring是没有办法解决这种循环依赖的。因为多例就不会进入缓存,也就没有办法解决。但也不是完全没办法,可以使用@Lazy,注意是加到属性上。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class A {

    @Resource
    @Lazy
    private B b;
}

本质上还是使用@Lazy破坏循环依赖。不过这里还是有问题,我们为A/B增加空的构造方法,创建时打印一下。类似这样

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class A {

    @Resource
    @Lazy
    private B b;

    public A() {
        System.out.println("a create");
    }

    public void useB(){
        System.out.println(b.toString());
    }
}

@Component
public class B {

    private A a;

    public B(A a) {
        this.a = a;
        System.out.println("b create");
    }
}

测试代码:

try (ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("spring/beansAutowire.xml");) {
    A a= (A) ctx.getBean("a");
    a.useB();
}

输出:

a create
b create
a create

发现了问题了吧,A->B->A,这里的A是两个对象,仔细想想,也没啥问题,但又好像有问题。解决了,又好像没解决(看似说了什么,又好像啥也没说)。

如果真想创建两个互相依赖的多实例Bean,直接手动创建吧,像这样:

try (ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("spring/beansAutowire.xml");) {
    A a= (A) ctx.getBean("a");
    B a= (B) ctx.getBean("a");
    a.setB(b);
    b.setA(a);
}

3.2.1 单例多例互相依赖

如果是一个单例,一个多例呢?

@Component
public class A {

    @Resource
    private B b;

    public A() {
        System.out.println("a create");
    }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class B {

    @Resource
    private A a;

    public B() {
        System.out.println("b create");
    }
}

这种创建不会有问题,因为ApplicationContext在初始化的时候,会把所有的单实例bean都初始化。也就是说,一定是先创建单例,也就是会进入所谓的“缓存”,也就可以解决循环依赖。但这里还是有一个问题,是个哲学问题。

单实例Bean为啥用引用多实例的Bean呢,单实例仅创建一次,那么注入的B也仅创建一次,也就是说,B在A中,某种意义上也是单实例的,是不是哪里不对劲。

此时,可以使用@Lookup注解,这里不做介绍了,Spring 常用注解之@Lookup