文章目录
-
- 1 Bean 创建流程
-
- 1.1 Bean的扫描注册
- 1.2 创建Bean的顺序
-
- 1.2.1 存在依赖关系
- 1.2.2 不存在依赖关系
- 2 三种Bean注入方式
-
- 2.1 构造器注入 | Constructor Injection(推荐)
- 2.2 字段注入 | Field Injection(常用)
- 2.3 方法注入 | Setter Injection
- 2.4 三种方式注入顺序
- 3 循环依赖
-
- 3.1 构造器注入
- 3.2 字段/setter注入
-
- 3.2.1 三级缓存实现
- 3.2.2 三级缓存的工作原理:
- 3.2.3 具体例子
- 3.2.4 为何要第三级缓存ObjectFactory?
- 3.3 乱想:构造器+字段?
- 3.4 解决方案
1 Bean 创建流程
简单来说,当容器里要放的Bean很多时,Spring会优先创建依赖最少的Bean。本文主要考虑单例模式。
1.1 Bean的扫描注册
Spring启动后首先会根据SpringbootApplication的包扫描配置扫描包里的所有文件,然后将使用了注解标记的类(如@Component、@Service、@Repository、@Configuration
等)和xml文件里定义的Bean生成BeanDefinition
对象注册到上下文ApplicationContext
中,该对象包括Bean的名称、类型、作用域、构造函数参数、依赖等信息。
1.2 创建Bean的顺序
1.2.1 存在依赖关系
Spring在创建Bean之前会先通过BeanDefinition
分析Bean之间的依赖关系,通常使用有向无环图DAG构建,若BeanA依赖BeanB那么会有一条由BeanB指向BeanA的有向边。根据DAG获得拓扑排序,优先对入度为0(或最少)的Bean节点初始化,这样才能尽量确保在创建某个Bean时,它依赖的Bean已经存在。
在创建Bean的时候,Spring仍会检查它需要的依赖是否已经存在,如果存在则直接注入,如果依赖Bean还没创建,那么会去递归创建依赖的Bean,直到所有依赖都被创建,再创建当前Bean。
1.2.2 不存在依赖关系
对于不存在依赖关系的Bean,Spring会按照以下顺序加载Bean:
- XML配置文件里按照Bean的定义顺序加载;
@ComponentScan
及其子注解(@Component、@Service、@Repository
)声明的Bean按照字母顺序加载;- 配置类中
@Import
按照导入的顺序加载; - 配置类@Bean方法定义的Bean按照方法的声明顺序加载。
如果要人为控制Bean的加载顺序,可以使用@order
或@DependsOn
注解。
2 三种Bean注入方式
2.1 构造器注入 | Constructor Injection(推荐)
构造器注入是在组件的构造函数中注入所需的依赖,它是在Bean创建时就注入依赖,创建流程如1.2。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class BeanA {private final BeanB beanB;private final BeanC beanC;public BeanA(BeanB beanB, BeanC beanC) {this.beanB = beanB;this.beanC = beanC;}// 其他方法...
}@Component
public class BeanB {// 其他方法...
}@Component
public class BeanC {// 其他方法...
}
使用构造器进行依赖注入时,依赖的对象通常会被声明为final
,这样当对象创建后,依赖的Bean不会被改变,可以保证类的一致性。
这样注入的优势是能使Bean之间的依赖关系更加清楚,避免了字段注入可能存在的隐式依赖,如果存在问题(比如循环依赖)会在Spring初始化时就抛出异常,而不会等到执行时才出错。
需要注意的是不能显示提供无参构造函数,否则Spring会优先执行无参构造,导致所有依赖的Bean都为null,如果有多个构造函数,选择一个使用@Autowired
注解,否则可能报错。
2.2 字段注入 | Field Injection(常用)
字段注入就是使用@Autowired
注解自动注入依赖的Bean。它不会在构造函数中注入,而是通过反射在组件构造函数执行后才注入依赖Bean(即Bean实例化完成后才注入依赖项),因此不能使用final修饰依赖Bean,因为使用final字段修饰的变量必须在声明时或在构造函数中初始化,而字段注入在构造函数之后执行。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class BeanA {@Autowiredprivate BeanB beanB;
}@Component
public class BeanB {// 其他方法...
}
字段注入是开发时最常用的方式,但由于字段注入是在Bean实例后才注入,属于隐式依赖,所以可能会存在空指针问题,而这个问题只有当程序运行时才出现,因此有一定隐患,所以Spring官方更推荐构造器注入。
2.3 方法注入 | Setter Injection
和字段注入类似,只是需要写好一个setter函数,在setter中注入依赖,一个setter方法通常对应一个依赖,@Autowired
注解写在setter方法上:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component ![qwqw](https://i-blog.csdnimg.cn/direct/28c3f979e67f4abab03bc2ab850b2837.png#pic_center)
public class BeanA {private final BeanB beanB;private final BeanC beanC;@Autowiredpublic setBeanB(BeanB beanB) {this.beanB = beanB;}@Autowiredpublic setBeanC(BeanC beanC) {this.beanC = beanC;}
setter注入的优势是可以灵活注入bean,相比构造器一次性写入更加清晰一些,缺点和字段注入类似。
2.4 三种方式注入顺序
如果一个组件中同时存在以上三种注入方式,执行顺序是?
按照构造器注入–>字段注入–>方法注入的原则执行。
首先执行构造函数,注入在构造函数初始化的Bean。构造函数执行结束后,Spring将处理字段注入,然后在容器中查找并注入依赖Bean。最后如果存在带有@Autowired
注解的setter方法,Spring会再调用这些方法注入依赖。
3 循环依赖
循环依赖指的是多个Bean之间互相需要对方作为成员变量,导致依赖链变成了环的状态,如BeanA需要注入BeanB,BeanB需要注入BeanA。
3.1 构造器注入
构造器注入时会通过构造函数注入所有必须的依赖,当两个组件BeanA和BeanB之间存在循坏依赖时,执行BeanA的构造函数需要注入BeanB(此时BeanA还未创建),由于BeanB还未生成,因此转而先创建BeanB,执行BeanB的构造函数,而BeanB同样需要注入BeanA,于是出现了死锁情况,两个Bean都无法创建,因此如果使用构造器注入而又出现循环依赖时,Spring会直接抛出BeanCurrentlyInCreationException
异常。
3.2 字段/setter注入
使用字段/setter注入在循环依赖时不会抛出异常(但很容易出问题),主要是通过三级缓存来提前暴露Bean的方式解决的。
一般的Bean需要通过实例化(构造器)、属性填充(注入依赖)、初始化(执行Bean的init方法)才能完成初始化。而在循环依赖的情况下,字段/setter注入会在实例化后先创建当前对象的“代理”或“占位符”实例/引用(半成品实例,但可以被注入到其他Bean中),这个实例可以通过反射等它依赖的对象存在后再进行属性填充,因此不会在创建时出现死锁(没有循环等待),而构造器注入必须注入_完全初始化_的依赖后才能实例化,因此会死锁。
3.2.1 三级缓存实现
三级缓存是解决循环依赖的关键,spring的三级缓存实现在DefaultSingletonBeanRegistry
类中,主要有三个Map,分别是:
- singletonObjects,一级缓存,用于存放完全初始化完成的单例Bean,可以被其他Bean直接引用
- earlySingletonObjects,二级缓存,用于存放提前暴露,即未完全初始化的半成品Bean,目的是为了解决循环依赖
- singletonFactories,三级缓存,用于存放ObjectFactory对象,该对象可以创建半成品Bean放入二级缓存。三级缓存主要用于解决AOP代理对象。
3.2.2 三级缓存的工作原理:
当需要注入某个Bean时,spring通过如下顺序到三级缓存中找:
- 检查一级缓存是否存在完全初始化的Bean,若有则返回;
- 一级缓存未找到且该Bean正在创建中则继续检查二级缓存,否则返回null。二级缓存保存的是半成品Bean,在循环依赖时才用到,若有也直接返回;
- 二级缓存未找到且允许早期依赖则去三级缓存找,否则返回null。如果查询到对象工厂,则会调用工厂的
getObject()
方法生成Bean的早期引用然后将其存入二级缓存,同时将工厂从三级缓存中移除; - 当Bean的生命周期完成后(注入依赖,初始化函数回调),spring会将完全体Bean放入一级缓存,同时移除二三级缓存中的该Bean。
3.2.3 具体例子
以BeanA/B为例:
- 当容器尝试创建BeanA时候,发现它依赖BeanB,但此时在缓存中未存在BeanB,因此会先创建BeanA的
ObjectFactory
对象放入三级缓存中并标记BeanA为正在创建中,然后转而去创建BeanB; - 创建BeanB时,发现其依赖于BeanA,且能够在三级缓存中发现BeanA的工厂对象,那么会使用工厂创建BeanA的半成品对象放入二级缓存,然后清除BeanA在三级缓存中的工厂;
- 半成品BeanA返回给BeanB完成注入,打破循环依赖,BeanB继续初始化生成完全体BeanB放入一级缓存,二三级缓存清除BeanB;
- BeanB创建后返回BeanA完成注入,A继续初始化生成完全体BeanA放入一级缓存,二三级缓存清除BeanA;
- BeanA和BeanB都完成初始化。
3.2.4 为何要第三级缓存ObjectFactory?
在上面的Bean创建流程中可以看到,解决循环依赖时最终还是从二级缓存中找到半成品Bean,那既然如此,为什么在递归创建依赖对象时,不直接先生成当前对象的半成品Bean放入二级缓存,而是先创建当前Bean的工厂对象呢?
这是因为二级缓存只是看似能解决循环依赖,但是存在着以下问题:
- 可能创建多余的半成品对象:若没有三级缓存保存工厂对象,那么在创建某Bean并在递归创建依赖Bean之前就应该创建好半成品Bean,以解决可能存在的循环依赖,但是循环依赖并不是一定存在的,这种方式在无循环依赖的情况下会创建很多无用的半成品Bean,因为它们本来都可以正常初始化直接生成完全Bean。
- 难以支持代理Bean创建:如果某个类使用了AOP特性(或事物),那么这个类被注入时应该使用它的代理对象。若只使用二级缓存,那么要么生成Bean的原始半成品对象,要么生成Bean的代理半成品对象,但无论是哪种模式都不合适,因为前者无法满足代理需求,后者会使得普通对象也注入代理对象,造成不必要的开销。
正是因为这些问题,spring才采取了三级缓存,工厂对象的引入可以很好解决上面两个问题:
- 延迟生成半成品Bean:在三级缓存架构下,首先生成的是对象工厂放入三级缓存,当后续存在循环依赖时才会生成半成品Bean放入二级缓存并被使用。
- 支持动态增强(代理),能生成原始对象或代理对象。在Bean的创建过程中,spring会检测该类是否使用了切面或增强(事物注解),如果是则证明需要代理对象,那么在使用
objectFactory.getObject()
时就会生成代理对象,否则生成原始对象。
3.3 乱想:构造器+字段?
想到了一个组合:如果BeanA使用构造器注入BeanB,而BeanB使用字段注入注入BeanA,那么能否通过占位符(半成品)实例实现注入呢?
答案是不行,原因主要有三点:
- 构造器注入要求注入的依赖必须是完全初始化的实例【核心】。
- 构造器注入时,在循环依赖情况下被动生成的占位符实例不允许使用(因为构造函数不允许注入未完全实例化的对象,本质上与第二点一样)。
- 构造器注入不会像字段注入那样生成占位符实例,因为就算生成了也不完全,无法使用
因此无论是先创建BeanA还是先创建BeanB都会抛出异常。
先创建BeanA:发现依赖BeanB,转而创建BeanB,又发现BeanB依赖BeanA,因此尝试创建BeanA的占位符实例,但是因为A是构造器注入,必须注入BeanB的完整实例(但并不存在),因此不允许使用占位符实例,失败。
先创建BeanB:发现依赖BeanA,转而创建BeanA,BeanA必须使用完全实例化的BeanB,不会创建BeanB的占位符实例,因此无法达成,失败。
3.4 解决方案
一般情况下需要避免循环依赖,如果存在,可以尝试将一些依赖关系移除,重构依赖关系,降低耦合。或者可以使用@Lazy
注解延迟Bean的加载,懒加载可以让Bean在被使用时才注入。
@Component
public class BeanA {@Autowired@Lazyprivate BeanB beanB; // 延迟注入// 其他方法...
}@Component
public class BeanB {@Autowiredprivate BeanA beanA; // 直接注入// 其他方法...
}