一个小小的里程碑
首先感谢能看到本文的朋友, 感谢你的一路陪伴.
如果每篇都认真看的话, 会发现本系列以 bean 定义作为切入点, 先是详细解说了什么是 bean 定义, 接着又强调了 bean 定义为什么如此重要.
然后又讲了获取 bean 定义详细信息的方法, 接着又讲了 bean 定义注册的若干种方式, 然后是 bean 定义注册方式的实现细节.
最后又以 SpringBoot 应用为例, 从容器启动前, 启动后分两个阶段解说 bean 定义是如何进入到容器里的.
就是 bean 工厂后处理器配合使用 @ComponentScan 注解和 @Import 注解, 一起完了所有 bean 定义的注册.
当 bean 定义注册完毕后, 紧接着就是对单例(singleton)bean 的实例化. 此时容器处在启动中.
之前面试别人时问过一个问题, 为什么 Spring 要在容器启动时就实例化所有单例 bean, 而不是放到首次使用时?
记忆中没有人回答到点上, 原因很简单, 就是为了提前发现潜在的错误. 启动时报错比运行时报错好得多.
OK, 从现在开始将进入一个新的阶段, 即 bean 的实例化, bean 的依赖装配, bean 的初始化.
为了能够深度参与这个过程, 使之更加灵活可配, Spring 引入了 bean 后处理器的概念.
五个 bean 后处理器接口
bean 后处理器主要应用于 bean 的创建过程中的一些操作, 如检测下是否实现了指定接口, 或用一个代理包装下 bean 实例, 把代理返回等等.
首先来看第一个接口, BeanPostProcessor, 如下图 01:
可以看到这是一组对称的方法, 一个是 BeforeInitialization, 一个是 AfterInitialization.
一个在初始化前, 一个在初始化后. 所以首先要搞清楚什么是初始化?
在 Spring 中, 初始化指的是在 bean 实例上执行一个特定的方法, 该方法就称为初始化方法.
编程新说注: 一般情况下, 一个类一个初始化方法就够了, 也可以有多个, Spring 的源码实现是支持的.
那么如何指定这个初始化方法呢? 共有三种方案可选:
1)实现 Spring 提供的一个接口, InitializingBean, 它只有一个方法, 就是 afterPropertiesSet.
2)使用 @Bean 注解注册 bean 定义时, 设置注解的 initMethod 属性为 bean 的一个方法名.
3)使用 java 的注解 @PostConstruct, 把它标在 bean 的一个方法上.
这三种方式指定的方法都是初始化方法, 所谓初始化就是调用这些方法.
所以这个接口的两个方法的调用位置就是:
bean 的实例化 -> bean 的依赖装配 ->接口方法一 (初始化前)-> bean 的初始化方法 -> 接口方法二(初始化后)-> OK.
我们可以看到, 这个接口的切入位置是在 bean 的依赖已经装配好之后, 似乎有些 "晚了", 因为这样只能参与 bean 的初始化, 有没有稍靠前的?
当然有了, 接着看第二个接口, InstantiationAwareBeanPostProcessor, 如下图 02:
接口的名字中有 InstantiationAware, 说明是 "实例化感知" 的 bean 后处理器. 即可以参与到 bean 的实例化过程中. 确实比上一个接口提前了.
接口有三个方法, 一个是 BeforeInstantiation, 一个是 AfterInstantiation, 一个是 Properties. 光从名字上就能看出个七七八八了.
所以这个接口的三个方法的调用位置就是:
bean 的实例化准备阶段 ->接口方法一 (实例化前)-> bean 的实例化 -> 接口方法二 (实例化后)-> 接口方法三(定制 bean 所需的属性值)-> bean 的属性设置 -> OK.
第一个方法在 bean 实例化前调用, 如果返回一个非 null 对象, 则 Spring 就使用这个对象了, 不再进行实例化了.
所以这里可以返回一个目标 bean 的代理, 来压制 (延迟) 目标 bean 的实例化.
这个方法的参数是 bean 的类型, 因为此时还没有 bean 实例呢.
第二个方法在 bean 实例化后且属性设置 (显式的属性设置或依赖的装配) 前调用.
这是一个理想的地方用来执行自定义字段注入, 因为此时 Spring 的自动装配尚未到来.
通常方法返回 true, 如果返回 false, 后续的属性设置将被跳过.
同时, 后面的该接口类型的实例都将不会再在这个 bean 实例上调用.
第三个方法在 bean 属性设置前调用, 可以用来定制即将为 bean 实例设置的属性.
方法 pvs 是传进来的已有属性. 方法默认返回 null. 表示不对属性进行操作.
第四个方法现已经废除, 等它移除后, 第三个方法将默认返回 pvs.
下面看第三个接口, SmartInstantiationAwareBeanPostProcessor, 也是和 bean 创建相关的, 如下图 03:
这个接口也有三个方法:
第一个方法, 用来预测最终的 bean 类型, 这是给我们提供一个修改 bean 类型的机会, 方法的参数是原始的 bean 类型.
如果方法返回 null, 则不进行预测, 按照 Spring 自己的逻辑走.
第二个方法, 用来确定候选的构造方法, 给我们一个定制构造方法的机会, 方法的参数是原始的 bean 类型.
如果方法返回 null, 则不进行指定, 按照 Spring 自己的逻辑去判断出最适合的构造方法.
第三个方法, 用来获取一个早期 bean 实例的引用. 为什么说是早期呢? 因为 bean 实例的初始化方法还没有执行.
编程新说注: 可以认为此时的 bean 还处于一种不完善的状态.
典型的用法是可以用来解决循环引用. 这个地方可以在目标 bean 完全初始化之前较早地暴露一个包装器.
第四个接口, 说完了 bean 的创建, 再来看看 bean 的销毁, DestructionAwareBeanPostProcessor, 如下图 04:
这个接口比较简单, 只有两个方法:
第一个方法, 在 bean 实例销毁前会被调用, 来执行一些定制的销毁代码.
这些销毁代码通常位于一个方法里, 叫做销毁方法, 是与初始化方法对应的.
同样也有三种方式来指定销毁方法:
1)实现 Spring 提供的一个接口, DisposableBean, 它只有一个方法 destroy.
2)使用 @Bean 注解注册 bean 定义时, 设置注解的 destroyMethod 属性为 bean 的一个方法名.
3)使用 java 的注解 @PreDestroy, 把它标在 bean 的一个方法上.
这三种方式指定的都是销毁方法. 如果指定了的话, 就在刚刚的接口方法里调用了.
只有被容器完全管理生命周期的 bean 才会应用, 如 singleton 和 scoped 的 bean 实例.
第二个方法, 就是决定是否要为 bean 实例调用第一个方法来执行一些销毁代码.
返回 true 表示需要, false 表示不需要调用.
因为第五个接口是和 bean 定义有关系的, 所以先来看看 bean 定义的实现类都有哪些.
有几个类需要了解一下, 如下图 05:
1)AbstractBeanDefinition, 是所有 bean 定义的父类.
2)RootBeanDefinition, 是在 xml 配置时代, 注册 bean 定义时用的类.
3)ChildBeanDefinition, 是在 xml 配置时代, 注册 bean 定义时用的类, 必须在配置时指定一个父 bean 定义.
4)GenericBeanDefinition, 在注解配置时代, 推荐使用的 bean 定义类, 可以在运行时动态指定一个父 bean 定义, 也可以不指定.
5)AnnotatedGenericBeanDefinition, 在注解配置时代, 通过编程方式注册 bean 定义时用的类, 继承了 GenericBeanDefinition.
6)ScannedGenericBeanDefinition, 在注解配置时代, 通过扫描 jar 包中. class 文件的方式注册 bean 定义时用的类, 继承了 GenericBeanDefinition.
来分析一下, 第 3 必须有一个父 bean 定义, 第 4 可以有一个父 bean 定义, 第 5,6 继承自第 4. 第 1 是一个抽象类.
所以只有第 2 是一个没有父 bean 定义且非抽象的类, 因此, Spring 会先把 bean 定义转换为第 2. 然后再生成 bean 实例.
因为可能存在父子关系, 所以需要合并 bean 定义. 父子关系其实就是一种 "继承" 和 "重写".
子可以继承父的信息, 也可以重写父的信息, 同样也有些信息不继承, 只使用子自己的.
这种继承可以是多级的, 如 A 继承 B,B 继承 C,C 继承 D.
在合并 bean 定义时, 会把 A,B,C,D 合起来变成一个 M, 但是 ABCD 本身不会再被改变.
合成的 bean 定义 M 会被缓存起来, 就是用它来生成 bean 实例的.
这些基本知识了解了之后, 接下来看最后一个接口.
第五个接口, MergedBeanDefinitionPostProcessor, 如下图 06:
这个接口的主要目的不是用来修改合并后的 bean 定义的, 虽然也可以进行一些修改.
它主要用来进行一些自省操作, 如一些检测, 或在处理 bean 实例之前缓存一些相关的元数据.
这些作用都在第一个方法里实现. 第二个方法是一个通知方法, 当一个 bean 定义被重置时调用.
这个方法用于清除和受影响的 bean 相关的任何元数据.
>>> 品 Spring 系列文章 <<<品 Spring: 帝国的基石
品 Spring:bean 定义上梁山
品 Spring: 实现 bean 定义时采用的 "先进生产力"
品 Spring: 注解终于 "成功上位"
品 Spring: 能工巧匠们对注解的 "加持"
品 Spring:SpringBoot 和 Spring 到底有没有本质的不同?
品 Spring: 负责 bean 定义注册的两个 "排头兵"
品 Spring:SpringBoot 轻松取胜 bean 定义注册的 "第一阶段"
品 Spring:SpringBoot 发起 bean 定义注册的 "二次攻坚战"
品 Spring: 注解之王 @Configuration 和它的一众 "小弟们"
品 Spring:bean 工厂后处理器的调用规则
>>> 热门文章集锦 <<<
毕业 10 年, 我有话说
[面试] 我是如何面试别人 List 相关知识的, 深度有点长文
我是如何在毕业不久只用 1 年就升为开发组长的
爸爸又给 Spring MVC 生了个弟弟叫 Spring webFlux
[面试] 我是如何在面试别人 Spring 事务时 "套路" 对方的
[面试] Spring 事务面试考点吐血整理(建议珍藏)
[面试] 我是如何在面试别人 Redis 相关知识时 "软怼" 他的
[面试] 吃透了这些 Redis 知识点, 面试官一定觉得你很 NB(干货 | 建议珍藏)
[面试] 如果你这样回答 "什么是线程安全", 面试官都会对你刮目相看(建议珍藏)
[面试] 迄今为止把同步 / 异步 / 阻塞 / 非阻塞 / BIO/NIO/AIO 讲的这么清楚的好文章(快快珍藏)
[面试] 一篇文章帮你彻底搞清楚 "I/O 多路复用" 和 "异步 I/O" 的前世今生(深度好文, 建议珍藏)
[面试] 如果把线程当作一个人来对待, 所有问题都瞬间明白了
Java 多线程通关 --- 基础知识挑战
品 Spring: 帝国的基石
作者是工作超过 10 年的码农, 现在任架构师. 喜欢研究技术, 崇尚简单快乐. 追求以通俗易懂的语言解说技术, 希望所有的读者都能看懂并记住. 下面是公众号和知识星球的二维码, 欢迎关注!
来源: https://www.cnblogs.com/lixinjie/p/taste-spring-012.html