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

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

循环依赖问题

循环依赖指两个或多个Bean相互依赖,形成闭环。例如:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class A {
@Autowired
private B b;
}

@Component
public class B {
@Autowired
private A a;
}

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 的生命周期。为了观感,我还是以图和图注解的方式来理清他的具体实现。

image-20250321132123001

关键方法 getSingleton()

image-20250321133342107

这里有一个疑问:为什么要两次访问一二级缓存。其实仔细观察一下,第二次访问的时候都是带锁的,很好推断是为了解决并发带来的问题(其实看到存放缓存的容器也是ConcurrentHashMap

第一次访问是无锁的,目的是减少同步开销,提高并发性能,如果能直接获取到实例,就返回,避免进入同步块。第二次访问是在同步锁内进行的,目的是确保线程安全,防止并发情况下重复创建 Bean。如果 Bean 还未创建,则从三级缓存 singletonFactories 获取早期引用,并存入二级缓存,删除三级缓存,这样下次可以直接使用早期引用,解决循环依赖问题。

思考一下:

为什么要设置三级缓存,而不是两级?如果是去掉中间那一层,也就是earlySingletonObjects,我们在使用bean的时候会反复调用ObjectFactory.getObject()会影响效率。

三级缓存存在的问题

构造器注入的循环依赖无法使用三级缓存解决循环依赖

其实看下来,三级缓存解决循环依赖很容易吧,但是有个缺点就是三级缓存无法解决构造器注入的循环依赖。实例化A时需要B,但B的创建同样需要A,无法提前暴露引用。

比如这样:

1
2
3
4
5
@Component
public class A {
private B b;
public A(B b) { this.b = b; }
}

Prototype作用域的Beans无法使用三级缓存解决循环依赖

第一,Spring 不缓存原型 Bean,它们的生命周期由调用方管理,每次 getBean() 都会创建新的实例,缓存它们没有意义。

第二,因为每次获取的实例都是新的,即使放入缓存,也无法保证其他 Bean 获取的是同一个实例。

第三,三级缓存的作用是减少实例化次数,而原型 Bean 本来就允许多次实例化,不需要缓存。

要解决在原型作用域的循环依赖,有三个办法:

  • 使用 ObjectProviderProvider 进行懒加载
  • 使用 @Lazy 让依赖延迟初始化
  • 接在代码中手动获取 ApplicationContext 来获取原型 Bean