报错
Description:
The dependencies of some of the beans in the application context form a cycle:
appController (field private com.yzf.service.AppService com.yzf.controller.AppController.service)
┌─────┐
| appServiceImpl defined in file [...]
↑ ↓
| aiAssistantFactorydefined in file [...]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
很明显这是 Spring Bean 的循环依赖问题。想看解决方法可以直接看 作为开发者,如何解决和避免循环依赖
目录
- 什么是循环依赖?
- 循环依赖的场景
- Spring 如何解决循环依赖?(核心)
-
- 关键:三级缓存
- 详细流程拆解
- 为什么需要三级缓存,二级可以吗?
- Spring 无法解决的循环依赖
- 作为开发者,如何解决和避免循环依赖?
- 总结
1. 什么是循环依赖?
- 循环依赖指的是两个或多个 Bean 之间相互依赖,形成一个闭环。
- 最简单的形式是:
- A 依赖 B。
- B 依赖 A。
- Spring 容器启动时,它需要创建
A
和B
的实例- 创建
A
时,发现它需要B
,于是容器会去创建B
。 - 创建
B
时,发现它需要A
,容器又会去创建A
。 - 这就形成了一个死循环。
- 创建
2. 循环依赖的场景
循环依赖主要分为三种:
- 构造器注入的循环依赖:Spring 无法解决的。
- Setter、字段注入的循环依赖:Spring 已经解决的,也是我们讨论的重点。
- 多例(
prototype
) 作用域的循环依赖:Spring 无法解决的。
“Spring 解决了循环依赖”,特指的是默认单例(Singleton)作用域下,通过 Setter 注入的循环依赖。
3. Spring 如何解决循环依赖?(核心)
- 核心是 三级缓存 和 提前暴露。
- 这三个缓存都位于
DefaultSingletonBeanRegistry
类中:
- 一级缓存(
singletonObjects
)- 类型:
Map<String, Object>
- 作用:成品缓存。存放完全初始化好的 Bean 实例。
- 类型:
- 二级缓存(
earlySingletonObjects
)- 类型:
Map<String, Object>
- 作用:半成品缓存。存放提前暴露的 Bean 实例,Bean 已经被实例化,但未完成属性注入和初始化
- 类型:
- 三级缓存(
singletonFactories
)- 类型:
Map<String, ObjectFactory<?>>
- 作用:工厂缓存。存放用于创建 Bean 的工厂对象。工厂对象的主要作用是,在需要的时候,可以生产出一个 Bean 的实例(可能是原始实例,也可能是代理实例)。这是解决 AOP 代理问题的关键。
- 类型:
- 实例化?属性注入?初始化?这些是啥?这是 Bean 的生命周期。
- 以下是 Bean 的简单的生命周期 -- 5 大步,看完再回去看 三个缓存,就不懵了
详细流程拆解
我们以来实例化(创建) A
和 B
为例,逐步分析 Spring 的处理流程:
1.getBean("A")
开始:
- Spring 容器尝试获取
A
。 - 检查一级缓存 :没有
A
。 - 发现
A
正在实例化的标记也没有,于是开始实例化A
。 - 检查三级缓存 :没有
A
。 - 检查二级缓存 :没有
A
。 - 实例化 A
- 通过反射实例化
A
。此时A
是一个“半成品”,它的B
属性还是null
。 - 【关键步骤 1】:Spring 并不是直接将这个半成品放入二级缓存,而是创建一个对象工厂 (
ObjectFactory
),并将这个工厂放入三级缓存 中。 - 这个工厂对象的
getObject()
方法会在被调用时,决定是返回原始的A
实例还是经过 AOP 代理后的代理对象。
- 通过反射实例化
2.为 A
注入属性:
- Spring 开始填充
A
的属性,发现它需要B
。 - 于是 Spring 会去调用
getBean("B")
。
3.getBean("B")
开始:
- Spring 容器尝试获取
B
。 - 检查一级缓存:没有
B
。 - 开始实例化
B
。 - 检查三级缓存 :没有
B
。 - 检查二级缓存 :没有
B
。 - 实例化
B
:- 通过反射实例化
B
。此时B
也是一个半成品 - 将
B
的 对象工厂 放入三级缓存
- 通过反射实例化
4.为 B
注入属性:
- Spring 填充
B
的属性,发现它需要A
。 - 于是 Spring 再次调用
getBean("A")
。
5.再次 getBean("A")
(循环依赖解决点):
- 检查一级缓存 :没有
A
(因为它还没完全创建好)。 - 检查二级缓存 :没有
A
。 - 【关键步骤 2】:检查三级缓存 :命中了! 之前为
A
放入的工厂在这里被找到了。- Spring 调用这个工厂的
getObject()
方法,获取A
的早期引用(Early Reference)。 - 早期引用(可能是原始对象,也可能是代理对象)被放入二级缓存,并从三级缓存中移除对应的工厂。
getBean("A")
返回这个早期引用。
- Spring 调用这个工厂的
6.B
创建完成:
B
成功获取到了A
的早期引用,并完成了属性注入。B
继续执行后续的初始化操作(如InitializingBean
、@PostConstruct
等)。B
完全创建完成。- 将完整的
B
实例放入一级缓存 ,并从二级缓存中移除(如果存在)。
7.A
创建完成:
- 回到第 3 步,
A
的getBean("B")
调用现在可以成功返回一个完整的B
实例了。 A
成功完成了属性注入。A
继续执行后续的初始化操作。A
完全创建完成。- 将完整的
A
实例放入一级缓存,并从二级缓存和三级缓存中移除。
至此,A
和 B
的循环依赖被成功解决,二者都已是完整的 Bean。
1、按着 step 看这个图,就应该可以看明白了,比如:step1 -> step1-1 -> step2 -> step2-1
2、放入二级缓存的没画,知道就行了
3、细心的同学可能会考虑,最后的一级缓存B的属性A不是A的早期引用吗,会不会有什么问题?
答:属性A指向了A的内存地址,最后A的初始化好的了,是完整的。但是早期引用在 init 方法中可能会产生空指针异常
4. 为什么需要三级缓存,二级可以吗?
1. 这是一个经典面试题。如果没有 AOP,二级缓存是足够的。
流程会变成:
- 实例化
A
,将半成品的A
放入二级缓存。 A
依赖B
,去创建B
。- 实例化
B
,将半成品的B
放入二级缓存。 B
依赖A
,从二级缓存中拿到半成品的A
。B
创建完成,放入一级缓存。A
拿到完整的B
,创建完成,放入一级缓存。
2. 那么,为什么需要三级缓存呢?
答案是:为了处理 AOP 代理。
如果一个 Bean 需要被 AOP 代理,那么注入到其他 Bean 中的应该是它的代理对象,而不是原始对象。但 Spring 无法提前知道一个 Bean 是否需要被代理(因为代理的逻辑,如 BeanPostProcessor
,是在 Bean 完全初始化之后才执行的)。
- 如果 Spring 在实例化后立即创建代理,并放入二级缓存,这违背了 Spring 的设计。因为 BeanPostProcessor 应该在 Bean 初始化后(
populateBean
之后)才执行。 - 如果 Spring 把原始对象放入二级缓存,那么当其他 Bean 依赖它时,拿到的就是原始对象,而不是代理对象,AOP 就会失效。
三级缓存 完美地解决了这个问题。
它存放的不是 Bean 对象,而是一个工厂。当其他 Bean 需要依赖这个半成品 Bean 时,会调用这个工厂的 getObject()
方法。在这个方法内部,Spring 就可以判断“这个 Bean 将来是否需要被代理?”,如果需要,就返回代理对象;如果不需要,就返回原始对象。
5. Spring 无法解决的循环依赖
- 构造器注入
- 原因:Bean 的实例化(调用构造函数)和依赖注入是原子操作。创建
A
的实例就需要B
的实例,而创建B
的实例就需要A
的实例。没有机会像 Setter 注入那样,先把一个“半成品”暴露出去。这是一个无法解开的死结。 - 报错:
BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation: Is there an unresolvable circular reference?
- 原因:Bean 的实例化(调用构造函数)和依赖注入是原子操作。创建
- 多例
prototype
作用域- 原因:Spring 不会缓存
prototype
作用域的 Bean。每次请求都会创建一个新的实例,因此无法使用三级缓存机制来提前暴露引用。这会导致无限循环创建,最终栈溢出。
- 原因:Spring 不会缓存
6. 作为开发者,如何解决和避免循环依赖?
尽管 Spring 能解决部分循环依赖,但循环依赖通常被认为是一种代码设计缺陷。它表明类与类之间的职责划分不清。
1.如果你是 springBoot >=2.6.0,你会发现普通的 setter、字段注入都会报循环依赖问题
spring:
main:
allow-circular-references: true
2.重构代码结构(最佳实践)
// 重构前: A -> B -> A
// 重构后: A -> C, B -> C
- 这是最推荐的方式。重新思考你的设计,是否可以将公共逻辑抽取到一个新的类 C 中,让 A 和 B 都依赖 C,从而打破循环。
3.使用 @Lazy
注解
@Service
public class ServiceA {
@Autowired
@Lazy // 关键点
private ServiceB serviceB;
}
- 在注入点使用
@Lazy
注解。这会延迟该依赖的加载。Spring 会注入一个代理对象,只有当你第一次使用这个依赖(调用它的方法)时,真正的目标 Bean 才会被创建和注入。这打破了启动时的创建循环。
4.使用 @PostConstruct
@Service
public class ServiceA implements ApplicationContextAware {
private ServiceB serviceB;
@Autowired
private ApplicationContext context;
@PostConstruct
public void init() {
// 在构造之后,手动获取依赖
this.serviceB = context.getBean(ServiceB.class);
}
}
- 将依赖注入的逻辑从构造或字段注入移到
@PostConstruct
标记的方法中。此时 Bean 本身已经实例化完成,再手动去容器中获取依赖。
5.使用 spring 的发布订阅机制
@Slf4j
@Component
public class AiAssistantFactory {
@Autowired
private AppService appService;
@Async
@PostConstruct
public void init() {
log.info("=== 初始化 AiAssistant 开始 ===");
List<App> apps = appService.list();
apps.forEach(this::createOrUpdateAiAssistant);
log.info("=== 初始化 AiAssistant 结束 ===");
}
}
@Service
public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {
@Override
public Long addApp(AppAddRequest addRequest) {
....
// 添加 aiAssistant
SpringContextUtils.publishEvent(new CreateOrUpdateAiAssistantEvent(entity));
....
}
}
@EventListener({CreateOrUpdateAiAssistantEvent.class})
public void AiAssistantBuildListener(CreateOrUpdateAiAssistantEvent event) {
log.info("===== 新增或更新 aiAssistant 事件开始 =====");
aiAssistantFactory.createOrUpdateAiAssistant((App) event.getSource());
log.info("===== 新增或更新 aiAssistant 事件结束 =====");
}
6.将 Setter 注入改为构造器注入
- Spring 官方推荐使用构造器注入。因为它可以保证依赖的不可变性,并且能在启动时就暴露循环依赖问题(快速失败),迫使你优化设计。
7. 总结
特性 |
描述 |
核心问题 |
两个或多个 Bean 在创建过程中相互等待对方完成,形成死锁。 |
Spring 的解决方案 |
三级缓存 + 提前暴露引用。 |
一级缓存 |
成品池,存放完全初始化的单例 Bean。 |
二级缓存 |
半成品池,存放已实例化但未初始化的 Bean 的早期引用。 |
三级缓存 |
工厂池,存放能产生 Bean(或其代理)的工厂,用于解决 AOP 问题。 |
能解决的场景 |
单例(Singleton)作用域下的 Setter/Field 注入。 |
不能解决的场景 |
1. 构造器注入:实例化和注入是原子操作,无法提前暴露。 |
最佳实践 |
重构代码,避免循环依赖。如果无法避免,可使用 |
为何需要三级缓存 |
为了在不破坏 Bean 生命周期的情况下,延迟 AOP 代理对象的创建,确保注入的是正确的代理对象。 |