什么是循环依赖:
Spring循环依赖是指在Spring管理的Bean(即由Spring容器负责创建、配置和管理的对象)之间,存在一种互相依赖的关系,使得它们形成了一个闭环。具体来说,循环依赖是指两个或多个Bean之间通过构造器注入、setter注入或其他依赖注入方式,以如下形式互相引用:
- 直接循环依赖:如Bean A依赖Bean B,同时Bean B反过来也依赖Bean A。
- 间接循环依赖:更为复杂的情况,如Bean A依赖Bean B,Bean B依赖Bean C,而Bean C又依赖回Bean A,形成一个闭环。
什么场景会遇到循环依赖:
循环依赖通常发生在以下几种软件设计或实现的场景中,特别是在使用Spring框架进行依赖注入(Dependency Injection, DI)的过程中:
-
业务逻辑耦合:
- 当不同业务组件(如服务、模块或类)的设计过于紧密,导致它们在功能或数据处理上互相依赖,可能形成循环依赖。例如,服务A处理某个业务流程的一部分,依赖于服务B提供的某些数据或功能;而服务B在处理另一个业务环节时,又反过来依赖于服务A的输出或操作。
-
过度复杂的类结构:
- 类设计中,如果类A包含类B的实例,类B又包含类A的实例,形成直接的双向关联。这种设计往往源于过度细化的职责划分或对关联关系处理不当,导致类之间的边界模糊,互相渗透。
-
跨层依赖:
- 在分层架构(如MVC、三层架构等)中,各层之间应当遵循一定的依赖方向(如表现层依赖业务层,业务层依赖数据访问层)。但如果设计不当,可能会出现如控制器依赖服务,服务反过来依赖特定控制器的情况,形成循环。
-
事件监听/回调机制:
- 在实现事件驱动或回调功能时,发布者(publisher)和订阅者(subscriber)之间可能存在隐含的循环依赖。例如,发布者注册了订阅者的回调方法,而订阅者在初始化时又依赖于发布者的状态或接口。
-
模块间的循环依赖:
- 在大型项目中,不同模块或组件可能通过接口、服务或共享库互相依赖。如果没有合理规划模块边界和依赖关系,可能会导致模块间的循环依赖。
-
依赖注入配置不当:
- 使用Spring等DI框架时,通过
@Autowired
、@Inject
等注解进行依赖注入。如果在配置Bean间关系时,不慎设置了循环依赖的注入路径,如通过setter注入或字段注入导致Bean A依赖Bean B,Bean B又依赖回Bean A。
- 使用Spring等DI框架时,通过
-
事务管理器与事务代理:
- 在Spring事务管理中,事务代理Bean(如
TransactionProxyFactoryBean
)可能依赖于事务管理器Bean,而事务管理器Bean在某些配置下可能又依赖于事务代理Bean,形成特殊的循环依赖。
- 在Spring事务管理中,事务代理Bean(如
Spring如何解决循环依赖:
Spring解决循环依赖主要针对的是基于setter方法(或字段注入)的循环依赖情况。下面详细说明Spring如何通过三级缓存机制解决setter循环依赖:
Spring的三级缓存机制:
-
一级缓存(singletonObjects):存储已经完全初始化好的单例Bean,包括所有依赖注入完成、初始化方法调用完毕的Bean。
-
二级缓存(earlySingletonObjects):存储已经实例化但尚未完成属性填充(依赖注入)的Bean。这些Bean可以作为其他Bean依赖注入的早期引用。
-
三级缓存(singletonFactories):存储能生产半成品Bean(即仅实例化但未完成依赖注入)的
ObjectFactory
对象。当Bean正在创建且其依赖需要提前暴露时,Spring会将一个能生成该Bean的工厂对象放入此缓存。
解决循环依赖的步骤:
假设存在Bean A和Bean B之间的循环依赖:
-
Bean A开始初始化,Spring为其创建实例,并将其加入三级缓存中,标记为“正在创建”。
-
Bean A在填充属性时发现依赖Bean B,此时Bean B尚未创建,于是Spring尝试创建Bean B。
-
Bean B开始初始化,同样加入三级缓存,标记为“正在创建”。
-
Bean B在填充属性时发现依赖Bean A,此时虽然Bean A尚未完全初始化,但由于其已在三级缓存中(作为一个能生成半成品的工厂),Spring可以从三级缓存中取出
ObjectFactory
立即创建一个未完成属性注入的Bean A实例,并将其放入二级缓存。 -
Bean B完成了对半成品Bean A的引用注入,然后继续完成自身的初始化,并将完全初始化好的Bean B放入一级缓存。
-
Bean A恢复其初始化过程,由于所需依赖Bean B现在已经在一级缓存中可用,Spring可以从一级缓存获取Bean B完成对Bean A的属性注入。
-
Bean A完成初始化后移入一级缓存,整个循环依赖得以解决。
通过上述过程,Spring利用三级缓存实现了Bean实例的提前曝光,打破了循环依赖链。尽管Bean A在被注入到Bean B时还没有完成自己的初始化,但它已经是一个可以使用的对象,可以满足Bean B对其的依赖。随后,Bean A继续完成自身的初始化,确保最终两个Bean都处于完全初始化的状态。