前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring中的循环依赖解决详解

Spring中的循环依赖解决详解

作者头像
猿芯
发布于 2021-03-22 03:32:42
发布于 2021-03-22 03:32:42
39800
代码可运行
举报
运行总次数:0
代码可运行

原 https://www.cnblogs.com/leeego-123/p/12165278.html

前言

说起Spring中循环依赖的解决办法,相信很多园友们都或多或少的知道一些,但当真的要详细说明的时候,可能又没法一下将它讲清楚。本文就试着尽自己所能,对此做出一个较详细的解读。另,需注意一点,下文中会出现类的实例化跟类的初始化两个短语,为怕园友迷惑,事先声明一下,本文的实例化是指刚执行完构造器将一个对象new出来,但还未填充属性值的状态,而初始化是指完成了属性的依赖注入。

一、先说说Spring解决的循环依赖是什么

Java中的循环依赖分两种,一种是构造器的循环依赖,另一种是属性的循环依赖。

构造器的循环依赖就是在构造器中有属性循环依赖,如下所示的两个类就属于构造器循环依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class Student {
    @Autowired
    private Teacher teacher;

    public Student (Teacher teacher) {
        System.out.println("Student init1:" + teacher);
    }

    public void learn () {
        System.out.println("Student learn");
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class Teacher {
    @Autowired
    private Student student;

    public Teacher (Student student) {
        System.out.println("Teacher init1:" + student);

    }

    public void teach () {
        System.out.println("teach:");
        student.learn();
    }
}

这种循环依赖没有什么解决办法,因为JVM虚拟机在对类进行实例化的时候,需先实例化构造器的参数,而由于循环引用这个参数无法提前实例化,故只能抛出错误。

Spring解决的循环依赖就是指属性的循环依赖,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class Teacher {
    @Autowired
    private Student student;

    public Teacher (Student student) {
        System.out.println("Teacher init1:" + student);

    }

    public void teach () {
        System.out.println("teach:");
        student.learn();
    }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class Student {
    @Autowired
    private Teacher teacher;

    public Student () {
        System.out.println("Student init:" + teacher);
    }

    public void learn () {
        System.out.println("Student learn");
    }
}

测试扫描类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@ComponentScan(value = "myPackage")
public class ScanConfig {    
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
测试启动类:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SpringTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanConfig.class);
        applicationContext.getBean(Teacher.class).teach();
    }
}

测试类执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1 Student init:null
2 Teacher init:null
3 teach:
4 Student learn
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
可以看到,在构造器执行的时候未完成属性的注入,而在调用方法的时候已经完成了注入。下面就一起看看Spring内部是在何时完成的属性注入,又是如何解决的循环依赖。

二、循环依赖与属性注入

1、对于非懒加载的类,是在refresh方法中的

finishBeanFactoryInitialization(beanFactory) 方法完成的包扫描以及bean的初始化,下面就一起追踪下去。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
     // 其他代码
     // Instantiate all remaining (non-lazy-init) singletons.
     beanFactory.preInstantiateSingletons();
}

可以看到调用了beanFactory的一个方法,此处的beanFactory就是指我们最常见的那个DefaultListableBeanFactory,下面看它里面的这个方法。

2、DefaultListableBeanFactory的preInstantiateSingletons方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void preInstantiateSingletons() throws BeansException {

        List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

        // Trigger initialization of all non-lazy singleton beans...
        for (String beanName : beanNames) {
            RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { // 判断为非抽象类、是单例、非懒加载 才给初始化
                if (isFactoryBean(beanName)) {
                    // 无关代码(针对FactoryBean的处理)
                }
                else {
                    // 重要!!!普通bean就是在这里初始化的
                    getBean(beanName);
                }
            }
        }

        // 其他无关代码
    }

可以看到,就是在此方法中循环Spring容器中所有的bean,依次对其进行初始化,初始化的入口就是getBean方法

3、AbstractBeanFactory的getBean跟doGetBean方法

追踪getBean方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Object getBean(String name) throws BeansException {
    return doGetBean(name, null, null, false);
}

