Spring循环依赖的死结之谜 🔄

53 阅读11分钟

一、开篇故事:鸡生蛋还是蛋生鸡?🐔🥚

想象这样一个场景:

小明对小红说: "只有你先借我100块,我才能还你之前欠的200块。"
小红对小明说: "只有你先还我200块,我才能借你100块。"

结果:两人都在等对方先行动,陷入死锁!😱

这就是循环依赖问题的本质:A依赖B,B又依赖A,谁都无法先创建完成。


二、什么是循环依赖?🤔

2.1 定义

循环依赖是指两个或多个Bean相互依赖,形成一个闭环。

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB; // A依赖B
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA; // B依赖A
}

2.2 图解

     ServiceA
        ↓
    依赖 ServiceB
        ↓
    依赖 ServiceA
        ↓
       ......
     (死循环!)

2.3 生活类比

装修房子的死循环:

  • 木工:"等电工布完线,我才能装天花板。"
  • 电工:"等木工装完天花板,我才能安装灯具。"
  • 结果:两人都在等对方,房子永远装不完!🏚️

三、Spring如何解决循环依赖?—— 三级缓存大法 🎯

3.1 三级缓存机制

Spring通过三级缓存解决循环依赖:

public class DefaultSingletonBeanRegistry {
    
    // 一级缓存:存放完全初始化好的Bean(成品)
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 二级缓存:存放早期暴露的Bean(半成品)
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    // 三级缓存:存放Bean工厂对象(生产线)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}

3.2 三级缓存的角色

缓存级别名称作用生活类比
一级缓存singletonObjects存放完全初始化的Bean成品仓库 🏢
二级缓存earlySingletonObjects存放早期暴露的Bean半成品仓库 🏗️
三级缓存singletonFactories存放Bean工厂生产线 🏭

3.3 解决过程(字段注入)

场景:A依赖B,B依赖A

1. 创建A实例(还未填充属性)
   → 放入三级缓存(singletonFactories)
   
2. 填充A的属性,发现需要B
   → 去创建B
   
3. 创建B实例(还未填充属性)
   → 放入三级缓存
   
4. 填充B的属性,发现需要A
   → 从三级缓存获取A的ObjectFactory
   → 调用getObject()获取A的早期引用
   → A从三级缓存移到二级缓存
   → B注入A的早期引用
   
5. B完成初始化
   → 放入一级缓存
   → 从二级、三级缓存移除
   
6. A注入BA完成初始化
   → 放入一级缓存
   → 从二级、三级缓存移除
   
✅ 循环依赖解决!

3.4 图解过程

时刻1: 创建A
三级缓存: [A的工厂]
二级缓存: []
一级缓存: []

时刻2: 创建B,B需要A
三级缓存: [A的工厂, B的工厂]
二级缓存: []
一级缓存: []

时刻3: B从三级缓存获取A
三级缓存: [B的工厂]
二级缓存: [A半成品]
一级缓存: []

时刻4: B完成
三级缓存: []
二级缓存: [A半成品]
一级缓存: [B成品]

时刻5: A完成
三级缓存: []
二级缓存: []
一级缓存: [A成品, B成品]

四、为什么构造器注入无法解决循环依赖?💣

4.1 问题代码

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    // 构造器注入
    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    // 构造器注入
    @Autowired
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

4.2 启动报错

***************************
APPLICATION FAILED TO START
***************************

Description:
The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  serviceA defined in file [ServiceA.class]
↑     ↓
|  serviceB defined in file [ServiceB.class]
└─────┘

4.3 为什么无法解决?

核心原因:构造器注入时,Bean还没创建出来!

创建A的流程:
1. 调用A的构造器
   → 需要传入参数B
   → 去创建B
   
2. 调用B的构造器
   → 需要传入参数A
   → 去创建A(又回到步骤13. 死循环!💀

关键问题:

  • 字段注入:先创建实例(空对象),后填充属性
    ✅ 可以先把空对象暴露出去

  • 构造器注入:必须先有依赖对象,才能调用构造器
    ❌ 对象都没创建,无法暴露

4.4 生活类比

字段注入(可解决):

1. 先盖房子框架(创建实例)
2. 再装修内部(填充属性)
   → 框架已经存在,可以先让别人参观

构造器注入(无法解决):

1. 必须先准备好所有建材(依赖对象)
2. 才能开始盖房子(调用构造器)
   → 建材还没准备好,房子根本盖不起来

4.5 源码分析

// AbstractAutowireCapableBeanFactory.doCreateBean()
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) {
    
    // 1. 实例化Bean(调用构造器)
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    // ⚠️ 构造器注入在这一步就需要依赖对象
    // 如果依赖对象还没创建,就会触发创建
    // 形成死循环!
    
    Object bean = instanceWrapper.getWrappedInstance();
    
    // 2. 提前暴露(字段注入才能走到这一步)
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    
    // 3. 填充属性(字段注入在这一步才需要依赖对象)
    populateBean(beanName, mbd, instanceWrapper);
    
    return bean;
}

五、为什么Prototype(多例)无法解决循环依赖?💥

5.1 问题代码

@Component
@Scope("prototype") // 多例
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Component
@Scope("prototype") // 多例
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

5.2 启动报错

Error creating bean with name 'serviceA': 
Requested bean is currently in creation: Is there an unresolvable circular reference?

5.3 为什么无法解决?

核心原因:Prototype Bean不会被缓存!

Singleton(单例):
  → 创建一次,放入缓存
  → 下次直接从缓存拿
  → ✅ 可以提前暴露半成品
  
Prototype(多例):
  → 每次都创建新实例
  → 不会放入缓存
  → ❌ 无法提前暴露

5.4 详细过程

1. 获取A(多例)
   → 创建新的A实例
   → 不放入缓存(因为是prototype)
   
2. 填充A的属性,需要B
   → 获取B(多例)
   → 创建新的B实例
   
3. 填充B的属性,需要A
   → 获取A(多例)
   → 又创建新的A实例(不是第1步的A4. 无限循环创建新实例!💀

5.5 图解

Singleton(单例):
   [缓存]A实例(唯一)
  
Prototype(多例):
  每次都newA实例1, A实例2, A实例3, ......(无穷多个)

5.6 源码分析

// AbstractBeanFactory.doGetBean()
if (mbd.isSingleton()) {
    // 单例:使用缓存
    sharedInstance = getSingleton(beanName, () -> {
        return createBean(beanName, mbd, args);
    });
} else if (mbd.isPrototype()) {
    // 多例:每次都创建新实例,不缓存
    Object prototypeInstance = createBean(beanName, mbd, args);
    // ⚠️ 没有缓存机制,无法提前暴露
}

六、三级缓存为什么要有三级?🎯

6.1 为什么不是两级缓存?

假设只有两级:

  • 一级:成品
  • 二级:半成品

问题来了:如果需要AOP代理怎么办?

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    @Transactional // 需要创建代理对象
    public void doSomething() { }
}

困境:

  1. 提前暴露原始对象A
  2. B注入了原始对象A
  3. 后来A需要创建代理对象A'
  4. 最终容器中有两个A(原始A和代理A')
  5. B注入的是原始A,不是代理A'
  6. 💥 事务失效!

6.2 三级缓存的作用

三级缓存存的是ObjectFactory(工厂):

// 三级缓存存的是lambda表达式
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    
    // 如果需要AOP,这里会创建代理对象
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    
    return exposedObject; // 可能是原始对象,也可能是代理对象
}

流程:

1. A创建,放入三级缓存(工厂)
2. B需要A,调用三级缓存的工厂方法
3. 工厂方法判断是否需要AOP
   → 需要:返回代理对象A'
   → 不需要:返回原始对象A
4. A'移到二级缓存
5. B注入A'(代理对象)
6. ✅ 最终B注入的是代理对象,事务生效!

6.3 总结

缓存作用为什么需要
一级成品存放完全初始化的Bean
二级半成品存放早期暴露的Bean(可能是代理)
三级工厂延迟决定是否需要代理,保证一致性

如果没有三级缓存,AOP会出问题!


七、解决方案 💡

方案1:改用字段注入(推荐)✅

@Component
public class ServiceA {
    @Autowired // 字段注入
    private ServiceB serviceB;
}

@Component
public class ServiceB {
    @Autowired // 字段注入
    private ServiceA serviceA;
}

优点: 简单,Spring能自动解决
缺点: 字段不可变性较差,不利于测试

方案2:使用@Lazy延迟注入 ✅

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    @Autowired
    public ServiceA(@Lazy ServiceB serviceB) { // 延迟注入
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    @Autowired
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

原理:

  • @Lazy会注入一个代理对象
  • 真正使用时才会去获取真实对象
  • 打破循环依赖

方案3:Setter注入 ✅

@Component
public class ServiceA {
    private ServiceB serviceB;
    
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private ServiceA serviceA;
    
    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

方案4:重新设计(最佳)⭐⭐⭐⭐⭐

// 问题:A和B相互依赖,说明设计有问题

// 解决方案1:提取公共依赖
@Component
public class CommonService {
    // 公共逻辑
}

@Component
public class ServiceA {
    @Autowired
    private CommonService commonService;
}

@Component
public class ServiceB {
    @Autowired
    private CommonService commonService;
}

// 解决方案2:使用事件驱动
@Component
public class ServiceA {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void doSomething() {
        // 不直接调用B,而是发送事件
        eventPublisher.publishEvent(new SomeEvent());
    }
}

@Component
public class ServiceB {
    @EventListener
    public void handleEvent(SomeEvent event) {
        // 监听事件
    }
}

方案5:使用@PostConstruct ✅

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    @PostConstruct
    public void init() {
        // 在这里使用serviceB
    }
}

八、循环依赖解决能力总结表 📊

Bean作用域注入方式是否能解决原因
Singleton字段注入✅ 能三级缓存
SingletonSetter注入✅ 能三级缓存
Singleton构造器注入❌ 不能实例还未创建
Prototype字段注入❌ 不能没有缓存
PrototypeSetter注入❌ 不能没有缓存
Prototype构造器注入❌ 不能没有缓存
Singleton + Prototype任何❌ 不能Prototype不缓存

总结规律

能解决:

  • ✅ 单例 + 字段注入
  • ✅ 单例 + Setter注入
  • ✅ 单例 + 构造器注入 + @Lazy

不能解决:

  • ❌ 构造器注入(无@Lazy)
  • ❌ Prototype(任何注入方式)
  • ❌ 单例依赖Prototype

九、面试高频问题 🎤

Q1: Spring如何解决循环依赖?

答: 通过三级缓存机制:

  1. 一级缓存存成品Bean
  2. 二级缓存存早期暴露的Bean
  3. 三级缓存存Bean工厂

在Bean创建过程中,先实例化,然后提前暴露到三级缓存,再填充属性。如果有循环依赖,可以从三级缓存获取早期引用,打破循环。

Q2: 为什么构造器注入无法解决循环依赖?

答: 因为构造器注入时,Bean实例还未创建,无法提前暴露。而三级缓存的前提是Bean实例已经创建(至少是个空对象),才能暴露出去。

Q3: 为什么需要三级缓存,二级不够吗?

答: 为了支持AOP。三级缓存存的是ObjectFactory,可以在真正需要时才决定是否创建代理对象,保证注入的Bean和最终容器中的Bean是同一个(可能都是代理对象)。

Q4: Prototype为什么无法解决循环依赖?

答: 因为Prototype Bean每次获取都创建新实例,不会缓存。没有缓存就无法提前暴露,所以无法解决循环依赖。

Q5: 如何避免循环依赖?

答:

  1. 重新设计,避免相互依赖
  2. 提取公共依赖到第三个类
  3. 使用事件驱动代替直接调用
  4. 使用@Lazy延迟注入
  5. 改用字段注入或Setter注入

十、最佳实践建议 💡

1. 优先使用构造器注入

// ✅ 推荐(有循环依赖时加@Lazy)
@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    @Autowired
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

理由:

  • 字段可以是final,不可变
  • 依赖关系明确
  • 方便单元测试(不需要Spring容器)

2. 避免循环依赖

重构方法:

// ❌ 不好:A和B循环依赖
A → B
↑   ↓
←───┘

// ✅ 好:提取公共逻辑到C
A → C ← B

3. 使用事件解耦

// 替代直接调用
@Component
public class OrderService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void createOrder(Order order) {
        // 保存订单
        orderRepository.save(order);
        
        // 发送事件,不直接调用其他服务
        eventPublisher.publishEvent(new OrderCreatedEvent(order));
    }
}

@Component
public class InventoryService {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 减库存
    }
}

