Spring 的循环依赖问题

摘要

        在 Spring 框架中,循环依赖是指两个或多个 bean 之间相互依赖,形成一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。那么 Spring 提供了什么方式来解决循环依赖,这个方式解决循环依赖的原理又是如何?本篇文章就是回答这两个问题。

关键词:循环依赖、三级缓存、Spring

1、引言

        在 Spring 中为了解决循环依赖问题,其采用的方案是引入了三级缓存。但是 Spring 解决循环依赖是有一定限制的,一点就是要求相互依赖的 bean 必须要是单例的 bean,另一点就是依赖注入的方式不能都是构造函数注入的方式。那么什么是三级缓存,又为什么 Spring 解决循环依赖会有一定限制呢?相信通过接下来的讲解就会豁然开朗。

图1-1 循环依赖举例

2、相关工作

2.1、三级缓存

        Spring 的三级缓存机制是 Spring 框架为了解决循环依赖和优化 Bean 的创建过程而设计的一种高效缓存策略。这一机制主要涉及到三个缓存级别,分别是一级缓存 singletonObjects、二级缓存 earlySingletonObjects 和三级缓存 singletonFactories。在 Spring 中有一个基础接口 BeanFactory,其 DefaultSingletonBeanRegistry 类实现了 BeanFactory 接口,并且维护了三级缓存。

// 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
// 二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
// 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap(16);
  • singletonObjects:是一级缓存,存储的是完整创建好的单例 bean 对象。
  • earlySingletonObjects:是二级缓存,存储的是尚未完全创建好的单例 bean 对象。
  • singletonFactories:是三级缓存,存储的是单例 bean 的创建工厂。

2.2、三级缓存解决循环依赖

        在 Spring 中 Bean 的创建过程其实可以分成三步,第一步叫做实例化,第二步叫做初始化,第三步叫做销毁回调函数注册(这些是关于 Spring Bean 生命周期,如果对 Spring Bean 的生命周期不理解的可以看我这篇文章 深入理解 Spring Bean 的生命周期)。其中实例化的过程只需要调用构造函数把对象创建出来并给它分配存储空间,而初始化是对其属性进行赋值,而 Spring 之所以可以解决循环依赖正是因为对象的初始化是可以延后的,而当一个对象只进行了实例化,但是还没有进行初始化时,便称之为半成品对象。本章节接下来通过源码来窥探循环依赖是如何被解决的。

        在深入理解 Spring Bean 的生命周期中有一步叫做属性注入,而循环依赖就是发生在这里,以图 1-1 中的场景为例,Spring 容器在获取 ServiceA 的 bean 对象时,经历了实例化,来到了初始化步骤,此时正在进行属性注入,而 ServiceA 中有一个属性是 ServiceB 的 bean,于是 Spring 容器就尝试去提供 ServiceB 的 bean 来注入到 ServiceA 的这个属性中,但是 Spring 容器在尝试提供时发现容器中并没有 ServiceB 的 bean,于是转而创建 ServiceB 的 bean。如果创建成功了 Spring 容器中便会有 ServiceB 的 bean,那么就可以注入给 ServiceA 的 bean,于是 ServiceA 的 bean 也就完成了创建,从而交给 Spring 容器管理。但是如果在创建 ServiceB 的 bean 时,ServiceB 中有个属性是 ServiceA 的 bean ,那么 Spring 容器又会去尝试提供 ServiceA 的 bean,而此时 ServiceA 的 bean 还在等待 ServiceB 的 bean 创建,于是便形成了环,互相等待对方,循环依赖便出现了。

@Service
public class ServiceA {

    @Autowired
    private ServiceB serviceB;
}

@Service
public class ServiceB {

