【JavaEE】Spring IoC(二)

书接上文,我们已经把对象交给了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不会销毁

销毁阶段会依次调用:

  1. @PreDestroy
  2. DisposableBean#destroy
  3. 自定义 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
  1. Spring决定创建AService
  2. 调用构造方法
  3. 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的提前暴露发生在构造方法执行之后,现在流程变成了:

  1. 创建AService,需要BService
  2. 创建BService,需要AService
  3. 但此时AService还没构造完成,BService也没构造完成,连"半成品对象"都不存在

Spring连插手的机会都没有,于是只能抛出异常,循环依赖宣告失败

这也是为什么Spring官方推荐使用构造方法注入

4.6 补充说明

再补充两个容易踩坑的点:

  1. 只有singleton才能解决循环依赖,因为prototype bean每次都要创建新对象,因此无法提前暴露
  2. @Lazy有时能绕开循环依赖,如果把循环依赖的其中一个Bean标记为@Lazy,则会延迟这个Bean的创建,实际上是打破了创建顺序,没有真正改变依赖结构

FINISH!!我们下篇继续!

相关推荐
民乐团扒谱机9 小时前
【微实验】MATLAB 仿真实战:多普勒效应 —— 洒水车音乐的音调变化仿真
开发语言·matlab·多普勒效应·多普勒频移
寻星探路9 小时前
【Python 全栈测开之路】Python 基础语法精讲(一):常量、变量与运算符
java·开发语言·c++·python·http·ai·c#
朔北之忘 Clancy9 小时前
2020 年 6 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·算法·青少年编程·题解
行百里er9 小时前
代码跑得慢?让Spring的StopWatch告诉你真相!
java·后端·github
csbysj20209 小时前
组合实体模式
开发语言
又是忙碌的一天9 小时前
SpringMVC响应
java·服务器·数据库
万物皆字节9 小时前
Spring Cloud Gateway 启动流程源码分析
java·开发语言·spring boot
问水っ9 小时前
Qt Creator快速入门 第三版 第16-7章 其他内容
开发语言·qt
C_心欲无痕9 小时前
ts - 关于Object、object 和 {} 的解析与区别
开发语言·前端·javascript·typescript