Spring如何解决循环依赖
2025-08-31 14:50:41 0 举报
AI智能生成
在Spring框架中,循环依赖指的是两个或多个Bean相互依赖,形成闭环。为了解决这种问题,Spring主要通过三级缓存来处理: 1. 一级缓存(singletonObjects):存放完全初始化好的Bean,即该Bean已经实例化并注入了依赖的Bean。 2. 二级缓存(earlySingletonObjects):存放早期的Bean实例,即还未完全实例化的Bean,此时bean的实例已创建,但属性尚未填充。 3. 三级缓存(singletonFactories):存放Bean工厂对象,通过这个工厂可以获取到早期的Bean实例。 当容器创建一个Bean时,会优先使用一级缓存,如果在一级缓存中找不到,就会尝试通过三级缓存创建。如果在创建过程中发现依赖循环,会通过三级缓存中的工厂提前暴露Bean的引用(即代理或者AOP对象),这样即便Bean还未完全初始化完成,也能让其他Bean引用它。当其他的Bean创建并注入这个已提前暴露的Bean引用时,此时的Bean会继续完成其剩下的创建过程。这个过程是确保循环依赖可以正确处理的关键。 修饰语可以是:Spring采用的三级缓存策略,以及这种策略背后的有效性和稳定性。 注意:这种机制对于单例作用域(singleton scope)的对象有效。而对于原型作用域(prototype scope)的对象,Spring并不解决循环依赖问题,因此它要求在设计时避免原型作用域内的对象产生循环依赖。
作者其他创作
大纲/内容
什么是循环依赖
循环依赖是指两个或多个Bean相互依赖,形成了一个闭环,导致无法完成依赖注入。
简单示例:
代码表示
Spring如何解决循环依赖?
三级缓存
一级缓存 singletonObjects:
存放完全初始化好的、完整的单例Bean。
从这里取出的Bean可以直接使用。
从这里取出的Bean可以直接使用。
二级缓存 earlySingletonObjects:
存放早期的、不完整的Bean实例(对象已创建,但属性还未填充)。
用于解决循环依赖,暴露一个“半成品”Bean供其他Bean引用。
用于解决循环依赖,暴露一个“半成品”Bean供其他Bean引用。
三级缓存 singletonFactories:
存放Bean的对象工厂(ObjectFactory)。
这个工厂可以返回一个早期的Bean引用(可能经过AOP代理)。
这个工厂可以返回一个早期的Bean引用(可能经过AOP代理)。
解决流程(以Setter注入为例)
1.开始创建 ServiceA:
Spring调用ServiceA的构造器,创建一个“原始对象”(半成品A)。
Spring将“半成品A”包装成一个ObjectFactory,并放入三级缓存,同时从二级缓存中移除(如果有的话)。
Spring开始为ServiceA注入属性,发现它依赖ServiceB。
Spring将“半成品A”包装成一个ObjectFactory,并放入三级缓存,同时从二级缓存中移除(如果有的话)。
Spring开始为ServiceA注入属性,发现它依赖ServiceB。
2.转而创建 ServiceB:
Spring调用ServiceB的构造器,创建一个“原始对象”(半成品B)。
同样,将“半成品B”的工厂放入三级缓存。
Spring开始为ServiceB注入属性,发现它依赖ServiceA。
同样,将“半成品B”的工厂放入三级缓存。
Spring开始为ServiceB注入属性,发现它依赖ServiceA。
3.解决依赖:获取早期的 ServiceA:
Spring从三级缓存中查找ServiceA。
三级缓存中的ObjectFactory会返回ServiceA的早期引用(如果ServiceA需要被AOP代理,这里会返回代理对象;否则返回原始对象)。
将这个早期引用(对象A‘)放入二级缓存,并从三级缓存中移除。
将这个对象A’注入给ServiceB。
ServiceB的属性注入完成,成为一个完整的Bean,被放入一级缓存。同时清理二、三级缓存。
三级缓存中的ObjectFactory会返回ServiceA的早期引用(如果ServiceA需要被AOP代理,这里会返回代理对象;否则返回原始对象)。
将这个早期引用(对象A‘)放入二级缓存,并从三级缓存中移除。
将这个对象A’注入给ServiceB。
ServiceB的属性注入完成,成为一个完整的Bean,被放入一级缓存。同时清理二、三级缓存。
4.回归创建 ServiceA:
此时ServiceB已经创建完毕,Spring可以顺利地将完整的ServiceB注入给ServiceA。
ServiceA也完成属性注入,成为一个完整的Bean。
将完整的ServiceA放入一级缓存,并清理二、三级缓存。
ServiceA也完成属性注入,成为一个完整的Bean。
将完整的ServiceA放入一级缓存,并清理二、三级缓存。
流程图简化版
什么情况下循环依赖无法解决?
Spring并非能解决所有类型的循环依赖,构造器注入(Constructor Injection) 的循环依赖就无法解决。
为什么?
因为构造器注入发生在Bean实例化的那一刻。要创建A,必须先实例化A,而实例化A又需要已经实例化的B。要创建B,又需要已经实例化的A。这就形成了一个无法打破的死锁,对象都无法实例化,更谈不上将半成品放入缓存了。
因为构造器注入发生在Bean实例化的那一刻。要创建A,必须先实例化A,而实例化A又需要已经实例化的B。要创建B,又需要已经实例化的A。这就形成了一个无法打破的死锁,对象都无法实例化,更谈不上将半成品放入缓存了。
代码示例
其他无法解决的情况:
@Async注解的方法:因为@Async的代理对象是在Bean初始化之后通过BeanPostProcessor创建的,无法在早期暴露。
@Transactional注解的代理:虽然通常能解决,但如果循环依赖涉及多个复杂的BeanPostProcessor,也可能出现问题。
多例(Prototype)作用域的Bean:Spring不缓存prototype类型的Bean,因此无法暴露其早期引用。
如何避免循环依赖?
1.代码重构:
提取公共逻辑:将相互依赖的部分提取到一个新的第三方的Bean中。
使用接口与回调:通过接口进行解耦,使用事件或回调机制而非直接依赖。
重新审视设计:检查两个Bean是否职责过于集中,考虑是否应该拆分。
使用接口与回调:通过接口进行解耦,使用事件或回调机制而非直接依赖。
重新审视设计:检查两个Bean是否职责过于集中,考虑是否应该拆分。
2.使用Setter/Field注入替代强制性的构造器注入
这不是鼓励循环依赖,而是在确实需要时,为Spring提供解决的窗口。构造器注入被认为是更“好”的方式(明确依赖、不可变、易于测试),但Set注入在应对某些复杂场景时更灵活。
3.使用@Lazy注解
在其中一个依赖上添加@Lazy注解。这告诉Spring延迟初始化该Bean,先注入一个代理对象,等到真正需要使用时再创建真实对象,从而打破循环。
总结
0 条评论
下一页