美文网首页
Spring Bean生命周期之三级缓存循环依赖

Spring Bean生命周期之三级缓存循环依赖

作者: 上善若泪 | 来源:发表于2023-06-03 18:17 被阅读0次

    1 三级缓存

    在使用 spring框架的日常开发中, bean之间的循环依赖太频繁了, spring已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下 spring是如何解决bean之间循环依赖,为什么要使用到三级缓存,而不是二级缓存?
    点击了解 Spring Bean生命周期之概述

    1.1 引言

    必须先对bean的生命周期做了一个整体的流程分析,对spring如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring中生成,则会跑去生成属性对象实例。

    在这里插入图片描述

    我们可以看到填充属性的时候,spring会提前将已经实例化的bean通过ObjectFactory半成品暴露出去,为什么称为半成品是因为这时候的bean对象实例化,但是未进行属性填充,是一个不完整的bean实例对象
    实例化 Bean 之后,会往 singletonFactories 塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean

    在这里插入图片描述

    spring利用singletonObjects, earlySingletonObjects, singletonFactories三级缓存去解决的,所说的缓存其实也就是三个Map

    1.2 三级缓存各个存放对象

    三级缓存各个存放对象:

    • 一级缓存singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
    • 二级缓存earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
    • 三级缓存singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存

    这三个 map 是如何获取配合的:

    1. 获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2。
    2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
    3. singletonFactories (三级缓存)通过BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,放置到二级缓存earlySingletonObjects 中,并把三级缓存中给移除掉。
    4. 如果三个缓存都没找到,则返回 null


      在这里插入图片描述

    可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects和三级缓存singletonFactory,一级缓存可以进行忽略。前面我们讲过先实例化的bean会通过ObjectFactory半成品提前暴露在三级缓存中

    在这里插入图片描述

    singletonFactory是传入的一个匿名内部类,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。

    1.3 解决循环依赖条件

    1.3.1 解决循环依赖条件

    Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

    • 必须是单例
      依赖的 Bean 必须都是单例
      因为原型模式都需要创建新的对象,不能跟用以前的对象
    • 不能全是构造器注入
      依赖注入的方式,必须不全是构造器注入,且 beanName字母顺序在前的不能是构造器注入
      在 Spring 中创建 Bean 分三步:
      实例化,createBeanInstance,就是 new 了个对象
      属性注入,populateBean, 就是 set 一些属性值
      初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等
      明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。
      如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B 。但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。
      为什么找不到?因为 A 还没 new 完呢,所以找到不完整的 A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖
    • 一个set注入,一个构造器注入能否成功
      假设我们 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
      我们来分析下:实例化 A 之后,可以在 map 中存入 A,开始为 A 进行属性注入,发现需要 B,此时 new B,发现构造器需要 A,此时从 map 中得到 A ,B 构造完毕。
      B 进行属性注入,初始化,然后 A 注入 B 完成属性注入,然后初始化 A。
      整个过程很顺利,没毛病
      假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
      我们来分析下:实例化 A,发现构造函数需要 B, 此时去实例化 B。
      然后进行 B 的属性注入,从 map 里面找不到 A,因为 A 还没 new 成功,所以 B 也卡住了,然后就 失败
      看到这里,仔细思考的小伙伴可能会说,可以先实例化 B 啊,往 map 里面塞入不完整的 B,这样就能成功实例化 A 了啊
      确实,思路没错但是 Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面

    现在我们总结一下:

    • 如果循环依赖都是构造器注入,则失败
    • 如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟BeanName的字母序有关系

    1.3.2 Sprin中Bean的顺序

    spring容器载入bean顺序是不确定的,在一定的范围内bean的加载顺序可以控制。
    spring容器载入bean虽然顺序不确定,但遵循一定的规则:

    • 按照字母顺序加载(同一文件夹下按照字母顺序;不同文件夹下,先按照文件夹命名的字母顺序加载)
    • 不同的bean声明方式不同的加载时机,顺序总结:@ComponentScan > @Import > @Bean
      这里的ComponentScan@ComponentScan及其子注解,Bean指的是@configuration + @bean
    • 同时需要注意的是:
      • Component及其子注解申明的bean是按照字母顺序加载的
      • @configuration + @bean是按照定义的顺序依次加载的
      • @import的顺序,就是bean的加载顺序
      • xml中,通过<bean id="">方式声明的bean也是按照代码的编写顺序依次加载的
      • 同一类中加载顺序:Constructor >> @Autowired >> @PostConstruct >> @Bean
      • 同一类中加载顺序:静态变量 / 静态代码块 >> 构造代码块 >> 构造方法(需要特别注意的是静态代码块的执行并不是优先所有的bean加载,只是在同一个类中,静态代码块优先加载)

    1.3.3 更改加载顺序

    特别情况下,如果想手动控制部分bean的加载顺序,有如下方法:

    1.3.3.1 构造方法依赖 (推荐)

    @Component
    public class CDemo1 {
        private String name = "cdemo 1";
    
        public CDemo1(CDemo2 cDemo2) {
            System.out.println(name);
        }
    }
    
    @Component
    public class CDemo2 {
        private String name = "cdemo 2";
    
        public CDemo2() {
            System.out.println(name);
        }
    }
    

    CDemo2CDemo1之前被初始化。

    注意
    要有注入关系,如:CDemo2通过构造方法注入到CDemo1中,若需要指定两个没有注入关系的bean之间优先级,则不太合适(比如我希望某个bean在所有其他的Bean初始化之前执行)
    循环依赖问题,如过上面的CDemo2的构造方法有一个CDemo1参数,那么循环依赖产生,应用无法启动
    另外一个需要注意的点是,在构造方法中,不应有复杂耗时的逻辑,会拖慢应用的启动时间

    1.3.3.2 参数注入

    @Bean标注的方法上,如果传入了参数,springboot会自动会为这个参数在spring上下文里寻找这个类型的引用。并先初始化这个类的实例。
    利用此特性,我们也可以控制bean的加载顺序。

    @Bean
    public BeanA beanA(BeanB beanB){
        System.out.println("bean A init");
        return new BeanA();
    }
    
    @Bean
    public BeanB beanB(){
        System.out.println("bean B init");
        return new BeanB();
    }
    

    以上结果,beanB先于beanA被初始化加载。
    需要注意的是,springboot会按类型去寻找。如果这个类型有多个实例被注册到spring上下文,那就需要加上@Qualifier(“Bean的名称”)来指定

    1.3.3.3 @DependsOn(“xxx”)

    没有直接的依赖关系的,可以通过@DependsOn注解,我们可以在bean A上使用@DependsOn注解 ,告诉容器bean B应该优先被加载初始化。
    不推荐的原因:这种方法是通过bean的名字(字符串)来控制顺序的,如果改了bean的类名,很可能就会忘记来改所有用到它的注解,那就问题大了。

    当一个bean需要在另一个bean实例化之后再实例化时,可使用这个注解。

    @Component("dependson02")
    public class Dependson02 {
     
        Dependson02(){
            System.out.println(" dependson02 Success ");
        }
    }
    
    @Component
    @DependsOn("dependson02")
    public class Dependson01 {
     
        Dependson01(){
            System.out.println("Dependson01 success");
        }
    }
    
    执行结果:
    dependson02 Success 
    Dependson01 success
    

    1.3.3.4 BeanDefinitionRegistryPostProcessor接口

    通过实现BeanDefinitionRegistryPostProcessor接口,在postProcessBeanDefinitionRegistry方法中通过BeanDefinitionRegistry获取到所有bean的注册信息,将bean保存到LinkedHashMap中,并从BeanDefinitionRegistry中删除,然后将保存的bean定义排序后,重新再注册到BeanDefinitionRegistry中,即可实现bean加载顺序的控制。

    参考于:https://blog.csdn.net/u014365523/article/details/127101157

    1.3.4 执行顺序@Order

    注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@OrderOrdered接口的影响,@Order不控制Spring初始化顺序

    @Order(1)order的值越小越是最先执行,但更重要的是最先执行的最后结束

    以下内容选自官网:
    https://docs.spring.io/spring-framework/docs/5.3.24/reference/html/core.html#spring-core

    目标bean可以实现org.springframework.core.Ordered接口,如果希望数组或列表中的项按特定顺序排序,也可以使用@Order或标准@Priority注释。否则,它们的顺序将遵循容器中相应目标bean定义的注册顺序。
    您可以在目标类级别和@Bean方法上声明@Order注释,可能用于单个bean定义(在使用相同bean类的多个定义的情况下)。@Order值可能会影响注入点的优先级,但请注意,它们不会影响单例启动顺序,这是由依赖关系和@DependsOn声明确定的正交关注点。
    注意,标准的javax.annotation.Priority注释在@Bean级别上是不可用的,因为它不能在方法上声明。它的语义可以通过在每个类型的单个bean上结合@Order值和@Primary来建模。

    @Component
    @Order(0)
    public class Test01 {
       ...
    }
    
    @Component
    @Order(1)
    public class Test02 {
       ...
    }
    
    @Component
    @Order(2)
    public class Test03 {
       ...
    }
    

    如上述代码所示,通过@Order注解定义优先级,3个Bean对象从IOC容器中的执行载顺序为:Test01、Test02、Test03

    1.3.5 延迟注入@Lazy

    假设有如下情景:

    类A依赖于类B,同时类B也依赖于类A。这样就形成了循环依赖。

    为了解决这个问题,还以可以使用 @Lazy 注解,将类A或类B中的其中一个延迟加载。
    例如,我们可以在类A中使用 @Lazy 注解,将类A延迟加载,这样在启动应用程序时,Spring容器不会立即加载类A,而是在需要使用类A的时候才会进行加载。这样就避免了循环依赖的问题。

    示例代码如下:

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

    在类A中,我们使用了 @Lazy 注解,将类B延迟加载。这样在启动应用程序时,Spring容器不会立即加载类B,而是在需要使用类B的时候才会进行加载。
    这样就避免了类A和类B之间的循环依赖问题

    1.4 循环依赖示例说明

    我们假设现在有这样的场景AService依赖BServiceBService依赖AService

    1. AService首先实例化,实例化通过ObjectFactory半成品暴露在三级缓存中
    2. 填充属性BService,发现BService还未进行过加载,就会先去加载BService
    3. 在加载BService的过程中,实例化,也通过ObjectFactory半成品暴露在三级缓存
    4. 填充属性AService,(从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在, 把A放⼊⼆级缓存,同时删除三级缓存中的A ,此时,B已经实例化并且初始化完成,把B放入⼀级缓存)这时候能够从三级缓存中拿到半成品的ObjectFactory
      image.png

    拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果beanAOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例

    1. 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存
    2. 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象

    注意: B注入的半成品A对象只是一个引用,所以之后A初始化完成后,B这个注入的A就随之变成了完整的A

    1.5 是否可以移除二级缓存

    我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题

    只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个beanAOP进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下beanAOP进行了切面代理的场景

    image.png

    我们发现AServicetestAopProxyAOP代理了,看看传入的匿名内部类的getEarlyBeanReference返回的是什么对象。

    image.png

    发现singletonFactory.getObject()返回的是一个AService的代理对象,还是被CGLIB代理的。再看一张再执行一遍singletonFactory.getObject()返回的是否是同一个AService的代理对象

    image.png

    我们会发现再执行一遍singleFactory.getObject()方法又是一个新的代理对象,这就会有问题了,因为AService是单例的,每次执行singleFactory.getObject()方法又会产生新的代理对象。

    假设这里只有一级和三级缓存的话,每次从三级缓存中拿到singleFactory对象,执行getObject()方法又会产生新的代理对象,这是不行的,因为AService是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了singleFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍singletonFactory.getObject()方法再产生一个新的代理对象,保证始终只有一个代理对象。还有一个注意的点

    image.png

    既然singleFactory.getObject()返回的是代理对象,那么注入的也应该是代理对象,我们可以看到注入的确实是经过CGLIB代理的AService对象。所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象

    相关文章

      网友评论

          本文标题:Spring Bean生命周期之三级缓存循环依赖

          本文链接:https://www.haomeiwen.com/subject/zuevedtx.html