星驰编程网

免费编程资源分享平台_编程教程_代码示例_开发技术文章

Spring破解循环依赖:三级缓存的魔法揭秘

为什么每个Spring开发者都必须掌握循环依赖的解决方案?

在开发岗位面试中,Spring框架的循环依赖问题几乎是必考题。但更重要的是,在实际企业级开发中,理解循环依赖的解决机制能够帮助你:

  • 快速定位和解决诡异的Bean创建异常
  • 设计出更优雅、解耦的系统架构
  • 避免陷入"明明代码没问题,为什么启动失败"的困境
  • 提升对Spring核心机制的理解深度

本期咱们一起探讨剖析这个既经典又实用的问题,让你在面试和实际开发中都能游刃有余。

什么是循环依赖?

简单来说,循环依赖就是两个或多个Bean之间相互依赖,形成一个"死循环"。例如Bean A依赖于Bean B,而Bean B又依赖于Bean A。这种情况下,Spring在初始化这些Bean时会陷入两难境地:先创建A需要B,但创建B又需要A。

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

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

Bean的创建过程:理解关键前提

要理解Spring如何解决循环依赖,首先需要了解Bean的完整创建过程:

  1. 实例化:通过构造函数创建Bean的原始对象(相当于new操作)
  2. 属性填充:为对象注入依赖的其他Bean
  3. 初始化:执行各种回调方法(如@PostConstruct)

循环依赖问题的核心在于:我们需要在A的属性填充阶段注入B,但B此时可能还未创建完成

三级缓存:Spring的解决方案

Spring通过三级缓存机制巧妙解决了这个问题:

第一级缓存:singletonObjects

存放已经完全初始化好的单例Bean(成品),一旦需要获取Bean,首先从这里查找。

第二级缓存:earlySingletonObjects

存放提前暴露的原始Bean实例(半成品),用于解决循环依赖。

第三级缓存:singletonFactories

存放Bean工厂对象,用于生成原始Bean的提前引用。

解决循环依赖的完整流程

让我们通过一个具体例子来看Spring如何处理A和B的循环依赖:

  1. 开始创建A
  2. 调用A的构造函数实例化A对象
  3. 将A对象的工厂放入三级缓存(singletonFactories)
  4. 开始为A填充属性,发现需要B
  5. 转而创建B
  6. 调用B的构造函数实例化B对象
  7. 将B对象的工厂放入三级缓存
  8. 开始为B填充属性,发现需要A
  9. 获取A的引用
  10. 从一级缓存查找A(未找到)
  11. 从二级缓存查找A(未找到)
  12. 从三级缓存找到A的工厂,生成A的早期引用
  13. 将A的早期引用放入二级缓存,并从三级缓存移除
  14. 将A的早期引用注入到B中
  15. 完成B的创建
  16. B完成属性填充和初始化
  17. 将B放入一级缓存
  18. 从二级和三级缓存中移除B的相关引用
  19. 完成A的创建
  20. 将B注入到A中(此时B已在一级缓存中)
  21. A完成属性填充和初始化
  22. 将A放入一级缓存
  23. 从二级缓存中移除A的引用

这个过程中,关键点在于Spring在实例化后就将对象工厂放入三级缓存,使得其他Bean可以获取到它的早期引用,即使它还没有完成初始化。

AOP代理与循环依赖

当涉及AOP代理时,情况变得更加复杂。Spring需要确保循环依赖的双方注入的是代理对象而不是原始对象。三级缓存中的工厂正好解决了这个问题——它不仅可以返回原始对象,还可以返回代理对象。

// 三级缓存中的工厂逻辑
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    // 如果需要代理,这里会返回代理对象而不是原始对象
    return mbd.getEarlyBeanReference(beanName, bean);
}

无法解决的循环依赖场景

需要注意的是,Spring并非能解决所有循环依赖:

  1. 构造函数循环依赖:无法解决,因为实例化都无法完成


@Component
public class A {
    public A(B b) { ... }
}

@Component
public class B {
    public B(A a) { ... }
}
  1. 原型(prototype)作用域的循环依赖:无法解决
  2. @Async注解方法的循环依赖:可能无法正确解决

实际开发中的建议

虽然Spring提供了循环依赖的解决方案,但在实际项目中,咱们仍应该:

  1. 尽量避免循环依赖:良好的设计应该避免这种紧耦合
  2. 使用setter注入而非构造器注入:setter注入允许Spring解决循环依赖
  3. 使用@Lazy注解:延迟加载可以打破循环依赖
@Component
public class A {
    @Lazy
    @Autowired
    private B b;
}

Spring通过三级缓存机制巧妙解决了循环依赖问题,其核心思想是提前暴露原始对象的引用,使得Bean在尚未完成初始化时就能被其他Bean引用。这种设计既体现了Spring框架的精巧,也展示了解决复杂问题的创新思维。

小编认为理解这个机制不仅有助于应对面试,更能让我们在实际开发中更好地使用Spring框架,写出更健壮、可维护的代码。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言