在使用 spring
框架的日常开发中, bean
之间的循环依赖太频繁了, spring
已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下 spring
是如何解决bean
之间循环依赖,为什么要使用到三级缓存,而不是二级缓存?
点击了解 Spring Bean生命周期之概述
必须先对bean
的生命周期做了一个整体的流程分析,对spring
如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring
中生成,则会跑去生成属性对象实例。
我们可以看到填充属性的时候,spring
会提前将已经实例化的bean
通过ObjectFactory
半成品暴露出去,为什么称为半成品是因为这时候的bean
对象实例化,但是未进行属性填充,是一个不完整的bean
实例对象
在实例化 Bean
之后,会往 singletonFactories
塞入一个工厂,而调用这个工厂的 getObject
方法,就能得到这个 Bean
spring
利用singletonObjects, earlySingletonObjects, singletonFactories
三级缓存去解决的,所说的缓存其实也就是三个Map
三级缓存各个存放对象:
一级缓存
,singletonObjects
,存储所有已创建完毕的单例 Bean (完整的 Bean)二级缓存
,earlySingletonObjects
,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean三级缓存
,singletonFactories
,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存这三个 map 是如何获取配合的:
Bean
的时候会通过 BeanName
先去 singletonObjects
(一级缓存) 查找完整的 Bean
,如果找到则直接返回,否则进行步骤 2。Bean
是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects
(二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3singletonFactories
(三级缓存)通过BeanName
查找到对应的工厂,如果存着工厂则通过工厂创建 Bean
,放置到二级缓存earlySingletonObjects
中,并把三级缓存中给移除掉。可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects
和三级缓存singletonFactory
,一级缓存可以进行忽略。前面我们讲过先实例化的bean
会通过ObjectFactory
半成品提前暴露在三级缓存中
singletonFactory
是传入的一个匿名内部类,调用ObjectFactory.getObject()
最终会调用getEarlyBeanReference
方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。
在 Spring
中,只有同时满足以下两点才能解决循环依赖的问题:
Bean
必须都是单例beanName
的字母顺序
在前的不能是构造器注入实例化
,createBeanInstance,就是 new 了个对象属性注入
,populateBean, 就是 set 一些属性值初始化
,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等A(B b)
,那表明在 new
的时候,就需要得到 B,此时需要 new B
。但是 B 也是要在构造的时候注入 A ,即B(A a)
,这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。Spring
无法处理循环依赖假设我们 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面
现在我们总结一下:
BeanName
的字母序有关系spring
容器载入bean
顺序是不确定的,在一定的范围内bean
的加载顺序可以控制。
spring
容器载入bean
虽然顺序不确定,但遵循一定的规则:
bean
声明方式不同的加载时机,顺序总结:@ComponentScan > @Import > @Bean
ComponentScan
指@ComponentScan
及其子注解,Bean
指的是@configuration + @bean
Component
及其子注解申明的bean
是按照字母顺序加载的@configuration + @bean
是按照定义的顺序依次加载的@import
的顺序,就是bean
的加载顺序xml
中,通过
方式声明的bean
也是按照代码的编写顺序依次加载的Constructor >> @Autowired >> @PostConstruct >> @Bean
bean
加载,只是在同一个类中,静态代码块优先加载)特别情况下,如果想手动控制部分bean
的加载顺序,有如下方法:
@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参数,那么循环依赖产生,应用无法启动
另外一个需要注意的点是,在构造方法中,不应有复杂耗时的逻辑,会拖慢应用的启动时间
在@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的名称”)
来指定
没有直接的依赖关系的,可以通过@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
通过实现BeanDefinitionRegistryPostProcessor
接口,在postProcessBeanDefinitionRegistry
方法中通过BeanDefinitionRegistry
获取到所有bean
的注册信息,将bean
保存到LinkedHashMap
中,并从BeanDefinitionRegistry
中删除,然后将保存的bean
定义排序后,重新再注册到BeanDefinitionRegistry
中,即可实现bean
加载顺序的控制。
参考于:https://blog.csdn.net/u014365523/article/details/127101157
注解@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
假设有如下情景:
类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之间的循环依赖问题
我们假设现在有这样的场景AService
依赖BService
,BService
依赖AService
AService
首先实例化,实例化通过ObjectFactory
半成品暴露在三级缓存中BService
,发现BService
还未进行过加载,就会先去加载BService
BService
的过程中,实例化,也通过ObjectFactory
半成品暴露在三级缓存AService
,(从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在, 把A放⼊⼆级缓存,同时删除三级缓存中的A
,此时,B已经实例化并且初始化完成,把B放入⼀级缓存)这时候能够从三级缓存中拿到半成品的ObjectFactory
ObjectFactory
对象后,调用ObjectFactory.getObject()
方法最终会调用getEarlyBeanReference()
方法,getEarlyBeanReference
这个方法主要逻辑大概描述下如果bean
被AOP
切面代理则返回的是beanProxy
对象,如果未被代理则返回的是原bean实例
注意
: B注入的半成品A对象只是一个引用,所以之后A初始化完成后,B这个注入的A就随之变成了完整的A
我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题
只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个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()
方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象