Spring 如何解决循环依赖的问题

70 阅读4分钟
  1. 什么是循环依赖? 循环依赖是指两个或多个Bean相互依赖,形成一个闭环。例如:

A依赖B,B也依赖A:A -> B -> A

更复杂的循环:A -> B -> C -> A

如果没有特殊处理,这种依赖关系会导致无限递归,最终栈溢出。

  1. Spring解决循环依赖的核心原理 Spring解决循环依赖的核心在于 三级缓存 和 Bean的“实例化”与“属性注入”分离 的创建过程。

关键点一:Bean创建的生命周期(简化版) 对于一个单例Bean,Spring的创建过程主要分为以下几个步骤:

实例化: 通过反射调用构造函数创建一个对象(此时对象属性均为空,可以理解为new A())。

属性填充: 为刚刚创建的对象注入其依赖的Bean(即调用setter方法或填充@Autowired标记的字段)。

初始化: 调用@PostConstruct方法、执行InitializingBean接口的afterPropertiesSet方法等。

放入单例池: 将完全初始化好的Bean放入单例池(一级缓存),之后程序就可以获取到它了。

解决循环依赖的关键就在于将“实例化”和“属性填充”这两个步骤分开了。 你可以在A对象刚被实例化(但属性还为空)的时候,就提前暴露这个“不完整”的引用,让其他Bean(比如B)能够先引用到它。

关键点二:三级缓存 Spring容器内部维护了三个Map,也就是我们常说的“三级缓存”:

缓存级别 缓存名称 存储内容 说明 一级缓存 singletonObjects 完整的单例Bean 存放已经完全初始化好的Bean。程序正常获取Bean的地方。 二级缓存 earlySingletonObjects 早期的Bean引用 存放刚刚实例化,但还未进行属性填充的Bean的代理对象或原始对象。用于解决循环依赖。 三级缓存 singletonFactories 单例对象工厂 存放一个ObjectFactory工厂对象。当发生循环依赖时,会调用这个工厂的getObject()方法来获取一个早期的Bean引用。 3. 解决循环依赖的流程(以 A → B → A 为例) 让我们一步步跟踪Spring是如何解决这个循环依赖的:

开始创建A

首先,Spring准备创建Bean A。在调用A的构造函数之前,会先将一个能获取“早期A引用”的ObjectFactory工厂放入三级缓存(singletonFactories)中。

实例化A

通过反射new A()创建A的实例。此时A的属性b是null。

为A填充属性B(发现依赖B)

Spring发现A依赖B,于是尝试从容器中获取Bean B。

开始创建B

容器中还没有B,于是Spring开始创建B。同样,在实例化B之前,会将一个能获取“早期B引用”的ObjectFactory工厂放入三级缓存。

实例化B

通过反射new B()创建B的实例。此时B的属性a是null。

为B填充属性A(发现循环依赖!)

Spring发现B依赖A,于是尝试从容器中获取Bean A。

获取A的流程如下:

查找一级缓存(singletonObjects): 没有,因为A还没创建完。

查找二级缓存(earlySingletonObjects): 没有。

查找三级缓存(singletonFactories): 找到了! 在步骤1中存入的ObjectFactory。

执行工厂方法: Spring调用这个ObjectFactory的getObject()方法。这个方法会返回一个A的早期引用(可能是原始对象,也可能是AOP代理对象)。

升级缓存: 将得到的早期引用A从三级缓存移动到二级缓存(同时删除三级缓存中的工厂)。

此时,B成功获取到了A的早期引用(虽然不完整,但地址已经确定),并将其注入到自己的属性a中。

B完成创建

B顺利完成了属性填充和初始化。

B被放入一级缓存,同时清理二级和三级缓存中关于B的条目。

A继续属性填充

此时,步骤3中A获取B的请求终于返回了已经创建好的B(从一级缓存中获取)。

A将B注入到自己的属性b中。

A完成创建

A顺利完成了属性填充和初始化。

A被放入一级缓存,同时清理二级缓存中关于A的条目。

至此,循环依赖被成功解决,A和B都成为了完整的Bean。