Spring的循环依赖与三级缓存


我们都知道Spring通过三级缓存来解决循环依赖的问题,那么是不是必须是三级缓存?二级缓存不能解决吗?先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成,这个都不陌生。Spring的循环依赖有2种场景

  1. 构造器的循环依赖(singleton,prototype)
  2. 属性的循环依赖(singleton,prototype)

spring目前只能解决singleton类型的属性循环依赖,而构造函数的循环依赖Spring无法解决。构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
     this.constructorA = constructorA;
}

但这种方式不被推荐,在开始后面的内容的时候,我们先明确2个概念

  1. 实例化:调用构造函数将对象创建出来,半成品Bean
  2. 初始化:调用构造函数将对象创建出来后,对象的属性也被赋值,成品Bean

Spring解决循环依赖的核心思想在于提前曝光,在Spring的源码类 DefaultSingletonBeanRegistry 中定义了三级缓存

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

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);


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

关于三级缓存的说明如下

缓存 说明
singletonObjects 一级缓存,存放成品Bean
earlySingletonObjects 二级缓存,存放半成品Bean
singletonFactories 三级缓存,存放Bean工厂对象,用来生成半成品Bean

Spring从缓存中取对象的源码

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    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);
            }
        }
    }
}
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

从三个缓存的作用来看,在A依赖B,B依赖A的场景:先生成A的半成品放到二级缓存,然后去生成B,这时候B可以在二级缓存找到A,然后B初始化完成,把成品B放到一级缓存,这时再去检查一下二级缓存,发现A还没有初始化,但可以在一级缓存中找到它依赖的B,于是A也可以初始化完成,似乎两级缓存也能解决循环依赖的问题,那为什么要包装一个ObjectFactory对象?

如果创建的Bean有对应的aop代理,那其他对象注入时,注入的应该是对应的代理对象,「但是Spring无法提前知道这个对象是不是有循环依赖的情况」,而正常情况下(没有循环依赖情况),Spring都是在对象初始化后才创建对应的代理。这时候Spring有两个选择:

  1. 不管有没有循环依赖,实例化后就直接创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入(只需要2级缓存,singletonObjects和earlySingletonObjects即可)
  2. 不提前创建好代理对象,在出现循环依赖被其他对象注入时,才提前生成代理对象(此时只完成了实例化)。这样在没有循环依赖的情况下,Bean还是在初始化完成才生成代理对象(需要3级缓存)

Spring选择了第二种方式「所以到现在为止你知道3级缓存的作用了吧,主要是为了正常情况下,代理对象能在初始化完成后生成,而不用提前生成」如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。提前生成代理对象违背了Spring的设计初衷

前面说到Sping选择了第二种,如果是第一种,就会有以下不同的处理逻辑:

  1. 在提前曝光半成品时,直接执行getEarlyBeanReference创建到代理,并放入到缓存earlySingletonObjects中。
  2. 有了上一步,那就不需要通过ObjectFactory来延迟执行getEarlyBeanReference,也就不需要singletonFactories这一级缓存。

这种处理方式可行吗?

这里做个试验,对AbstractAutowireCapableBeanFactory做个小改造,在放入三级缓存之后立刻取出并放入二级缓存,这样三级缓存的作用就完全被忽略掉,就相当于只有二级缓存。

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
            throws BeanCreationException {            
        ……
        // 是否提前曝光
        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));
            // 立刻从三级缓存取出放入二级缓存
            getSingleton(beanName, true);
        }
        ……
    }   
}

测试结果是可以的,并且从源码上分析可以得出两种方式性能是一样的,并不会影响到Sping启动速度。

总结

只使用二级缓存可以解决循环依赖的问题,Spring为什么用三级缓存?从设计角度出发,只使用二级缓存需要提前生成代理对象,违背了设计原则。所以使用了三级缓存


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
云计算与虚拟化随笔 云计算与虚拟化随笔
对于微服务技术栈我比较熟悉的是Spring Cloud,最近公司在各应用升级到K8S部署。虽然对Docker与K8S我有所了解,但是一直没有深入的使用过。任何框架或者中间件的登场和消亡都有其历史原因,从历史角度会对各技术的演变有更深刻的理解
2022-04-10
Next 
时间轮算法 时间轮算法
时间轮算法在很多开源的中间件被广泛应用, Redisson 分布式锁中使用时间轮算法实现看门狗,来为锁续时。另一个场景是使用时间轮算法来实现心跳续时,在使用长连接的时候,我们通常需要使用心跳机制来防止意外断开的无效连接浪费系统资源。心跳机制
2022-04-05
  TOC