前言

作为一个 Java 程序员,平时打交道最多的自然是 Spring,正逢最近有空,打算借助源码和官方文档仔细温习一下 Spring 相关的特性,或许会持续更新。

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

IoC - 控制反转

依赖注入

这个环节 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.

不过根据我目前为止接触到的项目来看,大多数项目其实没有进行这一层面的区分,对于依赖都是采用 @Autowired 注解,这个注解的依赖注入方式其实跟 Setter 注入差距不大,只不过 @Autowired 是通过反射强制在属性上直接注入,而 Setter 注入是通过反射通过 Setter 方法注入,这些依赖绝大多数都是不可或缺的,按照官方指引实际上应该采用构造器注入。

官方实际上也推荐主要使用构造器注入依赖:The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null.

循环依赖

循环依赖算是八股文常问的了,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 要求依赖注入。

对循环依赖的一些思考

虽然 Spring 官方一定程度上解决了循环依赖的问题,但实际上 Spring 官方并不认同循环依赖的存在,为什么这么说呢?我觉得主要是以下两点:

  1. 官方推荐使用构造器注入:假设是一个全参构造函数,那么即使是单例 Bean 也无法解决循环依赖,因为全参缺一不可,本身都实例化不出来就更别提解决循环依赖了。
  2. 官方默认是不允许循环依赖的:spring.main.allow-circular-references 这个配置项默认是 false

所以一旦出现了循环依赖,更应该做的是审视自己的模块设计是否合理,而不是依赖于 Spring 解决。

异步实例化

相信在 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的异步初始化了!