前言

作为一个 Java 程序员,平时打交道最多的自然是 Spring,正逢最近有空,借助源码和官方文档仔细温习一下 Spring IoC 容器。

本文中贴出的 Spring 源码均是基于 Spring 6.2.12

IoC 容器

基本概念

个人理解上来说应该拆分为 IoC(控制反转) 和容器:

  1. 容器:屏蔽掉所有的细节的话,可以用 Map 的概念来代指,简单来说就是我告诉你我需要什么你就给我什么。
  2. 控制反转:把对象创建和依赖管理的控制权从程序内部转移到外部容器,简单来说就是本来你(对象)自己 new 对象,现在变成别人(容器)给你 new 好送来。

IoC 容器以 Bean 为单位管理应用程序组件,为了后续解读源码方便,最先应该了解的就应该是 Bean 的生命周期。

Bean 生命周期

个人理解是五个阶段:定义、实例化、依赖注入、初始化和销毁

  1. 定义:通过 BeanDefinition 定义 Bean 的基本配置信息,可以是 Class 也可以是 XML,我接触到的项目都是通过 Class 来定义的。
  2. 实例化:通过 BeanDefinition 中的配置信息来实例化 Bean,如果是构造器注入,在这一步会像树形结构一样来进行相关参数的 Bean 实例化和初始化。
  3. 依赖注入:通过 BeanDefinition 中的配置信息来注入 Bean,对于 Class 驱动的配置文件,简单来说就是 @Autowired
  4. 初始化:执行一些初始化的方法,比如 @PostConstruct
  5. 销毁:这个没啥好说的,接触到的项目基本上都没用到这个生命周期的相关特性。

依赖注入

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.

针对官方的介绍可以简单列出一些问题:

  1. 什么是构造器注入?
  2. 什么是 Setter 注入?
  3. 接触到的项目使用最多的是 @Autowired , 为什么官方没有提到 @Autowired 这种注解注入的方式呢?

构造器注入

1
2
3
4
5
6
7
8
@Component
public class AService {
private final BService bService;

public AService(BService bService) {
this.bService = bService;
}
}

通过对源码进行分析:

  1. 触发点:org.springframework.beans.factory.support.AbstractBeanFactory#getBean(String name)
  2. 入参准备:org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency(...) 这里面参数准备实际上又会回到上述的触发点
  3. 将准备好的参数通过反射强制调用构造函数实例化,之后就是相关的生命周期流程依赖注入和初始化,初始化完成之后标识该 Bean 已可使用

Setter 注入

1
2
3
4
5
6
7
8
9
@Component
public class AService {
private BService bService;

@Autowired
public void setBService(BService bService) {
this.bService = bService;
}
}

通过对源码进行分析,Setter注入的流程与 构造器注入 是一致的,不同的点主要在于作用的生命周期不同,构造器注入 起作用的生命周期是实例化有空值检测机制,而 Setter注入 起作用的生命周期是依赖注入无空值检测机制。

@Autowired 在这里的作用实际上更像一个触发器,由它指定使用哪个方法来进行依赖注入,因此跟方法名叫什么没有关系,只是按照 Java 的规范来说 Setter 方法是标准的 Java 对象 属性设置方式。假设 Bean 的属性全都是 Setter 注入 ,那么它的实例化会非常简单,因为根据 Java 的特性会生成一个无参构造函数,实例化之后再根据 @Autowired 这个触发器来进行依赖注入。

@Autowired

这个注解的解析在 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 中,他有两个内部类,分别是 AutowiredFieldElementAutowiredMethodElement,从名字就可以看出来分别支持字段注入方法注入,两个方式其实大差不差的,都是通过上述构造器注入 中的第二点来准备依赖,他的调用链路最终又会回到 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. 这句话其实挺暧昧的,只说了有可能产生一个无法解决的循环依赖,这就延申出了两个问题:

  1. Spring 是如何解决循环依赖的?
  2. 哪些循环依赖是可以解决的?哪些循环依赖是不可以解决的?