可见引用了重载的doGetBean方法,继续追踪之:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

        final String beanName = transformedBeanName(name);
        Object bean;

         // 方法1)从三个map中获取单例类
        Object sharedInstance = getSingleton(beanName);
        // 省略无关代码
        }
        else {
            // 如果是多例的循环引用,则直接报错
            if (isPrototypeCurrentlyInCreation(beanName)) {
                throw new BeanCurrentlyInCreationException(beanName);
            }
            // 省略若干无关代码
            try {
                // Create bean instance.
                if (mbd.isSingleton()) {
                    // 方法2) 获取单例对象
                    sharedInstance = getSingleton(beanName, () -> {
                        try { //方法3) 创建ObjectFactory中getObject方法的返回值
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }
         }
        // 省略若干无关代码
        return (T) bean;
    }

该方法比较长,对于解决循环引用来说,上面标出来的3个方法起到了至关重要的作用,下面我们挨个攻克。

3.1) getSingleton(beanName)方法注意该方法跟方法2)是重载方法,名字一样内部逻辑却大相径庭。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);// 步骤A
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);// 步骤B
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);// 步骤C
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

通过上面的步骤可以看出这三个map的优先级。其中singletonObjects里面存放的是初始化之后的单例对象;earlySingletonObjects中存放的是一个已完成实例化未完成初始化的早期单例对象;而singletonFactories中存放的是ObjectFactory对象,此对象的getObject方法返回值即刚完成实例化还未开始初始化的单例对象。所以先后顺序是,单例对象先存在于singletonFactories中,后存在于earlySingletonObjects中,最后初始化完成后放入singletonObjects中

当debug到此处时,以上述Teacher和Student两个循环引用的类为例,如果第一个走到这一步的是Teacher,则从此处这三个map中get到的值都是空,因为还未添加进去。这个方法主要是给循环依赖中后来过来的对象用。

3.2)getSingleton(String beanName, ObjectFactory<?> singletonFactory)方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "Bean name must not be null");
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                // 省略无关代码
                beforeSingletonCreation(beanName); // 步骤A
                boolean newSingleton = false;
                // 省略无关代码
                try {
                    singletonObject = singletonFactory.getObject();// 步骤B
                    newSingleton = true;
                }
                // 省略无关代码
                finally {
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }
                    afterSingletonCreation(beanName);// 步骤C
                }
                if (newSingleton) {
                    addSingleton(beanName, singletonObject);// 步骤D
                }
            }
            return singletonObject;
        }
    }

获取单例对象的主要逻辑就是此方法实现的,主要分为上面四个步骤,继续挨个看:

步骤A:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1 protected void beforeSingletonCreation(String beanName) {
2         // 判断,并首次将beanName即teacher放入singletonsCurrentlyInCreation中
3         if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
4             throw new BeanCurrentlyInCreationException(beanName);
5         }
6     }

步骤C:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 protected void afterSingletonCreation(String beanName) {
         // 得到单例对象后,再讲beanName从singletonsCurrentlyInCreation中移除
         if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
             throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
         }
}

步骤D:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void addSingleton(String beanName, Object singletonObject) {
         synchronized (this.singletonObjects) {
             this.singletonObjects.put(beanName, singletonObject);//添加单例对象到map中
             this.singletonFactories.remove(beanName);//从早期暴露的工厂中移除,此map在解决循环依赖中发挥了关键的作用
             this.earlySingletonObjects.remove(beanName);//从早期暴露的对象map中移除
             this.registeredSingletons.add(beanName);//添加到已注册的单例名字集合中
         }
     }
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
步骤B

此处调用了ObjectFactory的getObject方法,此方法是在哪里实现的呢?返回的又是什么?且往回翻,找到3中的方法3,对java8函数式编程有过了解的园友应该能看出来,方法3 【createBean(beanName, mbd, args)】的返回值就是getObject方法的返回值,即方法3返回的就是我们需要的单例对象,下面且追踪方法3而去。

3.3)

AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[]) 方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
            throws BeanCreationException {

        // 省略无关代码
        try {
            Object beanInstance = doCreateBean(beanName, mbdToUse, args);
            return beanInstance;
        }
        // 省略无关代码
    }

