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 是如何获取配合的:
- 获取单例
Bean
的时候会通过BeanName
先去singletonObjects
(一级缓存) 查找完整的Bean
,如果找到则直接返回,否则进行步骤 2。 - 看对应的
Bean
是否在创建中,如果不在直接返回找不到,如果是,则会去earlySingletonObjects
(二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3 - 去
singletonFactories
(三级缓存)通过BeanName
查找到对应的工厂,如果存着工厂则通过工厂创建Bean
,放置到二级缓存earlySingletonObjects
中,并把三级缓存中给移除掉。 -
如果三个缓存都没找到,则返回 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);
}
}
CDemo2
在CDemo1
之前被初始化。
注意
:
要有注入关系,如: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
的加载顺序不受@Order
或Ordered接口
的影响,@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
依赖BService
,BService
依赖AService
-
AService
首先实例化,实例化通过ObjectFactory
半成品暴露在三级缓存中 - 填充属性
BService
,发现BService
还未进行过加载,就会先去加载BService
- 在加载
BService
的过程中,实例化,也通过ObjectFactory
半成品暴露在三级缓存 - 填充属性
AService
,(从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在,把A放⼊⼆级缓存,同时删除三级缓存中的A
,此时,B已经实例化并且初始化完成,把B放入⼀级缓存)这时候能够从三级缓存中拿到半成品的ObjectFactory
image.png
拿到ObjectFactory
对象后,调用ObjectFactory.getObject()
方法最终会调用getEarlyBeanReference()
方法,getEarlyBeanReference
这个方法主要逻辑大概描述下如果bean
被AOP
切面代理则返回的是beanProxy
对象,如果未被代理则返回的是原bean实例
- 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存
- 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象
注意
: B注入的半成品A对象只是一个引用,所以之后A初始化完成后,B这个注入的A就随之变成了完整的A
1.5 是否可以移除二级缓存
我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题
只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean
没被AOP
进行切面代理,如果这个bean
被AOP
进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下bean
被AOP
进行了切面代理的场景
我们发现AService
的testAopProxy
被AOP
代理了,看看传入的匿名内部类的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()
方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象
网友评论