什么是 Spring 的循环依赖问题?

#Java#面试八股

什么是 Spring 的循环依赖问题?

结论先行:循环依赖指 Bean 之间形成依赖环——A 依赖 B、B 又依赖 A(也可以是 A→B→C→A 的自依赖链)。Spring 只能自动解决"单例 + setter/字段注入"这一种:靠三级缓存提前暴露引用。构造器注入、prototype 作用域、@Async 代理、@DependsOn 形成的环都解决不了,Spring Boot 2.6 起更是默认直接拒绝循环依赖,逼你把设计理顺。

一、环长什么样

A ──注入──▶ B ──注入──▶ A          (两节点环,最常见)
A ──▶ B ──▶ C ──▶ A                (三节点环,本质相同)
A ──▶ A                            (自注入,常见于 AOP 自调用修复)

二、能不能解决

循环类型能否解决原因建议做法
单例 + setter/字段注入能实例已创建,可提前暴露引用保持现状或重构
单例 + 构造器注入否实例还没造出来,无引用可暴露改字段注入 + @Lazy,或重构
prototype 任一方否原型 Bean 不缓存,无三级缓存重构、用 ObjectProvider 延迟获取
@Async / 自定义 BPP 代理否代理在初始化后生成,与早期引用不一致把代理方法拆到独立 Bean
@DependsOn 成环否依赖顺序无法拓扑排序去掉环
跨容器/父子容器通常不能缓存各自独立合并配置、抽公共服务

三、Spring 的处理过程(单例 + setter)

getBean(A) → new A → 暴露 A 的 ObjectFactory 到三级缓存
   → 填充 A:发现依赖 B → getBean(B)
        → new B → 暴露 B 的 ObjectFactory
        → 填充 B:发现依赖 A → getSingleton(A, true)
             → 三级缓存取出 A 的工厂 → 得到早期引用 → 放入二级
        → B 完成 → 放入一级
   → A 拿到 B → A 完成 → 放入一级

详细机制见三级缓存一章,这里只需记住结论:解决的前提是"实例已存在,只是没装配完"。

四、代码与配置

// 典型环:两个 Service 互相注入
@Service
public class AService {
    @Autowired
    private BService bService;      // A → B
}

@Service
public class BService {
    @Autowired
    private AService aService;      // B → A,构成环
}
// 用 @Lazy 打断环:注入的是延迟代理,首次调用才真正解析目标
@Service
public class BService {
    private final AService aService;

    public BService(@Lazy AService aService) {
        this.aService = aService;
    }
}
# 不建议长期开启,仅作兼容旧项目的过渡开关
spring:
  main:
    allow-circular-references: true

报错长这样,看到它就该想到"环 + 构造器注入":

The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  aService
↑     ↓
|  bService
└─────┘
Action: Relying upon circular references is discouraged...

五、正确的解法排序

  1. 重构分层:把双向依赖中真正被共享的逻辑抽到第三个 Bean(如 XxxSupport),让依赖变单向;
  2. 事件解耦:A 完成后发事件,B 监听,避免 A 直接依赖 B;
  3. @Lazy 兜底:注入代理,把"解析依赖"推迟到首次调用;
  4. ObjectProvider<T> / ApplicationContext.getBean:运行时按需拉取,属于依赖查找;
  5. allow-circular-references=true:最后手段,且要清楚它掩盖的是设计问题。

六、常见追问

  • 问:为什么构造器注入的环一定失败? 因为 new A(b) 需要先有 B,new B(a) 又需要先有 A,两者互等,且此时没有任何实例可曝光。
  • 问:单例 + 构造器注入 + @Lazy 为什么可以? @Lazy 让构造器参数变成代理对象,new A(proxyB) 不再要求 B 已存在。
  • 问:循环依赖和内存泄漏有关吗? 无关,但环状引用会让 Bean 无法被正常销毁顺序处理,容器关闭时顺序可能不符合预期。
  • 问:怎么提前发现环? 单测里 @SpringBootTest 启动即暴露;也可用 ArchUnit / 依赖分析工具在 CI 中检查包依赖方向。

小结

一句话:循环依赖是设计问题,Spring 的容忍只是补丁。答题时明确"只有单例 + 字段/setter 注入能救、靠三级缓存提前暴露",再补 @Lazy、事件解耦、allow-circular-references 三个层次的处理手段,最后表个态"生产上倾向重构",观点就完整了。

笔记加载中…