    @Autowired
    private ServiceA serviceA;
}

        那么 Spring 是如何解决循环依赖的问题的呢?其实通过上面的例子可以知道,问题是出现在属性注入的时候的,那么就去看看在属性注入时 Spring 是如何做的。

    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

        ......

        if (this.hasInstantiationAwareBeanPostProcessors()) {
                if (pvs == null) {
                    pvs = mbd.getPropertyValues();
                }

                PropertyValues pvsToUse;
                for(Iterator var11 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var11.hasNext(); pvs = pvsToUse) {
                    // 获取InstantiationAwareBeanPostProcessor
                    InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var11.next();
                    // 遍历执行其中的postProcessProperties方法
                    pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
                    if (pvsToUse == null) {
                        return;
                    }
                }
            }
        
        ......

    }

        这段代码是 Spring Bean 生命周期中处理 Bean 属性值的一个重要环节,它允许开发者在 Bean 的属性值被实际设置之前,通过实现 InstantiationAwareBeanPostProcessor 接口来修改这些属性值。这种机制为 Spring 的依赖注入提供了极大的灵活性和扩展性。首先会检查 Spring 的 BeanPostProcessor 列表中是否存在实现了 InstantiationAwareBeanPostProcessor 接口的 BeanPostProcessor,如果有则进入后续的逻辑。进入后续逻辑后会检查 pvs(PropertyValues的实例,代表 Bean 的属性值)是否为 null。如果是,则从 mbd 中获取 Bean 的属性值。这通常发生在 Bean 的定义中包含了属性设置时。接下来遍历所有实现了 InstantiationAwareBeanPostProcessor 接口的 BeanPostProcessor,对于每一个这样的处理器,调用 postProcessProperties 方法,传入当前的 pvs(可能已经被之前的处理器修改过),Bean 的实例(如果Bean已经部分实例化的话),以及 Bean 的名称。在每次迭代中,pvsToUse  都会更新为 postProcessProperties 方法的返回值,这意味着如果处理器修改了属性值,这些修改将在后续的处理器和最终的 Bean 实例化过程中得到反映。因此不妨看看 InstantiationAwareBeanPostProcessor 接口有哪些实现类。

图2-1 InstantiationAwareBeanPostProcessor 接口的实现类

        从图 2-1 可知 InstantiationAwareBeanPostProcessor 接口的实现类有 21 个,而在这些实现类中 AutowiredAnnotationBeanPostProcessor 格外引人注目,因为在 ServiceA 中将 ServiceB 作为属性时都会用到自动装配注解,因此不妨看看 AutowiredAnnotationBeanPostProcessor 类中是如何实现 postProcessProperties 方法的。

图2-2 AutowiredAnnotationBeanPostProcessor 类的 postProcessProperties 方法

        从图 2-2 可以看到,postProcessProperties 方法中调用了一个 findAutowiringMetadata 方法来获取需要被注入的信息。而在 findAutowiringMetadata 方法中其核心是调用了 buildAutowiringMetadata 方法。

