Spring 的循环依赖一直都是 Spring 中一个很重要的话题,一方面是 Spring 为了解决循环依赖做了很多工作,另一个方面是因为它是面试 Spring 的常客,因为他要求你看过 Spring 的源码,如果没有看过 Spring 源码你基本上是回答不了这个问题的,虽然也有一些面试经会分析这个问题,但是如果别人深究的话,你没有看过源码真心回答不了。
什么是循环依赖
从字面上来理解就是 Spring Bean 之间的依赖产生了循环,例如 A 依赖 B ,B 依赖 C,C 依赖 A,如下:
代码大致如下:
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private CService cService;
}
@Service
public class CService {
@Autowired
private AService aService;
}
Spring 解决了哪些情况的循环依赖
在文章Spring 中的 Bean 有几种作用域?,大明哥说到 Spring 有五种作用域:
singleton
:单例作用域prototype
:原型作用域request
:请求作用域session
:会话作用域application
:全局作用域
但是 Spring 只解决单例作用域(singleton
)循环依赖,主要原因如下:
prototype
:每次请求都会创建新的 Bean 实例,会形成一个死循环,同时用一次就丢,解决循环依赖成本比较大。request
、session
、application
:解决他们的循环依赖需要很复杂的处理机制,可能会引入额外的性能开销和复杂性。- 同时 ,Spring 的设计哲学倾向于鼓励良好的编程实践,依赖循环依赖可能是设计上的问题,我们应该是采取更加简洁和更加优雅的代码结构,而不是依赖 Spring 来解决它。
同时,在文章 Spring为什么建议使用构造器来注入 中讲到,Spring 有三种注入方式:
- 基于字段的注入
- 基于 setter 方法的注入
- 基于构造器注入
对于这三种注入方式 ,Spring 不解决全是基于构造器注入的方式 ,因为 Spring 会报错,例如我们将上面的调整为构造器注入,启动时会报如下错误 :
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| AService defined in file [/xxx/AService.class]
↑ ↓
| BService defined in file [/xxx/BService.class]
↑ ↓
| CService defined in file [/xxx/CService.class]
└─────┘
注:我是依赖 Spring Boot 写的测试案例
基于上面的分析,我们知道 Spring 解决循环依赖的前提条件是 :
- 出现循环依赖的 B 必须是单例作用域。
- 依赖注入的方式不能全是构造器注入的方式。
有很多文章说,Spring 只解决 setter 方法的循环依赖,这是错误的,我们来演示下:
@Service
public class AService {
@Autowired
private BService bService;
}
B、C 还是构造器注入,你会发现这样启动是不会报错的。
基于这个问题,大明哥留一个思考题,如果 A、B、C 的注入方式是如下:
@Service
public class AService {
private BService bService;
@Autowired
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
@Autowired
private CService cService;
}
@Service
public class CService {
private AService aService;
@Autowired
public CService(AService aService) {
this.aService = aService;
}
}
会报错吗?为什么?
Spring 是怎么解决循环依赖的
Spring 解决循环依赖就靠三招:
- 单例模式
- 三级缓存
- 提前暴露、早起引用
单例模式前面已经分析过了,我们先看三级缓存。Spring 提供了三级缓存用来存储单例 的 Bean 实例(下面所说的 Bean 全部都为单例模式的 Bean),这三个缓存是互斥的,同一个 Bean 实例只会三级缓存中的一个中存在。三级缓存分别是:
- 一级缓存:
singletonObjects
- 用于存放完全初始化好的 Bean。
- 当一个 Bean 被完全初始化好后(即所有的属性都被注入,所有的初始化方法都被调用),它会被放入这个缓存中。在此之后,每次请求这个 Bean 时,Spring 容器都会直接从这个缓存返回实例。
- 二级缓存:
earlySingletonObjects
- 用于存放提前暴露的 Bean 对象,即已经实例化但尚未完全初始化(未完成依赖注入和初始化方法调用)的 Bean,注意该 Bean 还处于创建中。
- 该缓存是解决循环依赖的核心所在。当一个 Bean 正在创建的过程中,如果另外一个 Bean 需要引用它,则 Spring 为它提供该 Bean的一个早期引用,这个早期引用就存放在
earlySingletonObjects
缓存中。
- 三级缓存:
singletonFactories
- 存放 Bean 的工厂对象,用于生成 Bean 的早期引用。
- 它是解决循环依赖的第一步。当一个 Bean 开始创建时,Spring 首先在这个缓存中放入一个工厂对象。这个工厂对象能够生成 Bean 的早期引用,当这个 Bean 需要被注入到其他 Bean 中时,就会通过这个工厂对象来创建早期引用。
三个缓存协同工作 ,以确保在应用中存在循环依赖的情况下,Spring 容器依然可以正确创建 Bean,并管理他们。关于三级缓存的详情请阅读这篇文章:什么是Spring的三级缓存?
现在跟着大明哥的脚步来详细分析 Spring 是如何解决循环依赖的,为了更好地演示,我们将上面三个 Bean 循环依赖调整为两个即 A 依赖 B,B 依赖 A。
Spring 创建 Bean 的过程分为三个步骤:
- 实例化:
AbstractAutowireCapableBeanFactory#createBean()
- 属性注入:
AbstractAutowireCapableBeanFactory#populateBean()
- 初始化:
AbstractAutowireCapableBeanFactory#initializeBean()
首先我们创建 A
,先进行 A
对象的实例化过程 ,跟踪 AbstractAutowireCapableBeanFactory#createBean()
,到 doCreateBean()
:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
// .....
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 加入到三级缓存 singletonFactories 中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// ...
}
earlySingletonExposure
为 true
,则将 singletonFactory 保存到三级缓存 singletonFactories 中。条件为:
mbd.isSingleton()
:为单例模式this.allowCircularReferences
:检查配置是否允许循环引用this.isSingletonCurrentlyInCreation(beanName)
:检查当前 Bean 是否正在创建。这是为了检测 Bean 是否处于创建的半成品状态(即已经开始创建但还没有完全初始化),这种状态的 Bean 是解决循环依赖的关键,因为他需要提前暴露给其他 Bean 引用,用来解决循环依赖 。
addSingletonFactory()
是将 singletonFactory 添加到三级缓存中:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 添加到三级缓存中
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
singletonFactory 由 getEarlyBeanReference()
创建:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
这个方法是一个很重要的方法,用来出来 AOP 的,我们先放在这里,后面来分析。
当 A
实例化后,就调用 populateBean()
来完成属性注入,这里开始注入 B
,调用 getBean(b)
,我们一直跟踪源代码到 getSingleton(String beanName, boolean allowEarlyReference)
,该方法有两个参数:
beanName
:获取 Bean 实例的名称。allowEarlyReference
:用于指定是否允许早期引用。true
:允许在 Bean 的初始化过程中提前获取引用,即使 Bean 正在创建中。false
:只有在 Bean 创建完成后才能获取引用
代码如下:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从一级缓存中获取完整的 Bean
Object singletonObject = this.singletonObjects.get(beanName);
// 如果 singletonObject 为空,且在新建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 从二级缓存中获取该 Bean 的早期引用
singletonObject = this.earlySingletonObjects.get(beanName);
// 早期引用为空,且允许早期引用
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 双重检查
// 一级
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) {
// 从单例工厂获取单例 Bean
// 注意这个 Bean 还只是一个早期引用
singletonObject = singletonFactory.getObject();
// 将 Bean 从三级缓存移动到二级缓存去
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
由于 B
还没有创建,所以这里会返回 null
。故 B
依然和 A 一样去走创建过程,也会在三级缓存中存放 Bean 工厂。当 B
完成实例化后开始属性注入,这个时候它会调用 getBean(a)
去获取 A
的实例对象,由于 A
还处于创建过程中,一级缓存没有,二级缓存也没有,但是在三级缓存 A
提前暴露了一个 Bean 工厂对象,B
可以在三级缓存中获取 A 的 Bean 工厂对象,通过 singletonFactory.getObject()
获取 A
的早期引用,完成注入,也就是这段代码:
// 从单例工厂获取单例 Bean
// 注意这个 Bean 还只是一个早期引用
singletonObject = singletonFactory.getObject();
// 将 Bean 从三级缓存移动到二级缓存去
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
B
完成注入后,开始初始化,最终得到一个完整 B
。B 完成后,这个时候 A 得到的就是一个完整的 B 实例对象,A 完成注入,并进行初始化。整个过程如下图:
到这里循环依赖已经解决了。整个过程这张图已经详细阐述了,大明哥就不过多阐述了。在上面大明哥还埋了一个点,就是 getEarlyBeanReference()
,我们继续。
我们知道注入到 B
中的 A
是通过 getEarlyBeanReference()
提前暴露出去的一个对象获取的,我们再看这个方法:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
这里的参数 bean 就是已经实例化的 A
对象。
这行代码是关键:if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors())
,如果它为 false,这个方法等同于:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
return exposedObject;
}
返回的是一个原生的 A 对象,但是如果 if 语句为 true 呢?那么 exposedObject = bp.getEarlyBeanReference(exposedObject, beanName)
,调用后置处理器的 getEarlyBeanReference()
,然后真正实现这个方法的后置处理器就只有一个地方,那就是 AOP 的 AnnotationAwareAspectJAutoProxyCreator
,如果我们的 A 进行了 AOP 代理 ,那么注入 B 的是 A 的代理对象而不是 A 本身。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。