其实只要弄懂第一点,第二点自然就能想通了。

Spring 如何解决循环依赖的?

以下代码均来自 org.springframework.beans.factory.support.AbstractBeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, 
@Nullable Object[] args, boolean typeCheckOnly) throws BeansException {

String beanName = transformedBeanName(name);
Object beanInstance;

// Eagerly check singleton cache for manually registered singletons.
// 关键方法即是 getSingleton(beanName)
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isTraceEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
// 这句日志已经能很明显看出来了,由于循环引用返回了 bean 的早期引用
logger.trace("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
}
else {
logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
}
}
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

// ............
return adaptBeanInstance(name, beanInstance, requiredType);
}

getSingleton 这个方法就没什么好解释的了,三个 Map 一级一级往下找,这也就是八股文中广为人知的 三级缓存解决循环依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock.
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
if (!this.singletonLock.tryLock()) {
// Avoid early singleton inference outside of original creation thread.
return null;
}
try {
// Consistent creation of early reference within full singleton lock.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// Singleton could have been added or removed in the meantime.
if (this.singletonFactories.remove(beanName) != null) {
this.earlySingletonObjects.put(beanName, singletonObject);
}
else {
singletonObject = this.singletonObjects.get(beanName);
}
}
}
}
}
finally {
this.singletonLock.unlock();
}
}
}
return singletonObject;
}

从源码里面可以注意到关键词 singleton ,稍微对 Bean 的配置有点了解就知道这应该代指的是作用域,初步的结论就是只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。

org.springframework.beans.factory.config.ConfigurableBeanFactory 可以看出 Spring 对 Bean 的作用域提供了两种配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {

/**
* Scope identifier for the standard singleton scope: {@value}.
* <p>Custom scopes can be added via {@code registerScope}.
* @see #registerScope
*/
String SCOPE_SINGLETON = "singleton";

/**
* Scope identifier for the standard prototype scope: {@value}.
* <p>Custom scopes can be added via {@code registerScope}.
* @see #registerScope
*/
String SCOPE_PROTOTYPE = "prototype";

//............
}

这里涉及到一点设计模式的概念:单例模式和原型模式,为了接下来的内容不产生歧义,请先弄明白这两个设计模式的概念。

解决循环依赖的核心手法:暴露实例的早期引用,基于这个前提就能想明白为什么只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。因为 singleton 每次返回的是同一个 Bean,只要实例化成功了,它的引用就不会发生变化,所以早点暴露出去被引用也无伤大雅。

能够解决哪些循环依赖?

两个发生循环引用的 Bean 之间就可以分为三种情况:singletonsingletonprototypesingletonprototypeprototype

Singleton 和 Singleton

这种情况不用过多分析,通过源码可以得知就是支持 singleton 的,所以这种情况下的循环依赖是可以被解决的。

Prototype 和 Singleton

这种情况就比较特殊了,需要分情况讨论:

  1. prototype Bean 先实例化: singleton Bean 实例化时会想依赖注入 prototype Bean,而 prototype 作用域是不支持暴露早期引用的,自然会依赖注入失败。
  2. singleton Bean 先实例化: prototype Bean 实例化时会想依赖注入 singleton Bean,而 singleton 作用域是支持暴露早期引用的,自然会依赖注入成功。

值得一提的是,从 org.springframework.beans.factory.support.DefaultListableBeanFactory 中可以看出, Spring 启动的时候只会初始化作用域是 singleton 的 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void preInstantiateSingletons() throws BeansException {
// ......
try {
List<CompletableFuture<?>> futures = new ArrayList<>();
for (String beanName : beanNames) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (!mbd.isAbstract() && mbd.isSingleton()) {
CompletableFuture<?> future = preInstantiateSingleton(beanName, mbd);
if (future != null) {
futures.add(future);
}
}
}
if (!futures.isEmpty()) {
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
}
finally {
this.mainThreadPrefix = null;
this.preInstantiationThread.remove();
}

// .........
}

基于这些 Bean 再来填充他们的属性进行依赖注入,所以保证了 singleton Bean 先实例化。