去掉无关代码之后,关键方法只有doCreateBean方法,追踪之:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
            throws BeanCreationException {

        BeanWrapper instanceWrapper = null;
        // 省略代码
        if (instanceWrapper == null) {
            // 实例化bean
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            // 重点!!!将实例化的对象添加到singletonFactories中
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }
        // 初始化bean
        Object exposedObject = bean;
        try {
            populateBean(beanName, mbd, instanceWrapper);//也很重要
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
        // 省略无关代码
        return exposedObject;
}

上面注释中标出的重点是此方法的关键。在addSingletonFactory方法中,将第二个参数ObjectFactory存入了singletonFactories供其他对象依赖时调用。然后下面的populateBean方法对刚实例化的bean进行属性注入(该方法关联较多,本文暂时不展开追踪了,有兴趣的园友自行查看即可),如果遇到Spring中的对象属性,则再通过getBean方法获取该对象。至此,循环依赖在Spring中的处理过程已经追溯完毕,下面我们总结一下。

小结

属性注入主要是在populateBean方法中进行的。对于循环依赖,以我们上文中的Teacher中注入了Student、Student中注入了Teacher为例来说明,假定Spring的加载顺序为先加载Teacher,再加载Student。

getBean方法触发Teacher的初始化后:

a. 首先走到3中的方法1),此时map中都为空,获取不到实例;

b. 然后走到方法2)中,步骤A、步骤C、步骤D为控制map中数据的方法,实现简单,可暂不关注。其中步骤B的getObject方法触发对方法3)的调用;

c. 在方法3)中,先通过createBeanInstance实例化Teacher对象,又将该实例化的对象通过addSingletonFactory方法放入singletonFactories中,完成Teacher对象早期的暴露;

d. 然后在方法3)中通过populateBean方法对Teacher对象进行属性的注入,发现它有一个Student属性,则触发getBean方法对Student进行初始化

e. 重复a、b、c步骤,只是此时要初始化的是Student对象

f. 走到d的时候,调用populateBean对Student对象进行属性注入,发现它有一个Teacher属性,则触发getBean方法对Teacher进行初始化;

g. 对Teacher进行初始化,又来到a,但此时map已经不为空了,因为之前在c步骤中已经将Teacher实例放入了singletonFactories中,a中得到Teacher实例后返回;

h.完成f中对Student的初始化,继而依次往上回溯完成Teacher的初始化;

完成Teacher的初始化后,Student的初始化就简单了,因为map中已经存了这个单例。

