Spring源码:解决循环依赖之三级缓存

Spring源码:解决循环依赖之三级缓存
JohnnySpring源码:解决循环依赖之三级缓存
循环依赖问题
循环依赖指两个或多个Bean相互依赖,形成闭环。例如:
1 |
|
Spring通过三级缓存解决单例Bean的属性注入循环依赖:
一级缓存(singletonObjects
):存放完全初始化好的Bean。
二级缓存(earlySingletonObjects
):存放早期暴露的Bean(已实例化但未初始化)。
三级缓存(singletonFactories
):存放Bean工厂(ObjectFactory
),用于生成早期Bean。
括号后面的其实就是在Spring具体实现三级缓存时用到的字段名称。
三级缓存的具体工作流程如下:
- 实例化A,将A的
ObjectFactory
放入三级缓存。 - A注入B时,发现B未创建,开始实例化B。
- B注入A时,从三级缓存获取A的
ObjectFactory
,生成早期对象A并放入二级缓存。 - B完成属性填充和初始化,放入一级缓存。
- A继续注入已初始化的B,完成自身初始化,移出二级缓存,放入一级缓存。
Spring 三级缓存的设计是为了解决单例Bean 的循环依赖问题,核心思路是 “实例化后,初始化前,提前暴露 Bean 引用”,让其他 Bean 能够访问它的早期引用,避免循环依赖。
源码中三级缓存的具体实现
在 Spring 框架中,三级缓存的实现位于 DefaultSingletonBeanRegistry
类中,通过三个 Map 结构管理 Bean 的生命周期。为了观感,我还是以图和图注解的方式来理清他的具体实现。
关键方法 getSingleton()
这里有一个疑问:为什么要两次访问一二级缓存。其实仔细观察一下,第二次访问的时候都是带锁的,很好推断是为了解决并发带来的问题(其实看到存放缓存的容器也是ConcurrentHashMap
)
第一次访问是无锁的,目的是减少同步开销,提高并发性能,如果能直接获取到实例,就返回,避免进入同步块。第二次访问是在同步锁内进行的,目的是确保线程安全,防止并发情况下重复创建 Bean。如果 Bean 还未创建,则从三级缓存 singletonFactories
获取早期引用,并存入二级缓存,删除三级缓存,这样下次可以直接使用早期引用,解决循环依赖问题。
思考一下:
为什么要设置三级缓存,而不是两级?如果是去掉中间那一层,也就是earlySingletonObjects
,我们在使用bean的时候会反复调用ObjectFactory.getObject()
会影响效率。
三级缓存存在的问题
构造器注入的循环依赖无法使用三级缓存解决循环依赖
其实看下来,三级缓存解决循环依赖很容易吧,但是有个缺点就是三级缓存无法解决构造器注入的循环依赖。实例化A时需要B,但B的创建同样需要A,无法提前暴露引用。
比如这样:
1 |
|
Prototype作用域的Beans无法使用三级缓存解决循环依赖
第一,Spring 不缓存原型 Bean,它们的生命周期由调用方管理,每次 getBean()
都会创建新的实例,缓存它们没有意义。
第二,因为每次获取的实例都是新的,即使放入缓存,也无法保证其他 Bean 获取的是同一个实例。
第三,三级缓存的作用是减少实例化次数,而原型 Bean 本来就允许多次实例化,不需要缓存。
要解决在原型作用域的循环依赖,有三个办法:
- 使用
ObjectProvider
或Provider
进行懒加载 - 使用
@Lazy
让依赖延迟初始化 - 接在代码中手动获取
ApplicationContext
来获取原型 Bean