面试官:Spring是如何解决循环依赖的,一定需要三级缓存吗?

185 阅读6分钟

循环依赖是Spring面试中的经典问题,也是实际开发中容易踩的坑。比如,Bean A依赖Bean B,而Bean B又依赖Bean A,Spring是如何解决这种“鸡生蛋还是蛋生鸡”的问题的?有人说必须用三级缓存,但真的没有其他办法吗?本文用大白话+代码示例,帮你彻底搞懂!


目录

一、什么是循环依赖?

二、Spring的三级缓存是什么?

三、三级缓存如何解决循环依赖?

四、为什么必须用三级缓存?

五、不用三级缓存的替代方案?

六、哪些循环依赖无法解决?

七、实际开发中的建议

总结


一、什么是循环依赖?

假设有两个类互相依赖,Spring在创建Bean时会陷入死循环:

@Component
public class A {
    @Autowired
    private B b;  // A依赖B
}

@Component
public class B {
    @Autowired
    private A a;  // B依赖A
}

启动时会报错:BeanCurrentlyInCreationException。但实际开发中,Spring却成功解决了这个问题!它的秘密武器就是三级缓存


二、Spring的三级缓存是什么?

Spring内部通过三个Map(俗称三级缓存)管理Bean的创建过程:

缓存名称作用
singletonObjects存放完全初始化好的单例Bean(成品)
earlySingletonObjects存放提前暴露的Bean(半成品,未填充属性)
singletonFactories存放Bean的工厂对象,用于生成半成品Bean

核心思想:在Bean未完全初始化前,提前暴露它的引用,打破循环依赖。


三、三级缓存如何解决循环依赖?

以A和B的循环依赖为例,Spring创建Bean的流程如下:

  1. 创建A实例

    • 实例化A(调用构造方法,得到一个“原始对象”)。
    • 将A的工厂对象(用于生成半成品A)放入三级缓存singletonFactories
  2. 填充A的属性

    • 发现A依赖B,于是开始创建B。
  3. 创建B实例

    • 实例化B(得到原始对象)。
    • 将B的工厂对象放入三级缓存。
  4. 填充B的属性

    • 发现B依赖A,于是从三级缓存中找到A的工厂,生成半成品A,并放入二级缓存earlySingletonObjects
    • 将半成品A注入到B中,完成B的初始化。
    • 将初始化好的B放入一级缓存singletonObjects
  5. 完成A的初始化

    • 将初始化好的B注入到A中,完成A的初始化。
    • 将A从二级缓存移到一级缓存。

整个过程通过提前暴露半成品对象,避免了死锁


四、为什么必须用三级缓存?

关键问题:如果只有两级缓存(一级成品+二级半成品),能不能解决问题?

答案是:能,但前提是没有AOP代理
如果Bean需要被代理(比如用了@Transactional@Async),Spring会在初始化阶段生成代理对象。此时,如果只有两级缓存:

  1. 在填充属性时,注入的是原始对象,而不是代理对象。
  2. 最终放入一级缓存的代理对象和已注入的原始对象不一致,导致严重问题!

三级缓存的作用
通过singletonFactories存储的工厂对象,可以动态决定返回原始对象还是代理对象。例如,当A被AOP增强时,工厂会生成代理对象并返回,确保全局唯一性。


五、不用三级缓存的替代方案?

假设没有AOP,可以简化流程,仅用两级缓存。但实际开发中,Spring的AOP功能被广泛使用,因此三级缓存是必要的。
结论

  • 如果项目完全不用AOP,理论上可以去掉三级缓存。
  • 但Spring设计时考虑了通用性,因此必须保留三级缓存。

六、哪些循环依赖无法解决?

Spring只能解决单例Bean通过Setter/字段注入的循环依赖。以下情况会失败:

  1. 构造器注入的循环依赖

    @Component
    public class A {
        private B b;
        public A(B b) { this.b = b; }  // 构造器注入
    }
    
    @Component
    public class B {
        private A a;
        public B(A a) { this.a = a; }  // 构造器注入
    }
    

    原因:构造器注入需要先完成Bean的实例化,无法提前暴露半成品。

  2. 多例Bean(@Scope("prototype"))
    Spring不缓存多例Bean,因此无法解决循环依赖。


七、实际开发中的建议

  1. 尽量避免循环依赖:代码结构不合理时容易引发循环依赖,建议通过重构解决。

  2. 优先使用Setter/字段注入:构造器注入虽然安全,但无法解决循环依赖。

  3. 利用@Lazy延迟加载:对某个Bean添加@Lazy,让Spring延迟注入,打破循环。

    @Component
    public class A {
        @Lazy  // 延迟注入B
        @Autowired
        private B b;
    }
    


总结

Spring通过三级缓存+提前暴露半成品对象解决循环依赖问题,核心目的是处理AOP代理对象的唯一性。虽然理论上两级缓存可以解决部分场景,但三级缓存是Spring设计上的必要选择。理解这一机制,不仅能应对面试,还能在遇到相关Bug时快速定位原因!