Spring 循环依赖详解:三级缓存、早期引用、AOP 代理与懒加载
本文整理 Spring 中循环依赖的核心知识,重点解释:Spring 为什么能解决部分循环依赖,三级缓存分别是什么,为什么需要提前暴露 Bean,AOP 代理在循环依赖中如何处理,以及
@Lazy是否能解决循环依赖。
1. 什么是循环依赖?
循环依赖指的是两个或多个 Bean 之间互相依赖,形成闭环。
例如:
java
@Service
public class AService {
@Autowired
private BService bService;
}
java
@Service
public class BService {
@Autowired
private AService aService;
}
依赖关系是:
text
AService -> BService -> AService
这就是典型的循环依赖。
如果没有特殊处理,Spring 创建 AService 时发现需要 BService,于是创建 BService;创建 BService 时又发现需要 AService,于是又去创建 AService,最终会陷入递归创建。
2. Spring 能解决所有循环依赖吗?
不能。
Spring 默认主要能解决的是:
text
单例 Bean + 属性注入 / Setter 注入 形成的循环依赖
Spring 通常解决不了的是:
text
构造方法注入形成的循环依赖
prototype 多例 Bean 的循环依赖
3. 为什么属性注入可以解决循环依赖?
因为属性注入可以把 Bean 创建过程拆成两步:
text
第一步:先实例化对象
第二步:再给属性赋值
例如:
java
@Service
public class AService {
@Autowired
private BService bService;
public AService() {
}
}
Spring 底层可以先通过反射调用无参构造方法创建对象:
java
AService aService = AService.class
.getDeclaredConstructor()
.newInstance();
此时 AService 对象已经存在,但里面的 bService 还是 null。
然后 Spring 再通过反射注入属性:
java
Field field = AService.class.getDeclaredField("bService");
field.setAccessible(true);
field.set(aService, bService);
所以属性注入的 Bean 可以出现一个"半成品对象":
text
AService 对象已经 new 出来了
但是属性还没有注入完成
初始化方法还没有执行
Spring 正是利用这个"半成品对象"来解决循环依赖。
4. 为什么构造方法循环依赖通常解决不了?
构造方法注入要求在创建对象之前,必须先准备好构造参数。
例如:
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
Spring 要创建 AService,必须执行:
java
AService aService = new AService(bService);
这意味着它必须先拿到 BService。
如果 BService 又是这样:
java
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
那么流程会变成:
text
创建 AService 需要 BService
创建 BService 又需要 AService
AService 还没有 new 出来,无法提前暴露
也就是说,构造方法注入没有办法先得到一个 AService 半成品对象。
所以构造器循环依赖通常无法通过三级缓存解决。
5. Spring 解决循环依赖的核心:三级缓存
Spring 解决单例属性注入循环依赖的核心机制是:
text
三级缓存 + 提前暴露早期引用
三级缓存位于:
java
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
里面有三个核心 Map。
5.1 一级缓存:singletonObjects
java
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
一级缓存保存的是:
text
已经创建完成的单例 Bean
例如:
text
userService -> 完整的 UserService Bean
orderService -> 完整的 OrderService Bean
5.2 二级缓存:earlySingletonObjects
java
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
二级缓存保存的是:
text
提前暴露出来的早期 Bean 引用
这个早期引用可能是:
text
原始对象
代理对象
5.3 三级缓存:singletonFactories
java
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
三级缓存保存的是:
text
可以生成早期 Bean 引用的 ObjectFactory
也就是:
text
beanName -> ObjectFactory
这个 ObjectFactory 被调用时,可以返回:
text
原始对象
或者提前生成的代理对象
6. 三个缓存的关系
可以这样理解:
text
一级缓存:正式仓库,放完整成品 Bean
二级缓存:临时仓库,放已经生成的早期引用
三级缓存:工厂仓库,放可以生成早期引用的工厂
查找顺序是:
text
一级缓存 singletonObjects
↓
二级缓存 earlySingletonObjects
↓
三级缓存 singletonFactories
7. Spring 解决 AService 和 BService 循环依赖的完整流程
代码如下:
java
@Service
public class AService {
@Autowired
private BService bService;
}
java
@Service
public class BService {
@Autowired
private AService aService;
}
假设 Spring 先创建 AService。
7.1 创建 AService 原始对象
Spring 先实例化 AService:
java
AService aService = new AService();
此时:
text
AService 已经 new 出来了
但是 bService 还没有注入
初始化方法还没有执行
AOP 代理也还没有最终完成
它是一个半成品 Bean。
7.2 把 AService 的 ObjectFactory 放入三级缓存
实例化之后、属性注入之前,Spring 会把一个 ObjectFactory 放入三级缓存:
text
singletonFactories.put("aService", objectFactory)
这个工厂将来可以返回 AService 的早期引用。
此时缓存状态大概是:
text
一级缓存 singletonObjects:
空
二级缓存 earlySingletonObjects:
空
三级缓存 singletonFactories:
aService -> ObjectFactory
7.3 AService 开始属性注入,发现需要 BService
Spring 给 AService 注入属性时,发现:
java
@Autowired
private BService bService;
于是开始获取 BService。
容器里还没有 BService,所以开始创建 BService。
7.4 创建 BService 原始对象
Spring 实例化 BService:
java
BService bService = new BService();
然后也会把 BService 的 ObjectFactory 放入三级缓存:
text
singletonFactories.put("bService", objectFactory)
此时:
text
三级缓存 singletonFactories:
aService -> ObjectFactory
bService -> ObjectFactory
7.5 BService 注入 AService 时发生回头依赖
Spring 给 BService 注入属性时,发现:
java
@Autowired
private AService aService;
于是它去找 AService。
查找顺序是:
text
1. 一级缓存 singletonObjects:没有
2. 二级缓存 earlySingletonObjects:没有
3. 三级缓存 singletonFactories:有 aService 的 ObjectFactory
于是 Spring 调用 aService 对应的 ObjectFactory#getObject(),拿到 AService 的早期引用。
然后:
text
earlySingletonObjects.put("aService", earlyAService)
singletonFactories.remove("aService")
并把这个早期引用注入给 BService:
java
bService.aService = earlyAService;
7.6 BService 创建完成
BService 拿到了 AService 的早期引用,于是可以继续完成:
text
属性注入
Aware 回调
BeanPostProcessor before
初始化方法
BeanPostProcessor after
最终 BService 创建完成,进入一级缓存:
text
singletonObjects.put("bService", bService)
7.7 回到 AService,注入 BService
现在 BService 已经创建完成,Spring 回到之前创建 AService 的流程。
把完整的 BService 注入给 AService:
java
aService.bService = bService;
然后 AService 继续完成自己的生命周期:
text
属性注入完成
Aware 回调
BeanPostProcessor before
初始化方法
BeanPostProcessor after
最后进入一级缓存:
text
singletonObjects.put("aService", aService)
8. 为什么是 AService 提前暴露给 BService,而不是 BService 提前暴露给 AService?
这是很多人理解循环依赖时最容易卡住的地方。
假设创建顺序是:
text
AService -> BService -> AService
真实调用栈类似:
text
createBean(AService)
└── getBean(BService)
└── createBean(BService)
└── getBean(AService) ← 卡在这里
此时真正卡住的是:
text
BService 正在创建中,但它需要 AService
所以 Spring 必须解决的是:
text
如何让 BService 拿到 AService
而不是:
text
如何让 AService 拿到 BService
因为 AService 正在等待 BService 创建完成,只有 BService 完成后,才能返回给 AService。
所以在 A -> B -> A 这个链路里:
text
回头依赖的是 AService
因此 Spring 提前暴露的是:
text
AService 的早期引用
不是:
text
BService 的早期引用
一句话:
text
谁被回头依赖,谁就提前暴露。
9. 为什么需要三级缓存,而不是二级缓存?
如果没有 AOP,二级缓存似乎已经够用了。
比如 AService 实例化后,直接把半成品对象放入二级缓存:
text
earlySingletonObjects.put("aService", aService)
当 BService 需要 AService 时,直接拿出来注入即可。
但是有 AOP 时,问题来了。
如果 AService 有事务:
java
@Service
public class AService {
@Autowired
private BService bService;
@Transactional
public void save() {
}
}
那么最终进入容器的应该不是 AService 原始对象,而是 AService 的代理对象。
如果提前暴露的是原始对象:
text
BService.aService = AService 原始对象
但最终容器保存的是:
text
singletonObjects.put("aService", AService 代理对象)
那么就会出现不一致:
text
BService 里面持有的是 AService 原始对象
容器里持有的是 AService 代理对象
这样 BService 调用 AService 的事务方法时,可能绕过代理,导致事务不生效。
所以 Spring 第三级缓存保存的是 ObjectFactory,而不是直接保存对象。
这个工厂在真正需要早期引用时,可以通过:
java
getEarlyBeanReference(beanName, mbd, bean)
让 AOP 后处理器参与进来。
如果这个 Bean 需要代理,就提前生成代理对象;如果不需要代理,就返回原始对象。
因此:
text
二级缓存:保存已经确定的早期引用
三级缓存:保存可以生成早期引用的工厂
三级缓存的核心价值是:
text
为 AOP 代理预留机会,避免提前暴露错误的原始对象。
10. 如果 AService 和 BService 都需要代理,总体流程怎样?
假设:
java
@Service
public class AService {
@Autowired
private BService bService;
@Transactional
public void aMethod() {
}
}
java
@Service
public class BService {
@Autowired
private AService aService;
@Transactional
public void bMethod() {
}
}
AService 和 BService 都需要代理。
假设 Spring 先创建 AService,流程是:
text
1. 实例化 AService 原始对象
2. 把 AService 的 ObjectFactory 放入三级缓存
3. AService 注入 BService,发现 BService 还不存在
4. 开始创建 BService
5. 实例化 BService 原始对象
6. 把 BService 的 ObjectFactory 放入三级缓存
7. BService 注入 AService
8. 从三级缓存调用 AService 的 ObjectFactory
9. 因为 AService 需要代理,所以提前生成 AService 代理对象
10. 把 AService 代理对象注入 BService
11. BService 继续完成初始化
12. BService 在 BeanPostProcessor after 阶段生成 BService 代理对象
13. BService 创建完成,返回给 AService
14. AService 注入 BService 代理对象
15. AService 继续完成生命周期
16. AService 之前已经提前代理过,不再重复创建代理
17. 最终容器中保存 AService 代理对象和 BService 代理对象
最终关系大概是:
text
容器中的 aService
↓
AService Proxy
↓
target: AService 原始对象
↓
bService = BService Proxy
容器中的 bService
↓
BService Proxy
↓
target: BService 原始对象
↓
aService = AService Proxy
需要注意:
text
AService 提前生成代理,不代表 AService 已经创建完成。
这只是提前暴露了一个早期引用。
此时代理对象背后的 AService 目标对象仍然可能是半成品。
11. 早期代理对象不等于完整 Bean
当 AService 的早期代理被注入给 BService 时,AService 本身还没有完成:
text
bService 属性还没注入
Aware 回调还没执行
@PostConstruct 还没执行
BeanPostProcessor after 还没走完
此时关系是:
text
AService Proxy
↓
target: AService 原始对象,半成品
所以 Spring 解决循环依赖,本质上是:
text
允许一个正在创建中的 Bean 被提前引用
这并不意味着这个 Bean 已经完全可用。
如果在初始化过程中立刻调用这个早期引用的方法,可能有风险。
例如:
java
@Service
public class BService {
@Autowired
private AService aService;
@PostConstruct
public void init() {
aService.doSomething();
}
}
此时 AService 可能尚未完成初始化,调用它的方法可能出现空字段、初始化逻辑未执行等问题。
所以 Spring 能解决循环依赖,不代表这种设计一定合理。
12. 三级缓存相关源码类
12.1 DefaultSingletonBeanRegistry
三级缓存的 Map 和获取逻辑主要在:
java
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
核心字段:
java
// 一级缓存:完整单例 Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:早期 Bean 引用
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:早期 Bean 引用工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
核心方法:
java
getSingleton(String beanName, boolean allowEarlyReference)
addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory)
addSingleton(String beanName, Object singletonObject)
12.2 getSingleton 的简化逻辑
java
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
return singletonObject;
逻辑是:
text
先查一级缓存
如果没有,并且 Bean 正在创建中,查二级缓存
如果还没有,并且允许早期引用,查三级缓存
如果三级缓存有 ObjectFactory,就调用 getObject()
拿到早期引用后放入二级缓存,并从三级缓存移除
12.3 AbstractAutowireCapableBeanFactory
Bean 创建主流程在:
java
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
核心方法:
java
doCreateBean()
createBeanInstance()
populateBean()
initializeBean()
getEarlyBeanReference()
其中 doCreateBean() 负责整体创建流程。
简化流程:
text
doCreateBean
↓
createBeanInstance
实例化 Bean
↓
addSingletonFactory
把 ObjectFactory 放入三级缓存
↓
populateBean
属性注入
↓
initializeBean
Aware 回调、初始化方法、BeanPostProcessor
提前暴露三级缓存的逻辑大概是:
java
boolean earlySingletonExposure =
mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
addSingletonFactory(beanName,
() -> getEarlyBeanReference(beanName, mbd, bean));
}
这个动作发生在:
text
实例化之后,属性注入之前
12.4 AbstractAutoProxyCreator
AOP 提前代理相关逻辑主要在:
java
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator
它会参与:
java
getEarlyBeanReference()
postProcessAfterInitialization()
作用是:
text
如果 Bean 需要 AOP 代理,则在早期引用阶段或初始化后阶段创建代理对象
如果循环依赖中某个 Bean 被提前引用,Spring 会通过 getEarlyBeanReference() 提前生成代理。
如果没有被提前引用,则通常在 postProcessAfterInitialization() 阶段生成代理。
13. Spring Boot 2.6 之后的注意点
Spring Boot 2.6 开始,默认不鼓励循环依赖。
如果项目中存在循环依赖,可能会看到类似错误:
text
The dependencies of some of the beans in the application context form a cycle
可以通过配置临时开启:
yaml
spring:
main:
allow-circular-references: true
但这不建议作为长期方案。
更推荐从设计上消除循环依赖。
14. @Lazy 是否能解决循环依赖?
@Lazy 可以缓解部分循环依赖,但它不是从根上消除循环依赖。
14.1 @Lazy 标在类上
java
@Service
@Lazy
public class AService {
}
含义是:
text
启动时先不创建这个 Bean
等第一次使用时再创建
这可能让循环依赖不在启动时报错,但并不代表循环依赖不存在。
第一次真正使用这个 Bean 时,仍然可能触发循环依赖。
14.2 @Lazy 标在注入点上
java
@Service
public class AService {
private final BService bService;
public AService(@Lazy BService bService) {
this.bService = bService;
}
}
含义是:
text
这里先不注入真正的 BService
而是先注入一个 BService 的懒代理对象
等真正调用方法时,再去容器里获取真实 BService
这可以缓解一些构造器循环依赖。
例如:
text
创建 AService 时,需要 BService
因为 BService 是 @Lazy,所以先注入 BService 代理
AService 可以先创建完成
之后创建 BService 时,需要 AService
AService 已经完成,可以注入给 BService
但如果你在构造方法或 @PostConstruct 中马上调用这个懒代理,也可能再次触发循环依赖。
例如:
java
@PostConstruct
public void init() {
bService.doSomething();
}
这会导致懒代理立刻去获取真实 BService,懒加载就失去意义了。
15. 如何在项目中避免循环依赖?
虽然 Spring 能解决一部分循环依赖,但业务代码中最好尽量避免。
15.1 重新划分职责
如果:
text
AService 依赖 BService
BService 又依赖 AService
通常说明两个 Service 职责边界可能不清晰。
可以把公共逻辑抽出来:
text
AService
BService
CommonService
变成:
text
AService -> CommonService
BService -> CommonService
15.2 使用事件解耦
例如订单创建后需要通知积分服务,不一定要让两个 Service 互相调用。
可以这样设计:
text
OrderService 发布 OrderCreatedEvent
PointService 监听事件并处理积分
示例:
java
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void createOrder() {
publisher.publishEvent(new OrderCreatedEvent(this));
}
}
java
@Component
public class PointListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 增加积分
}
}
15.3 使用 Facade / ApplicationService 协调流程
避免两个底层 Service 互相调用。
可以引入一个上层协调者:
text
OrderFacade
↓
OrderService
PointService
让 OrderFacade 同时调用 OrderService 和 PointService,而不是让它们互相依赖。
15.4 使用 ObjectProvider 延迟获取
java
@Service
public class AService {
private final ObjectProvider<BService> bServiceProvider;
public AService(ObjectProvider<BService> bServiceProvider) {
this.bServiceProvider = bServiceProvider;
}
public void doSomething() {
BService bService = bServiceProvider.getObject();
bService.doSomething();
}
}
这种方式把依赖获取推迟到真正使用时。
15.5 使用 @Lazy 延迟依赖
java
@Service
public class AService {
private final BService bService;
public AService(@Lazy BService bService) {
this.bService = bService;
}
}
这可以作为临时解决方式,但不建议滥用。
16. 常见误区总结
误区一:Spring 解决循环依赖说明设计没问题
不对。
Spring 能解决的是 Bean 创建过程中的引用问题,不代表业务设计合理。
循环依赖通常意味着职责边界不清晰。
误区二:提前生成代理对象就说明 Bean 创建完成了
不对。
早期代理只是一个提前暴露的引用。
代理背后的目标对象可能仍然是半成品。
误区三:三级缓存只是为了保存三个阶段的对象
不准确。
三级缓存最关键的意义是:
text
通过 ObjectFactory 延迟生成早期引用,为 AOP 提前代理提供机会。
误区四:@Lazy 可以彻底解决循环依赖
不准确。
@Lazy 只是延迟 Bean 创建或依赖获取。
如果后续真正调用时依赖链仍然成环,问题仍可能出现。
17. 最终总结
Spring 解决循环依赖的核心逻辑可以概括为:
text
单例 Bean 在实例化之后、属性注入之前,
Spring 会把一个 ObjectFactory 放入三级缓存。
如果后续创建其他 Bean 时回头依赖当前 Bean,
Spring 就从三级缓存中调用 ObjectFactory,
拿到当前 Bean 的早期引用。
这个早期引用可能是原始对象,
也可能是提前生成的代理对象。
拿到早期引用后,依赖方可以继续完成创建,
然后再回到当前 Bean,继续完成属性注入和初始化。
一张完整流程图:
text
实例化 AService
↓
AService ObjectFactory 放入三级缓存
↓
AService 属性注入,发现需要 BService
↓
实例化 BService
↓
BService ObjectFactory 放入三级缓存
↓
BService 属性注入,发现需要 AService
↓
从三级缓存获取 AService 早期引用
↓
如果 AService 需要代理,则提前生成 AService 代理
↓
把 AService 早期引用注入 BService
↓
BService 完成创建,必要时生成 BService 代理
↓
把 BService 最终对象注入 AService
↓
AService 完成创建
↓
最终完整 Bean 进入一级缓存
最重要的几句话:
text
1. Spring 主要解决单例属性注入循环依赖。
2. 构造器循环依赖通常解决不了,因为对象还没 new 出来,无法提前暴露。
3. 三级缓存的关键价值是 ObjectFactory,它可以返回原始对象,也可以返回代理对象。
4. 谁被回头依赖,谁就提前暴露。
5. 早期代理对象不等于完整 Bean。
6. @Lazy 只是延迟创建或延迟获取,不是彻底消除循环依赖。
7. 项目中应尽量通过职责拆分、事件、Facade 等方式避免循环依赖。