1. 什么是循环依赖?
在Spring框架中,循环依赖是指两个或多个bean之间相互依赖,形成了一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。导致 Spring 容器无法完成依赖注入。
例如:
@Service public class A {@Autowiredprivate B b; }@Service public class B {@Autowiredprivate A a; }
此时,A
依赖 B
,B
又依赖 A
,Spring 无法确定先初始化哪个 Bean。
要解决循环依赖问题的限制
Spring解决循环依赖是有一定限制的:
●首先就是要求互相依赖的Bean必须要是单例的Bean;
为什么呢?Spring循环依赖的解决方案主要是通过对象的提前暴露来实现的。当一个对象在创建过程中需要引用到另一个正在创建的对象时,Spring会先提前暴露一个尚未完全初始化的对象实例,以解决循环依赖的问题。这个尚未完全初始化的对象实例就是半成品对象。
在 Spring 容器中,单例对象的创建和初始化只会发生一次,并且在容器启动时就完成了。这意味着,在容器运行期间,单例对象的依赖关系不会发生变化。因此,可以通过提前暴露半成品对象的方式来解决循环依赖的问题。
相比之下,原型对象的创建和初始化可以发生多次,并且可能在容器运行期间动态地发生变化。因此,对于原型对象,提前暴露半成品对象并不能解决循环依赖的问题,因为在后续的创建过程中,可能会涉及到不同的原型对象实例,无法像单例对象那样缓存并复用半成品对象。
●另外就是依赖注入的方式不能都是构造函数注入的方式
为什么呢?Spring无法解决构造函数的循环依赖,是因为在对象实例化过程中,构造函数是最先被调用的,而此时对象还未完成实例化,无法注入一个尚未完全创建的对象,因此Spring容器无法在构造函数注入中实现循环依赖的解决,像下面这样
@Component
public class ClassA {
private final ClassB classB;
@Autowired
public ClassA(@Lazy ClassB classB) {
this.classB = classB;
}
// ...
}
@Component
public class ClassB {
private final ClassA classA;
@Autowired
public ClassB(ClassA classA) {
this.classA = classA;
}
// ...
}
但是这样可以通过一些方法解决的
1、重新设计,彻底消除循环依赖
循环依赖,一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,
2、改成非构造器注入
可以改成setter注入或者字段注入。
3、使用@Lazy解决(解决构造器循环依赖的)
首先要知道 Spring利用三级缓存是无法解决构造器注入这种循环依赖的。
@Lazy 是Spring框架中的一个注解,用于延迟一个bean的初始化,直到它第一次被使用。在默认情况下,Spring容器会在启动时创建并初始化所有的单例bean。这意味着,即使某个bean直到很晚才被使用,或者可能根本不被使用,它也会在应用启动时被创建。@Lazy 注解就是用来改变这种行为的。
也就是说,当我们使用 @Lazy 注解时,Spring容器会在需要该bean的时候才创建它,而不是在启动时。这意味着如果两个bean互相依赖,可以通过延迟其中一个bean的初始化来打破依赖循环。
缺点:过度使用 @Lazy 可能会导致应用程序的行为难以预测和跟踪,特别是在涉及多个依赖和复杂业务逻辑的情况下。
下面是一些例子
@Lazy 可以用在bean的定义上或者注入时。以下是一些使用示例:
@Component
@Lazy
public class LazyBean {
// ...
}
在这种情况下,LazyBean 只有在首次被使用时才会被创建和初始化。
@Component
public class SomeClass {
private final LazyBean lazyBean;
@Autowired
public SomeClass(@Lazy LazyBean lazyBean) {
this.lazyBean = lazyBean;
}
}
在这里,即使SomeClass在容器启动时被创建,LazyBean也只会在SomeClass实际使用LazyBean时才被初始化。
2. Spring 如何检测循环依赖?依赖三级缓存机制
Spring 在 创建 Bean 的流程 中会检查循环依赖,主要通过 三级缓存(3-level cache) 机制:
-
一级缓存(Singleton Objects)
存放 完全初始化好的 Bean(成品对象)。 -
二级缓存(Early Singleton Objects)
而当一个对象只进行了实例化,但是还没有进行初始化时,我们称之为半成品对象。所以,所谓半成品对象,其实只是 bean 对象的一个空壳子,还没有进行属性注入和初始化。
存放 半成品 Bean(已实例化但未完成属性注入)。 -
三级缓存(Singleton Factories)
存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景)。
如果 Spring 发现某个 Bean 正在创建中(存在于二级缓存),但又再次被依赖,则判定为循环依赖。
3. Spring 如何解决循环依赖?spring提供了一种三级缓存的机制
前面说过Spring 的三级缓存仅能解决 单例(Singleton)作用域 且 通过属性注入(@Autowired) 的循环依赖,核心步骤如下:
首先,Spring中Bean的创建过程其实可以分成两步,第一步叫做实例化,第二步叫做初始化。
具体流程如下:
(1) 创建 Bean A 的流程
-
实例化 A(调用构造函数,生成原始对象)。
-
将 A 的工厂对象放入三级缓存(用于后续可能的 AOP 代理)。
-
注入 A 的依赖(发现需要 B)。
-
去容器中获取 B(触发 B 的创建)。
(2) 创建 Bean B 的流程
-
实例化 B(生成原始对象)。
-
将 B 的工厂对象放入三级缓存。
-
注入 B 的依赖(发现需要 A)。
-
从三级缓存获取 A 的工厂,生成 A 的早期引用(可能是代理对象,半成品)并放入二级缓存。
-
B 完成属性注入,变成一个完整 Bean,放入一级缓存。
(3) 回到 A 的创建流程
-
从二级缓存拿到 B 的早期引用(半成品),注入到 A。
-
A 完成初始化,从二级缓存移除,放入一级缓存。
最终,A 和 B 都成功创建,且互相持有对方的代理或真实对象。
以下是DefaultSingletonBeanRegistry#getSingleton方法,代码中,包括一级缓存、二级缓存、三级缓存的处理逻辑,该方法是获取bean的单例实例对象的核心方法:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 首先从一级缓存中获取bean实例对象,如果已经存在,则直接返回
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果一级缓存中不存在bean实例对象,而且当前bean正在创建中,则从二级缓 存中获取bean实例对象
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 如果二级缓存中也不存在bean实例对象,并且允许提前引用,则需要在锁定一级缓存之前,
// 先锁定二级缓存,然后再进行一系列处理
synchronized (this.singletonObjects) {
// 进行一系列安全检查后,再次从一级缓存和二级缓存中获取bean实例对象
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 如果二级缓存中也不存在bean实例对象,则从三级缓存中获取bean的ObjectFactory,并创建bean实例对象
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 将创建好的bean实例对象存储到二级缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存中移除bean的ObjectFactory
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
Spring解决循环依赖一定需要三级缓存吗?()
上面的流程可以看见只用到了Spring二级缓存就能解决依赖注入的问题。
其实,在大多数简单的循环依赖场景(没有AOP代理)中,二级缓存(Early Singleton Objects) 已经足够解决问题。但 Spring 仍然使用 三级缓存(Singleton Factories,存放 Bean 的工厂对象(用于生成代理对象,如 AOP 场景)),主要是为了处理 AOP 代理 等特殊情况,确保返回的 Bean 是经过完整代理增强的对象。AOP又是Spring中很重要的一个特性,代理不能忽略。
所以三级缓存(Singleton Factories
)的核心作用是 处理 AOP 代理,确保返回的 Bean 是代理对象而非原始对象。
(1)AOP 代理的问题
如果 Bean 需要被代理(如 @Transactional、@Async),Spring 不能直接返回原始对象,而是返回代理对象。
代理对象的生成时机:需要在 Bean 初始化完成后(即 postProcessAfterInitialization 阶段)。
(2)第三级缓存的作用
三级缓存存储的是 ObjectFactory,它可以在需要时生成代理对象。
流程示例:
实例化 A(原始对象)。
将 A 的 ObjectFactory 放入三级缓存(而非原始对象)。
注入 A 的依赖时,调用 ObjectFactory.getObject():
如果 A 需要代理,则返回代理对象;
如果不需要代理,则返回原始对象。
最终放入二级缓存的是 代理对象(或原始对象),而非直接暴露原始对象。
(3)如果没有三级缓存
如果直接将原始对象放入二级缓存,后续 AOP 代理无法替换它,导致 注入的是原始对象而非代理,可能引发问题(如事务失效)。
4. 哪些情况是三级缓存无法解决循环依赖?
场景 | 原因 |
---|---|
构造器注入(Constructor Injection) | Spring 必须先完成构造器调用,无法提前暴露半成品 Bean。,可以使用@Lazy |
原型(Prototype)作用域 | Spring 不缓存原型 Bean,无法通过三级缓存机制解决。 提一点 对于原型对象,如果要解决循环依赖问题,要维护到底是哪两个对象之间的循环依赖,解决成本变高,而且循环依赖本来就不对,所以spring不支持。 如果检测到原型 Bean 的循环依赖,Spring 会直接报错: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'A': Requested bean is currently in creation: Is there an unresolvable circular reference? |
@Async/@Transactional 等代理类 | 如果循环依赖涉及 AOP 代理,可能因代理生成时机( 1:代理对象生成时机冲突,注入的可能是原始对象而非代理 2:构造器注入 + AOP 代理 无法提前暴露半成品 Bean 3:原型 Bean + AOP 代理 原型 Bean 无法缓存半成品 )问题导致失败(需用 一些极为特殊的情况。最好使用@Lazy,避免在复杂代理场景(如 |
5. 如何避免或修复循环依赖?
(1) 代码设计层面
-
避免双向依赖:重构代码,使用 单向依赖 或 接口隔离。
-
提取公共逻辑:将共用逻辑抽到第三个 Bean 中。
(2) Spring 提供的解决方案
-
使用
@Lazy
延迟加载
在其中一个依赖上添加@Lazy
,让 Spring 暂时不注入真实对象,而是注入一个代理。@Service public class A {@Autowired@Lazy // 延迟加载 Bprivate B b; }
-
改用 Setter/Field 注入
替换构造器注入为属性注入:@Service public class A {private B b;@Autowired // Setter 注入public void setB(B b) { this.b = b; } }
-
使用
ApplicationContext
手动获取 Bean
在需要时再获取依赖(不推荐,破坏 IoC 设计):@Service public class A {@Autowiredprivate ApplicationContext context;public void doSomething() {B b = context.getBean(B.class); // 使用时再获取} }
6. 总结
要点 | 说明 |
---|---|
可解决的循环依赖 | 单例 Bean + 属性注入(@Autowired) |
Spring不可自动解决的循环依赖 | 构造器注入、原型 Bean、某些 AOP 代理场景 |
解决方案 | @Lazy 、Setter 注入、代码重构 |
Spring 底层机制 | 三级缓存(Singleton Objects、Early Singleton Objects、Singleton Factories) |
最佳实践:
-
优先通过 代码设计 避免循环依赖。
-
必要时使用
@Lazy
或调整注入方式。 -
避免在复杂项目中滥用循环依赖,降低维护成本。