Spring 循环依赖详解:三级缓存、早期引用、AOP 代理与懒加载

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() {
    }
}

AServiceBService 都需要代理。

假设 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 同时调用 OrderServicePointService,而不是让它们互相依赖。


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 等方式避免循环依赖。
相关推荐
野生技术架构师1 小时前
2026年最全Java面试题及答案汇总(建议收藏,面试前看这篇就够了)
java·开发语言·面试
一只叫煤球的猫2 小时前
ThreadForge 源码解读一:ThreadScope 如何把并发任务放进清晰边界?
java·面试·开源
洛_尘3 小时前
Python 5:使用库
java·前端·python
程序员小假3 小时前
HTTP3 性能更好,为什么内网微服务依然多用 HTTP2?HTTP2 内网优势是什么?
java·后端
Mr数据杨3 小时前
【Codex】用教案主体模块沉淀标准化教学设计内容
java·开发语言·django·codex·项目开发
苍煜3 小时前
RocketMQ系列第三篇:Java原生基础使用实操,手把手写生产者消费者Demo
java·rocketmq·java-rocketmq
Andya_net4 小时前
Java | Java内存模型JMM
java·开发语言
182******20834 小时前
2026年java后端还有机会吗?还能找到工作吗?
java·开发语言
XS0301064 小时前
Java基础 map集合
java·哈希算法·散列表