什么是 spring 的循环依赖?
首先,认识一下什么是循环依赖,举个例子:A 对象被 Spring 管理,并且引入的 B 对象,同样的 B 对象也被 Spring 管理,并且也引入的 A 对象。这种相互被引用的情况,就是所谓的循环依赖
@Component
public class A {@Autowiredprivate B b;
}
@Component
public class B {@Autowiredprivate A a;
}
我们都知道,Spring 是将对象给管理起来,这些对象默认还都是单例的,需要的话从 Spring 中直接取出即可。
1. Spring 是如何存储这些 Bean 呢?
Spring 通过 Map 结构将对象给缓存起来,这里的 Map 其实是分为三个:
-
一级缓存(singletonObjects):此缓存中的对象是已经完全创建好的,可以直接使用的 Bean;
Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
-
二级缓存(earlySingletonObjects):此缓存中存储尚未完全初始化但已经创建了对象实例的 Bean(即提前暴露的 Bean 实例)。
Map<String, Object> earlySingletonObjects = new HashMap<>();
-
三级缓存(singletonFactories):比较特殊,存放的是 ObjectFactory,这是一个工厂,等到从缓存中取时,会执行其中的
getObject()
方法,来得到代理对象。Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>()
这样设计是为了在有 AOP 的情况下,可以返回代理对象,而且也能满足循环依赖。
2. 循环依赖的创建过程
-
当对象 a 被创建时,就会在缓存中进行查询,先从一级缓存中进行查询,如果没有,接着再从二级缓存中进行查询,如果依旧没用,最后从第三级缓存中查询,如果还是没有,就接着执行后续操作;
-
由于缓存中没有 a 对象,就要回到主流程,执行 a 对象的创建:
- 利用反射创建对象
- 这时候就要用到三级缓存,将创建出的对象包装成 ObjectFctory 类型,放到三级缓存中
-
填充 a 对象的属性(Bean 的引入也在此执行)
其中要引入 b 对象,同样也要执行与 a 对象创建相同的流程:
-
查询缓存
-
b 对象的创建:利用反射创建 b 对象,并存入三级缓存中
-
填充 b 对象的属性
此时又要引入 a 对象,由于已经将 a 存储到缓存中,因此这里要执行一些额外的操作(后面说)后,将得到的 a 对象填充到 b 对象中
-
初始化
-
缓存转移(具体步骤后面说)
-
-
a 对象初始化
-
缓存转移
总体流程大致如下:

3. 补充说明
3.1 在 b 对象创建中填充属性时,从缓存中读取 a 对象要经过什么操作?
参考源码,读取关键部分(红框位置):

首先从三级缓存中获取到 a 对象,由于这个缓存里存放的是 ObjectFactory 类型,并不是真正的对象,这里就要执行 getObject()
方法,从而创建出真正要使用的对象,将得到的真正的对象 a 存入二级缓存中,并将三级缓存中的 a 删除。
3.2 缓存转移的步骤是什么?
此时 b 已经完全创建完毕,所以要将缓存里面的对象进行转移,参考源码:

可以分析出具体操作为:
- 将 b 的完整对象放到一级缓存中
- 将三级缓存中的 b 移除掉
- 将二级缓存中的 b 移除掉(该场景下二级缓存中并没有 b 对象)
a 对象的缓存转移也是同理。
4. 扩展
4.1 第三级缓存的作用?
从上面的执行步骤,可以感觉到三级缓存和里面的 ObjectFactory 类型似乎有点多余,有一级、二级缓存也能搞定循环依赖,三级缓存的意义是什么?
其实主要作用就是为了 AOP。
举例:假如对 b 对象使用了 AOP 切面功能,那么 a 对象引入的 b 对象就必须是 b 对象的代理对象,当 Spring 在没有循环依赖的情况下,是先将普通的完整对象创建好之后,再生成对应的代理对象,然而 Spring 并没有办法提前知道这个对象有没有循环依赖,也不能直接将每个对象都创建出代理对象,所以就需要吧对象包装成 ObjectFactory 类型,提前曝光,等从三级缓存中获取到 ObjectFactory 后,就可以通过 getObject()
方法生成代理对象。
4.2 避免循环依赖的最佳实践
尽管 Spring 提供了循环依赖的解决方案,但在实际开发中应尽量避免循环依赖,因为它可能导致代码耦合度过高、可维护性差等问题。以下是一些避免循环依赖的建议:
- 重构代码:将公共逻辑提取到第三个类中,打破循环依赖。
- 使用接口或事件驱动:通过接口或事件机制解耦组件间的直接依赖。
- 谨慎使用构造器注入:对于可能存在循环依赖的场景,优先使用 Setter 注入。
4.3 构造器注入与 Setter 注入的区别
- Setter 注入:支持循环依赖。因为 Spring 可以通过三级缓存机制提前暴露部分初始化的 Bean 实例。
- 构造器注入:不支持循环依赖。原因在于构造器注入要求在创建 Bean 时必须提供所有依赖项,而循环依赖会导致死锁(A 等待 B,B 等待 A)。
因此,如果使用构造器注入,循环依赖会导致 BeanCurrentlyInCreationException
异常。
BeanCurrentlyInCreationException
异常:表示在尝试实例化一个 Bean 时,Spring 容器检测到正在创建的 Bean 已经在创建过程中,导致循环依赖。这种异常通常是由于循环依赖问题引起的。
5. 总结
Spring 通过三级缓存机制解决了单例 Bean 之间的循环依赖问题,但对于原型 Bean 或构造器注入的场景,Spring 无法解决循环依赖。开发时应尽量避免循环依赖,保持代码的清晰性和可维护性