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

发布时间 2023-06-04 18:19:46作者: 上善若泪

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
    在这里插入图片描述
    拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果beanAOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例
  5. 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存
  6. 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象

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

1.5 是否可以移除二级缓存

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

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

我们发现AServicetestAopProxyAOP代理了,看看传入的匿名内部类的getEarlyBeanReference返回的是什么对象。
在这里插入图片描述

发现singletonFactory.getObject()返回的是一个AService的代理对象,还是被CGLIB代理的。再看一张再执行一遍singletonFactory.getObject()返回的是否是同一个AService的代理对象
在这里插入图片描述

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

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

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