private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
        // 检查这个类是否包含任何自动装配相关的注解(这些注解类型由this.autowiredAnnotationTypes指定)
        if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
            return InjectionMetadata.EMPTY;
        } else {
            // 创建一个空的elements来存储找到的注入元素
            List<InjectionMetadata.InjectedElement> elements = new ArrayList();
            // 初始化targetClass为传入的类,并准备遍历该类及其所有父类
            Class<?> targetClass = clazz;

            // 使用do-while循环遍历targetClass及其所有父类(不包括Object类)
            // 对于每个类,分别检查其字段和方法
            do {
                List<InjectionMetadata.InjectedElement> fieldElements = new ArrayList();
                // 检查字段
                ReflectionUtils.doWithLocalFields(targetClass, (field) -> {
                    // 查找字段上是否存在自动装配相关的注解
                    MergedAnnotation<?> ann = this.findAutowiredAnnotation(field);
                    if (ann != null) {
                        if (Modifier.isStatic(field.getModifiers())) {
                            // 如果找到注解,但字段是静态的,则记录一条日志,不支持静态字段的自动装配,并跳过该字段
                            if (this.logger.isInfoEnabled()) {
                                this.logger.info("Autowired annotation is not supported on static fields: " + field);
                            }

                            return;
                        }

                        // 否则,根据注解信息(如是否必需)创建一个AutowiredFieldElement对象,并将其添加到fieldElements列表中
                        boolean required = this.determineRequiredStatus(ann);
                        fieldElements.add(new AutowiredFieldElement(field, required));
                    }

                });
                List<InjectionMetadata.InjectedElement> methodElements = new ArrayList();
                // 检查方法
                ReflectionUtils.doWithLocalMethods(targetClass, (method) -> {
                    Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                    if (BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                        // 方法上是否存在自动装配相关的注解,并且当前方法是最具体的
                        MergedAnnotation<?> ann = this.findAutowiredAnnotation(bridgedMethod);
                        if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                            // 如果方法是静态的或没有参数(且不是记录类中的方法),则记录日志并跳过
                            if (Modifier.isStatic(method.getModifiers())) {
                                if (this.logger.isInfoEnabled()) {
                                    this.logger.info("Autowired annotation is not supported on static methods: " + method);
                                }

                                return;
                            }

                            if (method.getParameterCount() == 0) {
                                if (method.getDeclaringClass().isRecord()) {
                                    return;
                                }

                                if (this.logger.isInfoEnabled()) {
                                    this.logger.info("Autowired annotation should only be used on methods with parameters: " + method);
                                }
                            }
                            // 否则,根据注解信息(如是否必需)和找到的PropertyDescriptor(如果适用),创建一个AutowiredMethodElement对象,并将其添加到methodElements列表中
                            boolean required = this.determineRequiredStatus(ann);
                            PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                            methodElements.add(new AutowiredMethodElement(method, required, pd));
                        }

                    }
                });
                // 方法元素和字段元素添加到elements列表中,方法元素在前,字段元素在后。
                elements.addAll(0, this.sortMethodElements(methodElements, targetClass));
                elements.addAll(0, fieldElements);
                targetClass = targetClass.getSuperclass();
            } while(targetClass != null && targetClass != Object.class);
            // 将收集到的所有注入元素和类信息封装成一个InjectionMetadata对象。
            return InjectionMetadata.forElements(elements, clazz);
        }
    }

        所以 findAutowiringMetadata 方法返回的是所有注入元素和类的信息,拿到这些信息后去执行 inject 方法。而在 inject 方法中会遍历所有 InjectedElement,并执行它的 inject 方法,来完成注入。此处以字段注入为例,有一个 AutowiredFieldElement 类,该类继承自 InjectedElement,所以在遍历所有 InjectedElement,并执行它的 inject 方法时,如果是字段类型,那么执行的是 AutowiredFieldElement 类的 inject 方法,其核心逻辑是调用了 resolveFieldValue 方法,而 resolveFieldValue 方法核心就在于调用了 DefaultListableBeanFactory 类中的 resolveDependency 方法,而该方法的核心又在于调用了 doResolveDependency 方法,而在这个方法中又调用了 resolveCandidate 方法。

图2-3 DependencyDescriptor 类中的 resolveCandidate 方法

        从图 2-3 可以发现调用了一个十分常用的方法 getBean,去从 Spring 容器中获取要注入的 bean 对象。来到获取 bean 对象的关键方法 doGetBean 方法中。

图2-4 AbstractBeanFactory 类的 doGetBean 方法

        可以看到该方法会首先执行 getSingleton 方法,而三级缓存解决循环依赖的关键就在其中,再来重现目前的场景 ServiceA 的 bean 在属性注入时需要 Spring 容器提供 ServiceB 的 bean,而此时 ServiceB 的 bean 并未在容器中,于是需要创建 ServiceB 的 bean,而 ServiceB 的 bean 在属性注入是又需要 ServiceA 的 bean,于是代码运行到图 2-4 所示的位置,执行了 getSingleton 方法。

    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 从一级缓存中获取 bean 对象,如果获取到直接返回
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            // 如果一级缓存中没有,并且该 bean 允许循环依赖,就从二级缓存中获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                if (!this.singletonLock.tryLock()) {
                    return null;
                }

                try {
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            // 如果二级缓存中没有,就从三级缓存中获取
                            ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                // 通过三级缓存获取到的工厂来生成 bean
                                singletonObject = singletonFactory.getObject();
                                if (this.singletonFactories.remove(beanName) != null) {
                                    将生成的半成品对象放入二级缓存,并清除三级缓存中的该工厂
                                    this.earlySingletonObjects.put(beanName, singletonObject);
                                } else {
                                    singletonObject = this.singletonObjects.get(beanName);
                                }
                            }
                        }
                    }
                } finally {
                    this.singletonLock.unlock();
                }
            }
        }
        // 要么返回完整 bean,要么返回半成品 bean,要么返回 null
        return singletonObject;
    }

        从源码可以看出 getSingleton 方法正是利用三级缓存给 ServiceB 在属性注入需要 ServiceA 的 bean 对象时获取到了 ServiceA 的半成品 bean 对象。于是 ServiceB 的 bean 可以正常完成创建,而 ServiceB 的 bean 创建完成并交给 Spring 容器管理后,就可以注入给 ServiceA 的 bean,于是 ServiceA 的 bea 也能创建完成并交给 Spring 容器管理,循环依赖问题就此解决。

        可以看到三级缓存之所以解决了循环依赖是因为在三级缓存中获取到了 ServiceA 的生成 bean 的工厂,从而拿到了只进行了实例化的 ServiceA 的 bean。那 ServiceA 的工厂又是何时放进去的?而且从源码中还看到判断了该 bean 是否正处于创建中,singletonCurrentlyInCreation 又是一个 Set 集合,那该 bean 又是何时加入该 Set 集合的呢?

