Spring IoC Container
前言
作为一个 Java 程序员,平时打交道最多的自然是 Spring,正逢最近有空,借助源码和官方文档仔细温习一下 Spring IoC 容器。
本文中贴出的 Spring 源码均是基于 Spring 6.2.12
IoC 容器
基本概念
个人理解上来说应该拆分为 IoC(控制反转) 和容器:
- 容器:屏蔽掉所有的细节的话,可以用 Map 的概念来代指,简单来说就是我告诉你我需要什么你就给我什么。
- 控制反转:把对象创建和依赖管理的控制权从程序内部转移到外部容器,简单来说就是本来你(对象)自己 new 对象,现在变成别人(容器)给你 new 好送来。
IoC 容器以 Bean 为单位管理应用程序组件,为了后续解读源码方便,最先应该了解的就应该是 Bean 的生命周期。
Bean 生命周期
个人理解是五个阶段:定义、实例化、依赖注入、初始化和销毁
- 定义:通过 BeanDefinition 定义 Bean 的基本配置信息,可以是 Class 也可以是 XML,我接触到的项目都是通过 Class 来定义的。
- 实例化:通过 BeanDefinition 中的配置信息来实例化 Bean,如果是构造器注入,在这一步会像树形结构一样来进行相关参数的 Bean 实例化和初始化。
- 依赖注入:通过 BeanDefinition 中的配置信息来注入 Bean,对于 Class 驱动的配置文件,简单来说就是
@Autowired。 - 初始化:执行一些初始化的方法,比如
@PostConstruct。 - 销毁:这个没啥好说的,接触到的项目基本上都没用到这个生命周期的相关特性。
依赖注入
Spring 官方主要介绍了两种 DI 的方式,构造器注入和 Setter 注入,Spring 官方也对如如何使用两种注入方式做了指引:Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.
针对官方的介绍可以简单列出一些问题:
- 什么是构造器注入?
- 什么是 Setter 注入?
- 接触到的项目使用最多的是
@Autowired, 为什么官方没有提到@Autowired这种注解注入的方式呢?
构造器注入
1 |
|
通过对源码进行分析:
- 触发点:
org.springframework.beans.factory.support.AbstractBeanFactory#getBean(String name) - 入参准备:
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency(...)这里面参数准备实际上又会回到上述的触发点 - 将准备好的参数通过反射强制调用构造函数实例化,之后就是相关的生命周期流程依赖注入和初始化,初始化完成之后标识该 Bean 已可使用
Setter 注入
1 |
|
通过对源码进行分析,Setter注入的流程与 构造器注入 是一致的,不同的点主要在于作用的生命周期不同,构造器注入 起作用的生命周期是实例化有空值检测机制,而 Setter注入 起作用的生命周期是依赖注入无空值检测机制。
@Autowired 在这里的作用实际上更像一个触发器,由它指定使用哪个方法来进行依赖注入,因此跟方法名叫什么没有关系,只是按照 Java 的规范来说 Setter 方法是标准的 Java 对象 属性设置方式。假设 Bean 的属性全都是 Setter 注入 ,那么它的实例化会非常简单,因为根据 Java 的特性会生成一个无参构造函数,实例化之后再根据 @Autowired 这个触发器来进行依赖注入。
@Autowired
这个注解的解析在 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 中,他有两个内部类,分别是 AutowiredFieldElement 和 AutowiredMethodElement,从名字就可以看出来分别支持字段注入和方法注入,两个方式其实大差不差的,都是通过上述构造器注入 中的第二点来准备依赖,他的调用链路最终又会回到 org.springframework.beans.factory.support.AbstractBeanFactory#getBean(String name) 从而形成一个调用链路的闭环。
思考
官方应该是把 构造函数注入 以外的所有注入方式统称为 Setter注入,而 @Autowired 只是一个触发器,并不是一种依赖注入的方式,它告诉 Spring:”这里需要注入依赖”,但真正的注入工作是由 Spring 底层机制完成的,这也是为什么官方只提到了 构造函数注入 和 Setter注入。
那么,这两种注入分别适用什么情况呢?这里引用官方的话:Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. 不过据我目前接触过的项目而言,能做到这点的其实比较少。
循环依赖
循环依赖算是八股文常问的了,Spring 官方虽然单独提了这点,但是却没有做详细的技术解释,只留下了一句简单的:If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario. 这句话其实挺暧昧的,只说了有可能产生一个无法解决的循环依赖,这就延申出了两个问题:
- Spring 是如何解决循环依赖的?
- 哪些循环依赖是可以解决的?哪些循环依赖是不可以解决的?
其实只要弄懂第一点,第二点自然就能想通了。
Spring 如何解决循环依赖的?
以下代码均来自 org.springframework.beans.factory.support.AbstractBeanFactory
1 | protected <T> T doGetBean(String name, Class<T> requiredType, |
getSingleton 这个方法就没什么好解释的了,三个 Map 一级一级往下找,这也就是八股文中广为人知的 三级缓存解决循环依赖。
1 | protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
从源码里面可以注意到关键词 singleton ,稍微对 Bean 的配置有点了解就知道这应该代指的是作用域,初步的结论就是只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。
从 org.springframework.beans.factory.config.ConfigurableBeanFactory 可以看出 Spring 对 Bean 的作用域提供了两种配置
1 | public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry { |
这里涉及到一点设计模式的概念:单例模式和原型模式,为了接下来的内容不产生歧义,请先弄明白这两个设计模式的概念。
解决循环依赖的核心手法:暴露实例的早期引用,基于这个前提就能想明白为什么只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。因为 singleton 每次返回的是同一个 Bean,只要实例化成功了,它的引用就不会发生变化,所以早点暴露出去被引用也无伤大雅。
能够解决哪些循环依赖?
两个发生循环引用的 Bean 之间就可以分为三种情况:singleton 和 singleton 、prototype 和 singleton 、prototype 和 prototype
Singleton 和 Singleton
这种情况不用过多分析,通过源码可以得知就是支持 singleton 的,所以这种情况下的循环依赖是可以被解决的。
Prototype 和 Singleton
这种情况就比较特殊了,需要分情况讨论:
prototypeBean 先实例化:singletonBean 实例化时会想依赖注入prototypeBean,而prototype作用域是不支持暴露早期引用的,自然会依赖注入失败。singletonBean 先实例化:prototypeBean 实例化时会想依赖注入singletonBean,而singleton作用域是支持暴露早期引用的,自然会依赖注入成功。
值得一提的是,从 org.springframework.beans.factory.support.DefaultListableBeanFactory 中可以看出, Spring 启动的时候只会初始化作用域是 singleton 的 Bean。
1 | public void preInstantiateSingletons() throws BeansException { |
基于这些 Bean 再来填充他们的属性进行依赖注入,所以保证了 singleton Bean 先实例化。
Prototype 和 Prototype
这种情况稍微有点奇葩,虽然本质上是不允许的,但可能会启动成功,也可能会启动失败。
- 启动成功:从上一个情况中已经清楚 Spring 在启动阶段只会实例化
singletonBean,如何两个prototypeBean 产生了循环依赖,但没有被任何singletonBean 要求依赖注入,那么他们会逃过一劫从而启动成功。 - 启动失败:自然就是上面的反例了,被
singletonBean 要求依赖注入,Spring 尝试实例化 Bean 时就会发现这个问题。
这种情况下启动成功还是失败不能作为依据,即使启动成功了,也有可能用着用着发现用不了,不过这种情况还是挺罕见的,毕竟绝大多数 Bean 是 singleton,而 prototype 的 Bean 基本上不可避免被 singleton Bean 要求依赖注入。
对循环依赖的一些思考
- 虽然 Spring 官方一定程度上解决了循环依赖的问题,但实际上 Spring 官方并不认同循环依赖的存在,为什么这么说呢?因为官方默认是不允许循环依赖的:
spring.main.allow-circular-references这个配置项默认是false。所以一旦出现了循环依赖,更应该做的是审视自己的模块设计是否合理,而不是依赖于 Spring 解决。 - 为什么需要三级缓存?一级放的是完全初始化好的 Bean,二级放的是“残疾”并且被代理了的 Bean,三级放的是如何获取到被代理了的 Bean。Spring 把 AOP 代理对象的创建时机放在了初始化之后,而发生循环依赖时往往是在依赖注入阶段就需要实例对象,如果有代理要求,注入的就应该是代理对象,如果没有,那么注入原始对象即可,介于这一点产生了第三级缓存,但是一二级缓存是很暧昧的,对于依赖注入来说,并不在乎内部残疾不残疾,我只需要当前依赖的引用就行,实际上 Spring 也确实是这样做的,二级缓存中的实例对象就已经可以暴露出去解决循环依赖了。基于这个前提,按照 Spring 目前的流程,实际上二级缓存就已经能完全解决问题了,要是 Spring 更改 AOP 代理对象的创建时机为实例化之后,甚至可以一级缓存就做到。
异步实例化
相信在 org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons() 中注意到了 CompletableFuture 。是的, Spring 在 6.2.0 版本之后推出了异步实例化功能。
1 | private CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { |
从源码可以看出,Spring 会优先初始化同步的 Bean,之后再去初始化异步的 Bean,不过根据 Bean 的实例化步骤来看,异步的 Bean 要是被同步的 Bean 要求依赖注入了,实际上“异步”就会失效了。如果不考虑同步和异步的先后顺序,那么整个 Bean 的实例化会变得十分复杂且不可控,要是考虑了先后顺序吧,能不能提速也是个问题。这估计也是为什么 Spring 官方一直到 6.2.0 才稍微妥协了一下的原因。
这里推荐一下 why 哥的文章,来龙去脉写地挺详细:13年过去了,Spring官方竟然真的支持Bean的异步初始化了!
