文章目录
- [1. 循环依赖的定义与问题](#1. 循环依赖的定义与问题)
-
- [1.1 什么是循环依赖?](#1.1 什么是循环依赖?)
- [1.2 循环依赖为何成为问题?](#1.2 循环依赖为何成为问题?)
- [2. Spring框架解决循环依赖的核心机制:三级缓存](#2. Spring框架解决循环依赖的核心机制:三级缓存)
-
- [2.1 三级缓存的构成与作用](#2.1 三级缓存的构成与作用)
-
- [2.1.1 一级缓存 (singletonObjects)](#2.1.1 一级缓存 (singletonObjects))
- [2.1.2 二级缓存 (earlySingletonObjects)](#2.1.2 二级缓存 (earlySingletonObjects))
- [2.1.3 三级缓存 (singletonFactories)](#2.1.3 三级缓存 (singletonFactories))
- [2.2 循环依赖的解决流程详解](#2.2 循环依赖的解决流程详解)
- [2.3 为何需要三级缓存而非二级?------ AOP代理的考量](#2.3 为何需要三级缓存而非二级?—— AOP代理的考量)
- [3. Spring循环依赖机制的局限性](#3. Spring循环依赖机制的局限性)
-
- [3.1 无法解决的场景:构造函数注入](#3.1 无法解决的场景:构造函数注入)
- [3.2 无法解决的场景:原型作用域 (Prototype Scope) Bean](#3.2 无法解决的场景:原型作用域 (Prototype Scope) Bean)
- [3.3 特殊场景:@Configuration类中的循环依赖](#3.3 特殊场景:@Configuration类中的循环依赖)
- [4. 解决与规避循环依赖的最佳实践与方案](#4. 解决与规避循环依赖的最佳实践与方案)
-
- [4.1 根本之道:优化代码与架构设计](#4.1 根本之道:优化代码与架构设计)
- [4.2 技术手段:利用Spring特性打破循环](#4.2 技术手段:利用Spring特性打破循环)
-
- [4.2.1 使用 @Lazy 注解进行延迟加载](#4.2.1 使用 @Lazy 注解进行延迟加载)
- [4.2.2 切换为Setter或字段注入](#4.2.2 切换为Setter或字段注入)
- [4.2.3 其他方法(如 @PostConstruct)](#4.2.3 其他方法(如 @PostConstruct))
1. 循环依赖的定义与问题
1.1 什么是循环依赖?
在Spring框架的上下文中,循环依赖(Circular Dependency)指的是两个或多个Bean之间形成了一个封闭的依赖链 。最简单的形式是Bean A依赖于Bean B,同时Bean B又依赖于Bean A。更复杂的情况可能涉及多个Bean,例如A -> B -> C -> A。当Spring容器在启动和初始化Bean的过程中,沿着这个依赖链进行实例化和注入时,会发现最终回到了依赖链的起点,从而形成一个无法自然解开的"死结"。
1.2 循环依赖为何成为问题?
循环依赖通常被认为是软件设计上的一个"坏味道"(Code Smell),它暗示了类之间的职责划分不清和耦合度过高 。其主要问题体现在:
- 破坏单一职责原则:相互依赖的类很可能承担了过多的职责,导致类的边界模糊。
- 增加代码的复杂性和维护成本:紧密耦合的组件难以被独立理解、测试和修改。对其中一个Bean的改动很可能会对依赖链上的其他Bean产生意想不到的影响。
- 可能导致初始化失败:在某些注入方式下(如构造函数注入),Spring容器无法完成Bean的创建,因为每个Bean都在等待对方先被创建,这会导致应用程序启动失败 。
2. Spring框架解决循环依赖的核心机制:三级缓存
尽管循环依赖在设计上不受推崇,但Spring框架为了提升其灵活性和兼容性,内置了一套精巧的机制来解决特定场景下的循环依赖问题。这个机制的核心是位于 DefaultSingletonBeanRegistry 类中的三级缓存 。这套机制仅对 单例作用域(Singleton Scope) 的 属性注入(Setter或字段注入) 有效。
2.1 三级缓存的构成与作用
DefaultSingletonBeanRegistry 内部维护了三个核心的Map结构,它们构成了所谓的三级缓存:
2.1.1 一级缓存 (singletonObjects)
- 定义:private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
- 作用:用于存放已经完全初始化好的单例Bean实例。这是一个"成品"缓存,一旦Bean被成功创建并完成所有属性注入和初始化回调,就会被放入此缓存中。后续任何对该Bean的请求都将直接从这里返回 。
2.1.2 二级缓存 (earlySingletonObjects)
- 定义:private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
- 作用 :用于存放提前暴露的单例Bean实例。这些Bean已经被实例化(构造函数已执行),但其属性注入和初始化尚未完成。它们是"半成品",主要目的是为了打破循环依赖。当一个Bean A在填充属性时需要Bean B,而Bean B又需要Bean A时,可以从这个缓存中获取一个A的早期引用。
2.1.3 三级缓存 (singletonFactories)
- 定义:private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
- 作用 :用于存放创建早期Bean实例的 工厂对象(ObjectFactory)。它本身不存储Bean实例,而是存储一个能够创建该Bean早期引用的工厂。这个工厂的意义在于,它允许Spring在真正需要暴露早期引用时才去创建它,更重要的是,它为AOP代理的创建提供了时机。如果一个Bean需要被代理,这个工厂返回的就不是原始对象,而是代理对象 。
2.2 循环依赖的解决流程详解
为了直观理解三级缓存的工作原理,我们以一个经典的A依赖B、B依赖A的场景为例,结合Spring getSingleton 方法的核心逻辑进行说明:
-
创建Bean A:
- getSingleton("A") 被调用,首先检查一级缓存 singletonObjects,未找到A。
- Spring开始创建A。在实例化A(即调用其构造函数)之后,但在进行属性注入之前,Spring会将一个用于创建A的早期引用的 ObjectFactory 放入三级缓存 singletonFactories 中。同时,将A的名称标记为"正在创建中" 。
-
A注入属性B:
- Spring发现A依赖于B,于是尝试获取Bean B,调用 getSingleton("B")。
-
创建Bean B:
- 同样,getSingleton("B") 检查一级缓存,未找到B。
- Spring开始创建B。实例化B后,同样将B的 ObjectFactory 放入三级缓存 singletonFactories,并将B标记为"正在创建中"。
-
B注入属性A(循环点):
- Spring发现B依赖于A,于是再次调用 getSingleton("A")。
- 此时的 getSingleton("A") 调用流程如下:
- 检查一级缓存 singletonObjects,未找到A。
- 检查A是否"正在创建中",发现是。
- 检查二级缓存 earlySingletonObjects,仍然未找到A 。
- 检查三级缓存 singletonFactories,成功找到了A的ObjectFactory 。
- 调用这个 ObjectFactory 的 getObject() 方法,生成A的早期引用(如果A有AOP代理,此时返回的就是代理对象)。
- 将这个早期引用放入二级缓存 earlySingletonObjects,并从三级缓存中移除A的 ObjectFactory。
- getSingleton("A") 返回A的早期引用。
-
B完成创建:
- B成功获取到了A的早期引用并完成属性注入。
- B执行后续的初始化流程,成为一个完整的Bean。
- B被放入一级缓存 singletonObjects,并从二级、三级缓存及"正在创建中"集合中移除。
- getSingleton("B") 调用结束,返回完整的B实例。
-
A完成创建:
- 回到第2步,A现在也获取到了完整的B实例,并完成属性注入。
- A执行后续初始化流程,成为一个完整的Bean。
- A被放入一级缓存 singletonObjects,并从相关集合中移除。
至此,A和B的循环依赖被成功解决。
2.3 为何需要三级缓存而非二级?------ AOP代理的考量
一个常见的问题是:为什么需要三级缓存,只用二级缓存存储早期引用不行吗?
答案关键在于AOP代理。如果一个Bean被AOP切面代理,那么注入到其他Bean中的应该是其代理对象,而不是原始对象。如果只用二级缓存,那么就必须在Bean实例化后立刻创建代理对象并放入二级缓存。但这会带来一个问题:无论该Bean最终是否真的陷入循环依赖,代理对象都会被提前创建,这违背了Spring尽可能延迟代理创建的设计原则。
三级缓存 singletonFactories 巧妙地解决了这个问题。它存储的是一个工厂,这个工厂的 getObject() 方法才真正执行代理对象的创建逻辑。只有当Bean B确实需要注入Bean A,并且在三级缓存中找到了A的工厂时,才会调用工厂方法创建A的代理对象。如果不存在循环依赖,这个工厂就永远不会被调用,代理的创建就可以推迟到标准的后处理阶段(BeanPostProcessor),这更加高效和符合逻辑。
3. Spring循环依赖机制的局限性
尽管三级缓存机制非常精巧,但它并非万能药,在以下几种常见场景中,Spring无法解决循环依赖。
3.1 无法解决的场景:构造函数注入
Spring无法解决构造函数注入(Constructor Injection)导致的循环依赖 。
- 原因:构造函数注入要求在对象实例化时,其所有依赖项必须已经可用并作为参数传入。在A依赖B、B依赖A的场景下,创建A的实例需要一个B的实例,而创建B的实例又需要一个A的实例。这个逻辑形成了一个无法打破的死锁,因为在任何一个Bean的构造函数完成之前,都无法创建出对方的实例,自然也就无法生成可以提前暴露的"早期引用"。
- 异常信息:当发生此问题时,Spring容器会抛出 BeanCurrentlyInCreationException,并附带一条清晰的错误信息,描述了循环依赖的路径 。
3.2 无法解决的场景:原型作用域 (Prototype Scope) Bean
Spring无法解决原型作用域Bean的循环依赖 。
- 原因:Spring容器对于原型作用域的Bean,其职责是"按需创建新实例",但并不会管理这些实例的完整生命周期,更不会缓存它们 。因此,三级缓存机制完全不适用于原型Bean。当原型Bean A需要原型Bean B,而B又需要A时,Spring会陷入一个无限创建新实例的递归调用中,直到栈溢出。
- 异常信息:为了防止无限递归,Spring在创建原型Bean时,会使用一个基于 ThreadLocal 的标记(如 prototypesCurrentlyInCreation)来检测当前线程是否正在创建同一个原型Bean 。一旦检测到,会立即抛出 BeanCurrentlyInCreationException 。
3.3 特殊场景:@Configuration类中的循环依赖
当 @Configuration 注解的配置类中的 @Bean 方法之间形成相互调用,也可能导致循环依赖问题 。例如,beanA() 方法中调用了 beanB(),而 beanB() 方法中又调用了 beanA()。Spring在解析这些配置类并构建Bean定义时,会检测到这种循环,并通常会抛出 BeanCurrentlyInCreationException。
4. 解决与规避循环依赖的最佳实践与方案
面对循环依赖问题,我们有多种解决策略,其优先级和推荐程度各不相同。
4.1 根本之道:优化代码与架构设计
这是最推荐、最彻底的解决方案。循环依赖的出现往往是职责划分不合理的信号。
- 提取公共逻辑:如果A和B相互依赖是因为它们共享某些业务逻辑,可以将这部分逻辑提取到一个新的服务C中。然后让A和B都依赖于C,形成 A -> C 和 B -> C 的单向依赖关系 。
- 接口分离与依赖倒置:通过引入接口,将依赖关系从具体实现解耦。有时候,通过重新设计接口和依赖关系,可以将双向依赖转为单向依赖。
- 使用事件驱动模型:对于一些非强同步的调用,可以使用Spring的事件发布/监听机制(ApplicationEventPublisher)来解耦。例如,A完成某个操作后,不再直接调用B的方法,而是发布一个事件,由B的监听器来异步处理。这样,A和B之间就不再有直接的编译时依赖 。
4.2 技术手段:利用Spring特性打破循环
当立即重构不现实时,可以采用以下技术手段作为变通方案。
4.2.1 使用 @Lazy 注解进行延迟加载
- 作用:@Lazy 是解决循环依赖,尤其是构造函数注入循环依赖的有力工具 。
- 原理 :当你在一个注入点(字段、构造函数参数或Setter方法)上使用 @Lazy 注解时,Spring容器不会立即注入实际的Bean实例,而是注入一个该Bean的代理对象 。这个代理对象在它的任何方法第一次被调用时,才会真正去容器中查找并委托给实际的Bean实例。通过这种方式,Bean的实例化和其依赖的实际使用被分离开来,从而打破了在Bean创建阶段的依赖闭环 。
- 内部机制:Spring内部会通过 ProxyFactory 等工具,围绕一个 LazyInitTargetSource 来创建这个代理。当代理方法被调用时,TargetSource 负责从BeanFactory中解析并返回真正的目标Bean。
4.2.2 切换为Setter或字段注入
如果项目遇到了构造函数注入的循环依赖,最直接的修改方式就是将其中的一个或多个依赖改为Setter注入或字段注入 。这使得Spring可以利用前述的三级缓存机制来解决依赖问题,因为对象可以先通过构造函数实例化,然后再进行属性填充。
4.2.3 其他方法(如 @PostConstruct)
在某些情况下,可以通过让一个Bean实现 InitializingBean 接口或使用 @PostConstruct 注解,在对象的构造和基本属性注入完成后,再手动从应用上下文中获取依赖的Bean。这种方式侵入性较强,但可以作为一种备选方案。