• Spring 是如何解决循环依赖的?


     

    1.由同事抛的一个问题开始

    最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的几个问题后,重新刷新了我的认识。

    我们先看看当时出问题的代码片段:

    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        @Async
        public void test1() {
        }
    }
    
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }

    这两段代码中定义了两个Service类:TestService1TestService2,在TestService1中注入了TestService2的实例,同时在TestService2中注入了TestService1的实例,这里构成了循环依赖

    最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

    [BAT大佬写的刷题笔记,让我offer拿到手软](这位BAT大佬写的Leetcode刷题笔记,让我offer拿到手软)

    只不过,这不是普通的循环依赖,因为TestService1的test1方法上加了一个@Async注解。

    大家猜猜程序启动后运行结果会怎样?

    org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

    报错了。。。原因是出现了循环依赖。

    「不科学呀,spring不是号称能解决循环依赖问题吗,怎么还会出现?」

    如果把上面的代码稍微调整一下:

    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        public void test1() {
        }
    }

    把TestService1的test1方法上的@Async注解去掉,TestService1TestService2都需要注入对方的实例,同样构成了循环依赖。

    但是重新启动项目,发现它能够正常运行。这又是为什么?

    带着这两个问题,让我们一起开始spring循环依赖的探秘之旅。

    2.什么是循环依赖?

    循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

    第一种情况:自己依赖自己的直接依赖

    第二种情况:两个对象之间的直接依赖

    第三种情况:多个对象之间的间接依赖

     

    前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

    3.循环依赖的N种场景

    spring中出现循环依赖主要有以下场景:

     

    最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

    [BAT大佬写的刷题笔记,让我offer拿到手软](这位BAT大佬写的Leetcode刷题笔记,让我offer拿到手软)

    单例的setter注入

    这种注入方式应该是spring用的最多的,代码如下:

    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        public void test1() {
        }
    }
    
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }

    这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了。

    spring内部有三级缓存:

    • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
    • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
    • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

    下面用一张图告诉你,spring是如何解决循环依赖的:

     

     

    图1

    细心的朋友可能会发现在这种场景中第二级缓存作用不大。

    那么问题来了,为什么要用第二级缓存呢?

    试想一下,如果出现以下这种情况,我们要如何处理?

    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
        @Autowired
        private TestService3 testService3;
    
        public void test1() {
        }
    }
    
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }
    
    @Service
    public class TestService3 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test3() {
        }
    }

    TestService1依赖于TestService2和TestService3,而TestService2依赖于TestService1,同时TestService3也依赖于TestService1。

    按照上图的流程可以把TestService1注入到TestService2,并且TestService1的实例是从第三级缓存中获取的。

    最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

    [BAT大佬写的刷题笔记,让我offer拿到手软](这位BAT大佬写的Leetcode刷题笔记,让我offer拿到手软)

    假设不用第二级缓存,TestService1注入到TestService3的流程如图:

     

     

    图2

    TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。

    这样不是有问题?

    为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。

     

     

    图3

    还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?

    答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

    针对这种场景spring是怎么做的呢?

    答案就在AbstractAutowireCapableBeanFactorydoCreateBean方法的这段代码中:

     

    它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

    多例的setter注入

    这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        public void test1() {
        }
    }
    
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }

    很多人说这种情况spring容器启动会报错,其实是不对的,我非常负责任的告诉你程序能够正常启动。

    为什么呢?

    其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了preInstantiateSingletons方法

     

    标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。

    而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。

    如何让他提前初始化bean呢?

    只需要再定义一个单例的类,在它里面注入TestService1

    @Service
    public class TestService3 {
    
        @Autowired
        private TestService1 testService1;
    }

    重新启动程序,执行结果:

    Requested bean is currently in creation: Is there an unresolvable circular reference?

    果然出现了循环依赖。

    注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

    构造器注入

    这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,看看如下代码:

    @Service
    public class TestService1 {
    
        public TestService1(TestService2 testService2) {
        }
    }
    
    @Service
    public class TestService2 {
    
        public TestService2(TestService1 testService1) {
        }
    }

    运行结果:

    Requested bean is currently in creation: Is there an unresolvable circular reference?

    出现了循环依赖,为什么呢?

    从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

    单例的代理对象setter注入

    这种注入方式其实也比较常用,比如平时使用:@Async注解的场景,会通过AOP自动生成代理对象。

    我那位同事的问题也是这种情况。

    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        @Async
        public void test1() {
        }
    }
    
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }

    从前面得知程序启动会报错,出现了循环依赖:

    org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

    为什么会循环依赖呢?

    答案就在下面这张图中:

     

     

    说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

     

     

    那位同事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异常。

    如果这时候把TestService1改个名字,改成:TestService6,其他的都不变。

    @Service
    public class TestService6 {
    
        @Autowired
        private TestService2 testService2;
    
        @Async
        public void test1() {
        }
    }

    再重新启动一下程序,神奇般的好了。

    what? 这又是为什么?

    这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以TestService1比TestService2先加载,而改了文件名称之后,TestService2比TestService6先加载。

    为什么TestService2比TestService6先加载就没问题呢?

    答案在下面这张图中:

     

     

    这种情况testService6中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

    DependsOn循环依赖

    还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

    @DependsOn(value = "testService2")
    @Service
    public class TestService1 {
    
        @Autowired
        private TestService2 testService2;
    
        public void test1() {
        }
    }
    
    @DependsOn(value = "testService1")
    @Service
    public class TestService2 {
    
        @Autowired
        private TestService1 testService1;
    
        public void test2() {
        }
    }

    程序启动之后,执行结果:

    Circular depends-on relationship between 'testService2' and 'testService1'

    这个例子中本来如果TestService1和TestService2都没有加@DependsOn注解是没问题的,反而加了这个注解会出现循环依赖问题。

    这又是为什么?

    答案在AbstractBeanFactory类的doGetBean方法的这段代码中:

     

    它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

    4.出现循环依赖如何解决?

    项目中如果出现循环依赖问题,说明是spring默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

    生成代理对象产生的循环依赖

    这类循环依赖问题解决方法很多,主要有:

    1. 使用@Lazy注解,延迟加载
    2. 使用@DependsOn注解,指定加载先后关系
    3. 修改文件名称,改变循环依赖类的加载顺序

    使用@DependsOn产生的循环依赖

    这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

    多例循环依赖

    这类循环依赖问题可以通过把bean改成单例的解决。

    构造器循环依赖

    这类循环依赖问题可以通过使用@Lazy注解解决。

    最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

    链接: 密码:bhbe

    不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激! 

    最后说一句(求关注,别白嫖我)

    如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下:苏三说技术,您的支持是我坚持写作最大的动力。收藏不是真爱,点赞才是。

     

    更多回答

     
     

    面试官:今天要不来聊聊Spring对Bean的生命周期管理?

    候选者:嗯,没问题的。

    候选者:很早之前我就看过源码,但Spring源码的实现类都太长了

    候选者:我也记不得很清楚某些实现类的名字,要不我大概来说下流程?

    面试官:没事,你开始吧

    候选者:首先要知道的是

    候选者:普通Java对象和Spring所管理的Bean实例化的过程是有些区别的

    候选者:在普通Java环境下创建对象简要的步骤可以分为:

    候选者:1):java源码被编译为被编译为class文件

    候选者:2):等到类需要被初始化时(比如说new、反射等)

    候选者:3):class文件被虚拟机通过类加载器加载到JVM

    候选者:4):初始化对象供我们使用

    候选者:简单来说,可以理解为它是用Class对象作为「模板」进而创建出具体的实例

     

     

    候选者:而Spring所管理的Bean不同的是,除了Class对象之外,还会使用BeanDefinition的实例来描述对象的信息

    候选者:比如说,我们可以在Spring所管理的Bean有一系列的描述:@Scope、@Lazy、@DependsOn等等

    候选者:可以理解为:Class只描述了类的信息,而BeanDefinition描述了对象的信息

    面试官:嗯,这我大致了解你的意思了。

    面试官:你就是想告诉我,Spring有BeanDefinition来存储着我们日常给Spring Bean定义的元数据(@Scope、@Lazy、@DependsOn等等),对吧?

    候选者:不愧是你

    面试官:赶紧的,继续吧

     

     

    候选者:Spring在启动的时候需要「扫描」在XML/注解/JavaConfig 中需要被Spring管理的Bean信息

    候选者:随后,会将这些信息封装成BeanDefinition,最后会把这些信息放到一个beanDefinitionMap中

    候选者:我记得这个Map的key应该是beanName,value则是BeanDefinition对象

    候选者:到这里其实就是把定义的元数据加载起来,目前真实对象还没实例化

     

     

    候选者:接着会遍历这个beanDefinitionMap,执行BeanFactoryPostProcessor这个Bean工厂后置处理器的逻辑

    候选者:比如说,我们平时定义的占位符信息,就是通过BeanFactoryPostProcessor的子类PropertyPlaceholderConfigurer进行注入进去

    候选者:当然了,这里我们也可以自定义BeanFactoryPostProcessor来对我们定义好的Bean元数据进行获取或者修改

    候选者:只是一般我们不会这样干,实际上也很有少的使用场景

     

     

    面试官:嗯….

    候选者:BeanFactoryPostProcessor后置处理器执行完了以后,就到了实例化对象啦

    候选者:在Spring里边是通过反射来实现的,一般情况下会通过反射选择合适的构造器来把对象实例化

    候选者:但这里把对象实例化,只是把对象给创建出来,而对象具体的属性是还没注入的。

    候选者:比如我的对象是UserService,而UserService对象依赖着SendService对象,这时候的SendService还是null的

    候选者:所以,下一步就是把对象的相关属性给注入(:

     

     

    候选者:相关属性注入完之后,往下接着就是初始化的工作了

    候选者:首先判断该Bean是否实现了Aware相关的接口,如果存在则填充相关的资源

    候选者:比如我这边在项目用到的:我希望通过代码程序的方式去获取指定的Spring Bean

    候选者:我们这边会抽取成一个工具类,去实现ApplicationContextAware接口,来获取ApplicationContext对象进而获取Spring Bean

     

     

    候选者:Aware相关的接口处理完之后,就会到BeanPostProcessor后置处理器啦

    候选者:BeanPostProcessor后置处理器有两个方法,一个是before,一个是after(那肯定是before先执行、after后执行)

     

     

    候选者:这个BeanPostProcessor后置处理器是AOP实现的关键(关键子类AnnotationAwareAspectJAutoProxyCreator)

    候选者:所以,执行完Aware相关的接口就会执行BeanPostProcessor相关子类的before方法

    候选者:BeanPostProcessor相关子类的before方法执行完,则执行init相关的方法,比如说@PostConstruct、实现了InitializingBean接口、定义的init-method方法

    候选者:当时我还去官网去看他们的被调用「执行顺序」分别是:@PostConstruct、实现了InitializingBean接口以及init-method方法

    候选者:这些都是Spring给我们的「扩展」,像@PostConstruct我就经常用到

     

     

    候选者:比如说:对象实例化后,我要做些初始化的相关工作或者就启个线程去Kafka拉取数据

    候选者:等到init方法执行完之后,就会执行BeanPostProcessor的after方法

    候选者:基本重要的流程已经走完了,我们就可以获取到对象去使用了

    候选者:销毁的时候就看有没有配置相关的destroy方法,执行就完事了

    面试官:嗯,了解,但我的观众好像不太满意,总感觉少了些什么。

    面试官:你看过Spring是怎么解决循环依赖的吗?

    面试官:如果现在有个A对象,它的属性是B对象,而B对象的属性也是A对象

    面试官:说白了就是A依赖B,而B又依赖A,Spring是怎么做的?

    候选者:嗯,这块我也是看过的,其实也是在Spring的生命周期里面嘛

    候选者:从上面我们可以知道,对象属性的注入在对象实例化之后的嘛。

    候选者:它的大致过程是这样的:

    候选者:首先A对象实例化,然后对属性进行注入,发现依赖B对象

    候选者:B对象此时还没创建出来,所以转头去实例化B对象

    候选者:B对象实例化之后,发现需要依赖A对象,那A对象已经实例化了嘛,所以B对象最终能完成创建

    候选者:B对象返回到A对象的属性注入的方法上,A对象最终完成创建

    候选者:上面就是大致的过程;

    面试官:听起来你还会原理哦?

    候选者:Absolutely

    候选者:至于原理,其实就是用到了三级的缓存

    候选者:所谓的三级缓存其实就是三个Map…首先明确一定,我对这里的三级缓存定义是这样的:

    候选者:singletonObjects(一级,日常实际获取Bean的地方);

    候选者:earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来);

    候选者:singletonFactories(三级,Value是一个对象工厂);

     

     

    候选者:再回到刚才讲述的过程中,A对象实例化之后,属性注入之前,其实会把A对象放入三级缓存中

    候选者:key是BeanName,Value是ObjectFactory

    候选者:等到A对象属性注入时,发现依赖B,又去实例化B时

    候选者:B属性注入需要去获取A对象,这里就是从三级缓存里拿出ObjectFactory,从ObjectFactory得到对应的Bean(就是对象A)

    候选者:把三级缓存的A记录给干掉,然后放到二级缓存中

    候选者:显然,二级缓存存储的key是BeanName,value就是Bean(这里的Bean还没做完属性注入相关的工作)

    候选者:等到完全初始化之后,就会把二级缓存给remove掉,塞到一级缓存中

    候选者:我们自己去getBean的时候,实际上拿到的是一级缓存的

    候选者:大致的过程就是这样

    面试官:那我想问一下,为什么是三级缓存?

    候选者:首先从第三级缓存说起(就是key是BeanName,Value为ObjectFactory)

    候选者:我们的对象是单例的,有可能A对象依赖的B对象是有AOP的(B对象需要代理)

    候选者:假设没有第三级缓存,只有第二级缓存(Value存对象,而不是工厂对象)

    候选者:那如果有AOP的情况下,岂不是在存入第二级缓存之前都需要先去做AOP代理?这不合适嘛

    候选者:这里肯定是需要考虑代理的情况的,比如A对象是一个被AOP增量的对象,B依赖A时,得到的A肯定是代理对象的

    候选者:所以,三级缓存的Value是ObjectFactory,可以从里边拿到代理对象

    候选者:而二级缓存存在的必要就是为了性能,从三级缓存的工厂里创建出对象,再扔到二级缓存(这样就不用每次都要从工厂里拿)

    候选者:应该很好懂吧?

     

     

    面试官:确实(:

    候选者:我稍微总结一下今天的内容吧

    候选者:怕你的观众说不满意,那我就没有赞了,没有赞我就很难受

    候选者:首先是Spring Bean的生命周期过程,Spring使用BeanDefinition来装载着我们给Bean定义的元数据

    候选者:实例化Bean的时候实际上就是遍历BeanDefinitionMap

    候选者:Spring的Bean实例化和属性赋值是分开两步来做的

    候选者:在Spring Bean的生命周期,Spring预留了很多的hook给我们去扩展

    候选者:1):Bean实例化之前有BeanFactoryPostProcessor

    候选者:2):Bean实例化之后,初始化时,有相关的Aware接口供我们去拿到Context相关信息

    候选者:3):环绕着初始化阶段,有BeanPostProcessor(AOP的关键)

    候选者:4):在初始化阶段,有各种的init方法供我们去自定义

    候选者:而循环依赖的解决主要通过三级的缓存

    候选者:在实例化后,会把自己扔到三级缓存(此时的key是BeanName,Value是ObjectFactory)

    候选者:在注入属性时,发现需要依赖B,也会走B的实例化过程,B属性注入依赖A,从三级缓存找到A

    候选者:删掉三级缓存,放到二级缓存

    面试官:嗯,你要不后面放点关键的源码吧

    候选者:这你倒是提醒我了,确实有必要

    面试官:这要是能听懂,是真的看过源码才行(:还好我看过

    关键源码方法(强烈建议自己去撸一遍)

    • org.springframework.context.support.AbstractApplicationContext#refresh(入口)
    • org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization(初始化单例对象入口)
    • org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons(初始化单例对象入口)
    • org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String)(万恶之源,获取并创建Bean的入口)
    • org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean(实际的获取并创建Bean的实现)
    • org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)(从缓存中尝试获取)
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])(实例化Bean)
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean(实例化Bean具体实现)
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance(具体实例化过程)
    • org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingletonFactory(将实例化后的Bean添加到三级缓存)
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean(实例化后属性注入)
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)(初始化入口)

    我最近一直在连载《对线面试官》系列,目前已经连载38篇啦!一个说人话的面试系列!

    【大厂面试知识点】、【简历模板】、【原创文章】电子书,共有1263页

    我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不会还想白嫖吧?点赞和转发又不用钱。

     

    链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

    收藏等于白嫖,点赞才是真情!

     
  • 相关阅读:
    开发日记1
    探索需求2
    探索需求1
    周总结8
    周总结7
    周总结6
    周总结5
    周总结4
    周总结3
    周总结2
  • 原文地址:https://www.cnblogs.com/weixupeng/p/15935583.html
Copyright © 2020-2023  润新知