至此,Spring循环依赖的总结分析结束,一句话来概括一下:Spring通过将实例化后的对象提前暴露给Spring容器中

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构荟萃 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Linux系统编程:进程控制(创建,终止,等待)
1.在fork返回之前,创建了子进程的 PCB数据结构,以及拷贝了一份父进程的地址空间和页表;此时页表是出于只读,一旦修改就会写实拷贝;
啊QQQQQ
2025/02/20
1360
Linux系统编程:进程控制(创建,终止,等待)
【Linux篇章】踏入 Linux 进程控制的奇幻迷宫,解锁系统调度奥秘(秒懂版)
因此;我们就可以把fork后理解成分流:返回的id=0的子进程是一个流而id=子进程pid 父进程是一个流;各自干各自的事。
羑悻的小杀马特.
2025/04/15
320
【Linux篇章】踏入 Linux 进程控制的奇幻迷宫,解锁系统调度奥秘(秒懂版)
【Linux】进程控制
我们在前面的文章中多次使用过fork函数,我们在这里再来简单概括一下进程的创建 fork可以在已有的进程中创建出一个新进程,老进程为父进程,新进程为子进程
s-little-monster
2025/02/14
550
【Linux】进程控制
详谈 Linux进程控制(看这一篇就够了)
本文将系统介绍进程控制的基本要素,包括进程创建, 进程终止, 进程等待, 进程替换等方面。深入理解进程创建的相关知识, 帮助更好的构建知识架构!
用户11317877
2024/12/20
1500
详谈 Linux进程控制(看这一篇就够了)
Linux之进程控制
此外还可以通过调用fork函数创建子进程,子进程和父进程共享fork之后的代码,可以采用对fork返回值进行判断的办法来让父子进程分别执行后续代码的一部分。
始终学不会
2023/04/17
8440
Linux之进程控制
【Linux篇】进程控制
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
_孙同学
2025/03/21
880
【Linux篇】进程控制
Linux进程控制
返回值:自进程中返回0,父进程返回子进程id,出错返回-1。 进程拥有独立性,fork之后就变成了两个程序,父子进程共享后边的代码。 那么为什么给父进程返回的就是子进程的pid,而给子进程返回的就是0呢? 就好比孩子只能有一个亲生的父亲,而一个父亲可以拥有很多亲生孩子,每个孩子都是独立不同的。 fork函数是在什么时候创建的子进程呢?
有礼貌的灰绅士
2023/03/28
2.9K0
Linux进程控制
Linux之进程控制
退出码是用来标识一个进程任务执行结果的情况。因为成功只有一种情况,而失败的情况很多,因此,一般情况下0表示执行成功,非0表示执行失败。非0的数字不同,所表示的错误不同。系统对于退出码一般都有着相应的文字藐视,当然我们也可以自定义,也可以直接使用系统给定的映射关系。(例如,strerror这个函数)
摘星
2023/10/15
2030
Linux之进程控制
【Linux】万字解读<进程控制>:创建&中止&等待&替换
YY的秘密代码小屋
2024/09/11
1240
【Linux】万字解读<进程控制>:创建&中止&等待&替换
【Linux】探索进程控制奥秘,解锁高效实战技巧
因为字符串具有常量属性,字符常量不可被修改。这里的问题是字符串为什么会有常量属性呢?
用户11316056
2024/10/16
660
【Linux】探索进程控制奥秘,解锁高效实战技巧
【Linux】进程控制
若父子进程数据都不修改,则父子进程指向同一个物理地址, 若子进程数据修改,则拷贝一个物理空间,将新的地址填到子进程对应的页表中,使子进程重新映射,访问到新的空间 进程的内核数据结构,父子各自有一套,彼此双方互不影响, 代码和数据通过写时拷贝的反方式,实现分开
lovevivi
2023/04/28
2K0
【Linux】进程控制
【Linux】Linux进程控制——进程创建、进程终止及进程等待详解
在Linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
P_M_P
2024/08/15
3800
【Linux】Linux进程控制——进程创建、进程终止及进程等待详解
Linux进程控制
hello,my friend!今天,我们要开始学习新的内容了--->进程控制,进程控制涉及到操作系统如果管理和控制运行在计算机系统内的进程。我们将从fork函数,Linux进程退出,Linux进程等待,Linux进程替换等方面学习。那么接下来我们就开始敲黑板了!!
破晓的历程
2024/06/24
1220
Linux进程控制
Linux系统-进程控制
Linux进程控制 零、前言 一、进程创建 1、fork函数 2、fork返回值 写时拷贝 3、fork用法 4、fork失败 二、进程终止 1、退出码 2、退出方法 1) 调用_exit函数 2)调用exit函数 3)main函数return 4)异常退出 3、理解终止 三、进程等待 1、等待方法 2、获取status 3、理解等待 四、进程替换 1、替换原理 2、替换方法 五、实现简易shell 零、前言 前篇我们讲解学习了关于进程的概念知识,本章主要讲解关于进程的控制,深入学习进程 一、进程创建
用户9645905
2022/11/30
1.5K0
Linux系统-进程控制
进程控制
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
海盗船长
2020/08/27
7410
【linux】进程等待与进程替换
任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,状态Z(僵尸状态),内存泄漏
用户11029103
2024/11/16
1700
【linux】进程等待与进程替换
Linux进程控制【创建、终止、等待】
进程 创建后,需要对其进行合理管理,光靠 OS 是无法满足我们的需求的,此时可以运用 进程 控制相关知识,对 进程 进行手动管理,如创建 进程、终止 进制、等待 进程 等,其中等待 进程 可以有效解决僵尸 进程 问题
北 海
2023/07/01
3780
Linux进程控制【创建、终止、等待】
【Linux】深入理解进程控制:从创建到终止和进程等待
fork 函数是 Unix/Linux 系统中用于创建新进程的系统调用。调用 fork 后,当前进程(父进程)会被复制,创建出一个新的进程(子进程)。 fork函数特点:
用户11305458
2024/11/21
2120
【Linux】深入理解进程控制:从创建到终止和进程等待
Linux进程编程
功能:创建一个与原来进程几乎完全相同的进程,即两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来的进程所有值都复制到新的进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
Marigold
2022/06/17
7.9K0
Linux进程编程
【Linux】开始掌握进程控制吧!
送给大家一句话: 我并不期待人生可以一直过得很顺利,但我希望碰到人生难关的时候,自己可以是它的对手。—— 加缪
叫我龙翔
2024/03/30
1220
【Linux】开始掌握进程控制吧!
推荐阅读
相关推荐
Linux系统编程:进程控制(创建,终止,等待)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档