书接上文,我们已经把对象交给了Spring,类被注册为Bean,依赖由容器注入,一切看起来都非常优雅
那么问题来了,这些Bean到底是在什么时候被创建的?
是项目启动的一瞬间?是第一次被使用的时候?依赖注入发生再构造方法之前,还是之后?
要回答这些问题,就必须走进Spring IoC的幕后,看一看一个Bean从类到可用对象到底经历了什么?
一、Bean是什么时候创建的?
1.1 默认情况:容器启动时创建
在Spring中,如果不做任何特殊配置,作用域是singleton且为非懒加载@Lazy
此时,Bean会在ApplicationContext启动阶段被创建
也就是说,当你写下
java
SpringApplication.run(Application.class, args);
Spring在指定run()的过程中,就已经完成了Bean扫描、Bean定义注册、Bean实例化、依赖注入
当容器启动完成时,大多数Bean已经是可直接使用的状态
1.2 哪些Bean不是启动时创建?
主要有三类情况
(1)原型Bean
java
@Scope("prototype")
@Component
public class User {
}
这种Bean的特点是:
- 每次getBean()都会创建一个新对象
- Spring只负责创建、不负责销毁
它的生命周期可以简单理解为:创建->使用->JVM回收
(2)懒加载Bean
java
@Lazy
@Service
public class UserService {
}
被@Lazy修饰的Bean不会在容器启动时创建,第一次被使用时才会实例化
就很像我们常说的"懒汉模式"
需要注意的是:@Lazy并不是"绝对延迟创建",而是"不主动创建"
(3)被依赖触发创建
即使是懒加载Bean,如果它被非懒加载Bean依赖,它依然会在容器启动阶段被创建
java
@Service
public class OrderService {
@Autowired
private UserService userService; // UserService 是 @Lazy 的
}
此时,OrderService是非懒加载,它在启动时需要UserService,Spring为了完成依赖注入,必须提前创建UserService
你可以把它理解为:你标注了今天不上班,但你的同事需要你来完成他的前置任务,那你还是需要来上班
所以@Lazy 只能保证"不主动创建",不能保证"绝不提前创建"
二、Bean的完整生命周期
理解Bean生命周期,关键不在于"背步骤",而在于抓住这几个关键节点
从宏观上看,一个Bean会经历以下阶段
Bean定义 -> 实例化 -> 依赖注入 -> 初始化 -> 可使用 -> 销毁
下面我们按时间顺序拆开来看
2.1 BeanDefiniton:还没创建对象之前
在真正new对象之前,Spring做的第一件事是:为Bean建立一份说明书
这份说明书就是BeanDefintion,它记录了Bean的Class、作用域、是否懒加载、依赖关系、初始化/销毁方法
此时类已经被扫描,对象还没创建
2.2 实例化:真正创建对象
当Spring决定要创建某个Bean时,才会进入实例化阶段
这个阶段做的事情只有一件:调用构造方法,创建对象实例
此时对象已经存在,但内部依赖还没有注入
也就是说,构造方法会先执行,字段上的@Autowired还没生效
2.3 依赖注入
实例化完成后,Spring会开始为Bean注入依赖
这个阶段会处理@Autowired、@Resource、构造方法参数、Setter方法
也正是这个阶段,Bean之间真正连了起来
2.4 Aware接口回调
如果Bean实现了一些Aware接口,Spring会在此阶段调用对应方法,把"容器信息"注入进来
这部分了解即可
2.5 初始化阶段
重头戏来了!!
Bean进入了初始化阶段,这个阶段可能触发三类东西(按顺序):
- @PostConstruct方法
- InitializingBean#afterPropertiesSet
- 自定义初始化方法
此时Bean的特点是:所有依赖已注入,可以安全使用其他Bean,非常适合做资源初始化
2.6 Bean就绪
经过初始化后,Bean才算真正进入可被外部使用的状态
此时,容器中拿到的就是最终对象,AOP代理也已经完成
2.7 Bean的销毁阶段
当容器关闭时,singleton Bean会进入销毁阶段;原型Bean不会销毁
销毁阶段会依次调用:
- @PreDestroy
- DisposableBean#destroy
- 自定义 destroy-method
三、为什么理解声明周期这么重要?
这里介绍了一大堆乱七八糟的创建啊销毁啊,看着就头痛,不禁会问,为啥呢?
因为很多"诡异问题",本质都是生命周期问题:
- 构造方法里用不到依赖?
- @PostConstruct能用依赖?
- @Lazy为什么没生效?
- 循环依赖为什么能解决?
答案全在生命周期顺序中
四、循环依赖是什么
既然前面提到了循环依赖,这里就来介绍一下
在日常开发中,我们很容易写出下面这种结构:
java
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
A依赖B,B又依赖A
站在人的角度看,这似乎没有什么问题,反正最后大家都在Spring容器里,互相拿一下不就好了?
但站在Spring创建对象的视角,这其实是一个致命问题:A在创建过程中需要B,而B在创建过程中又需要A,两个对象都在"没创建完"的状态下,互相等待
这就是循环依赖
4.1 如何解决呢?
这里先给结论:Spring只能解决单例+Setter/字段注入的循环依赖,解决不了构造方法注入的循环依赖
稍安勿躁,我们来一步一步解释原因
4.2 Spring是如何拆开循环依赖的?
要理解Spring是怎么解决循环依赖的,我们必须回到一个关键事实:Spring并不是等Bean完全创建好之后,才允许它被别的Bean使用,而是在合适的时机,把半成品Bean提前暴露出去
先看Spring能解决的情况
4.3 Spring能解决的循环依赖(字段 / Setter注入)
java
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
这两个Bean都是singleton,非构造方法注入
下面我们按时间顺序,一步步走Spring的真实流程
4.3.1 Spring开始创建AService
- Spring决定创建AService
- 调用构造方法
- AService对象诞生
此时的AService是一个半成品对象,对象存在、依赖未注入、初始化未执行
4.3.2 提前暴露AService
就在这一步,Spring做了一件非常反直觉但是关键的事情:把这个还没初始化完成的AService提前放进容器的缓存中
这一步在Spring内部叫做提前暴露引用
4.3.3 Spring发现AService依赖BService
接下来,Spring 发现 AService 需要 BService------>转而去创建 BService------>调用 BService 构造方法------>BService 对象诞生
4.3.4 BService依赖AService
此时,Spring要给BService注入AService,于是,Spring去容器中查找AService,发现这个AService已经存在,会直接把这个AService注入给BService。
此时,BService已经创建完成
4.3.5 回到AService,完成最后注入
现在,BService已经是完整对象,Spring回到AService,将BService注入进AService,AService创建完成
4.4 总结一下
Spring能解决循环依赖的前提,是Bean在依赖注入之前,就已经有了一个真实存在的对象实例
也就是说,构造方法先执行,对象先"出生",再慢慢补齐依赖
这正是字段、Setter注入能被解决的原因
4.5 为什么构造方法造成的循环依赖不行?
现在来看Spring解决不了的情况
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
这一次,依赖关系发生在构造方法参数中
问题就出现在构造方法注入的核心特点是:对象一出生,就必须拿到所有依赖
但Spring的提前暴露发生在构造方法执行之后,现在流程变成了:
- 创建AService,需要BService
- 创建BService,需要AService
- 但此时AService还没构造完成,BService也没构造完成,连"半成品对象"都不存在
Spring连插手的机会都没有,于是只能抛出异常,循环依赖宣告失败
这也是为什么Spring官方推荐使用构造方法注入
4.6 补充说明
再补充两个容易踩坑的点:
- 只有singleton才能解决循环依赖,因为prototype bean每次都要创建新对象,因此无法提前暴露
- @Lazy有时能绕开循环依赖,如果把循环依赖的其中一个Bean标记为@Lazy,则会延迟这个Bean的创建,实际上是打破了创建顺序,没有真正改变依赖结构
FINISH!!我们下篇继续!