Prototype 和 Prototype

这种情况稍微有点奇葩,虽然本质上是不允许的,但可能会启动成功,也可能会启动失败。

  1. 启动成功:从上一个情况中已经清楚 Spring 在启动阶段只会实例化 singleton Bean,如何两个 prototype Bean 产生了循环依赖,但没有被任何 singleton Bean 要求依赖注入,那么他们会逃过一劫从而启动成功。
  2. 启动失败:自然就是上面的反例了,被 singleton Bean 要求依赖注入,Spring 尝试实例化 Bean 时就会发现这个问题。

这种情况下启动成功还是失败不能作为依据,即使启动成功了,也有可能用着用着发现用不了,不过这种情况还是挺罕见的,毕竟绝大多数 Bean 是 singleton,而 prototype 的 Bean 基本上不可避免被 singleton Bean 要求依赖注入。

对循环依赖的一些思考

  1. 虽然 Spring 官方一定程度上解决了循环依赖的问题,但实际上 Spring 官方并不认同循环依赖的存在,为什么这么说呢?因为官方默认是不允许循环依赖的:spring.main.allow-circular-references 这个配置项默认是 false。所以一旦出现了循环依赖,更应该做的是审视自己的模块设计是否合理,而不是依赖于 Spring 解决。
  2. 为什么需要三级缓存?一级放的是完全初始化好的 Bean,二级放的是“残疾”并且被代理了的 Bean,三级放的是如何获取到被代理了的 Bean。Spring 把 AOP 代理对象的创建时机放在了初始化之后,而发生循环依赖时往往是在依赖注入阶段就需要实例对象,如果有代理要求,注入的就应该是代理对象,如果没有,那么注入原始对象即可,介于这一点产生了第三级缓存,但是一二级缓存是很暧昧的,对于依赖注入来说,并不在乎内部残疾不残疾,我只需要当前依赖的引用就行,实际上 Spring 也确实是这样做的,二级缓存中的实例对象就已经可以暴露出去解决循环依赖了。基于这个前提,按照 Spring 目前的流程,实际上二级缓存就已经能完全解决问题了,要是 Spring 更改 AOP 代理对象的创建时机为实例化之后,甚至可以一级缓存就做到。

异步实例化

相信在 org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons() 中注意到了 CompletableFuture 。是的, Spring 在 6.2.0 版本之后推出了异步实例化功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) {
if (mbd.isBackgroundInit()) {
Executor executor = getBootstrapExecutor();
if (executor != null) {
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
getBean(dep);
}
}
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> instantiateSingletonInBackgroundThread(beanName), executor);
addSingletonFactory(beanName, () -> {
try {
future.join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
return future; // not to be exposed, just to lead to ClassCastException in case of mismatch
});
return (!mbd.isLazyInit() ? future : null);
}
else if (logger.isInfoEnabled()) {
logger.info("Bean '" + beanName + "' marked for background initialization " +
"without bootstrap executor configured - falling back to mainline initialization");
}
}

if (!mbd.isLazyInit()) {
try {
instantiateSingleton(beanName);
}
catch (BeanCurrentlyInCreationException ex) {
logger.info("Bean '" + beanName + "' marked for pre-instantiation (not lazy-init) " +
"but currently initialized by other thread - skipping it in mainline thread");
}
}
return null;
}

从源码可以看出,Spring 会优先初始化同步的 Bean,之后再去初始化异步的 Bean,不过根据 Bean 的实例化步骤来看,异步的 Bean 要是被同步的 Bean 要求依赖注入了,实际上“异步”就会失效了。如果不考虑同步和异步的先后顺序,那么整个 Bean 的实例化会变得十分复杂且不可控,要是考虑了先后顺序吧,能不能提速也是个问题。这估计也是为什么 Spring 官方一直到 6.2.0 才稍微妥协了一下的原因。

这里推荐一下 why 哥的文章,来龙去脉写地挺详细:13年过去了,Spring官方竟然真的支持Bean的异步初始化了!