图2-5 AbstractAutowireCapableBeanFactory 类中 doCreateBean 方法的部分截图

        在 AbstractAutowireCapableBeanFactory 类中 doCreateBean 方法中有图 2-5 所示的部分代码,正是这一段代码先判断该 bean 是否允许循环依赖,如果允许的话就将其生成半成品的工厂放入三级缓存中。

        在调用 getSingleton 方法时,会调用一个 beforeSingletonCreation 方法。

图2-6 DefaultSingletonBeanRegistry 类的 beforeSingletonCreation 方法

        从图 2-6 可以看出正是在 beforeSingletonCreation 方法中将该 bean 加入 singletonCurrentlyInCreation 这个 Set 集合中的。

图2-7 三级缓存解决循环依赖

2.3、Spring 解决循环依赖的限制

        在第一章引言中层讲述了 Spring 解决循环依赖是有限制的,那为什么会有限制呢?

2.3.1、只支持单例

        Spring 循环依赖的解决方案主要是通过对象的提前暴露来实现的。当一个对象在创建过程中需要引用到另一个正在创建的对象时,Spring 会提前暴露一个只进行了实例化的对象,以此来解决了循环依赖问题。

        在 Spring 容器中,单例对象的创建和初始化只会发生一次,并且在容器启动时就完成了。因此在容器运行期间,单例对象的依赖关系不会发生变化,相比之下,原型对象的创建和初始化可以发生多次,并且在容器运行期间会动态变化。所以 Spring 只支持通过单例对象的提前暴露来解决循环依赖问题。

2.3.2、不支持构造函数注入

        Spring 无法解决构造函数带来的循环依赖,因为提前暴露需要提供一个半成品对象,但是提供一个半成品对象也需要执行构造函数,因此 Spring 容器无法在构造函数注入中实现循环依赖的解决。

        但是和只支持单例又不同,不支持构造函数的注入是可以解决的,那就是通过 @Lazy 注解。

2.4、三级缓存的必要性

        一定要有三级缓存吗,二级缓存就不可以解决循环依赖吗?其实使用二级缓存也能解决循环依赖,但是如果完全依靠二级缓存解决循环依赖,那当依赖的是一个代理类时就需要在 Bean 实例化之后完成 AOP 代理。然而在 Spring 的设计中,为了解耦 Bean 的初始化和代理,是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 初始化的最后一步完成 AOP 代理的,所以 Spring 为了依赖代理对象又不能破坏 Spring 的设计,便引入了第三级缓存,在三级缓存中保存对象工厂,通过工厂可以直接获取到对象,于是在后续发生循环依赖时,如果依赖的 Bean 被 AOP 代理了,那么通过这个工厂获取到的就是代理后的对象,如果没有被 AOP 代理,那么这个工厂获取到的就是实例化的真实对象。

3、总结

        本文通过分析源码解释了在 Spring 中是如何通过三级缓存来解决循环依赖问题,并解释了为什么 Spring 解决循环依赖会有限制以及三级缓存的必要性。而通过三级缓存解决循环依赖的详细过程也通过图 2-7 可以很直观的看出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值