十一、源码跟踪 🔍

核心方法

// DefaultSingletonBeanRegistry.getSingleton()
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        // 1. 先从一级缓存获取
        Object singletonObject = this.singletonObjects.get(beanName);
        
        if (singletonObject == null) {
            // 2. 标记正在创建
            beforeSingletonCreation(beanName);
            
            try {
                // 3. 调用工厂方法创建Bean
                singletonObject = singletonFactory.getObject();
            } finally {
                // 4. 移除创建标记
                afterSingletonCreation(beanName);
            }
            
            // 5. 放入一级缓存
            addSingleton(beanName, singletonObject);
        }
        
        return singletonObject;
    }
}

// 获取早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 从一级缓存获取
    Object singletonObject = this.singletonObjects.get(beanName);
    
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 2. 从二级缓存获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            
            if (singletonObject == null && allowEarlyReference) {
                // 3. 从三级缓存获取工厂
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                
                if (singletonFactory != null) {
                    // 4. 调用工厂方法获取早期引用
                    singletonObject = singletonFactory.getObject();
                    
                    // 5. 放入二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    
                    // 6. 从三级缓存移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    
    return singletonObject;
}

十二、总结口诀 📝

循环依赖要记清,
三级缓存来帮忙。
一级成品二级半,
三级工厂藏玄机。

字段注入能解决,
构造注入会翻车。
Prototype不缓存,
循环依赖解不了。

AOP需要三级存,
代理对象统一管。
设计合理是王道,
循环依赖要避免!

参考资料 📚


下期预告: 135-Spring的@Lookup方法注入的实现原理 🔍


编写时间:2025年
作者:技术文档小助手 ✍️
版本:v1.0

愿你的代码没有循环,只有优雅的单向流动! 🎯