Spring循环依赖:三级缓存解析

什么是循环依赖?

循环依赖指的是两个或多个 Bean 之间相互依赖的情况。例如:

复制代码
// A依赖B
@Component
public class A {
    @Autowired
    private B b;
}

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

在这个例子中,A 的创建需要 B,而 B 的创建又需要 A,形成了一个闭环。如果没有特殊处理,这将导致无限循环或初始化失败。

循环依赖的类型

Spring 中存在三种主要的循环依赖场景:

  1. 构造器注入循环依赖:通过构造方法相互依赖
  2. setter 注入循环依赖:通过 setter 方法相互依赖
  3. 字段注入循环依赖:通过字段直接注入相互依赖

其中,只有构造器注入的循环依赖是 Spring 无法解决的,而后两种可以通过 Spring 的缓存机制完美解决。

Spring 解决循环依赖的核心:三级缓存

Spring 通过三级缓存机制解决了单例 Bean 的循环依赖问题,这三个缓存实际上是三个 Map:

  1. 一级缓存(singletonObjects):存储完全初始化完成的单例 Bean
  2. 二级缓存(earlySingletonObjects):存储提前暴露的、尚未完全初始化的单例 Bean 实例
  3. 三级缓存(singletonFactories):存储 Bean 的工厂对象(ObjectFactory),用于创建 Bean 的早期引用

这三级缓存的定义可以在DefaultSingletonBeanRegistry类中找到:

复制代码
// 一级缓存:存放完全初始化好的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:存放早期暴露的Bean实例(未完全初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存:存放Bean工厂,用于生成早期Bean引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

解决循环依赖的详细流程

让我们以 A 依赖 B,B 依赖 A 的场景为例,详细解析 Spring 如何处理循环依赖:

  1. 初始化 A

    • 检查三级缓存,发现 A 不存在
    • 实例化 A(调用构造方法,此时 A 的属性尚未设置)
    • 将 A 的工厂对象添加到三级缓存:singletonFactories.put("a", () -> getEarlyBeanReference("a", mbd, a))
    • 开始为 A 填充属性,发现依赖 B
  2. 初始化 B

    • 检查三级缓存,发现 B 不存在
    • 实例化 B
    • 将 B 的工厂对象添加到三级缓存
    • 开始为 B 填充属性,发现依赖 A
  3. 处理 B 对 A 的依赖

    • 检查一级缓存,A 不存在(未完全初始化)
    • 检查二级缓存,A 不存在
    • 检查三级缓存,找到 A 的工厂对象
    • 通过工厂对象获取 A 的早期引用
    • 将 A 的早期引用从三级缓存移到二级缓存
    • 将 A 的早期引用注入到 B 中
    • B 完成初始化,放入一级缓存
  4. 完成 A 的初始化

    • 将 B 从一级缓存中取出,注入到 A 中
    • A 完成初始化,从二级缓存移到一级缓存

通过这个流程,Spring 成功打破了 "A 依赖 B,B 依赖 A" 的循环,两个 Bean 都能被正确初始化。

三级缓存的精妙之处

为什么需要三级缓存而不是两级?这与 Spring 的 AOP 特性密切相关。

三级缓存中存储的是ObjectFactory,它的作用是在必要时生成 Bean 的代理对象。当一个 Bean 需要被 AOP 增强时,这个工厂会返回代理对象而不是原始对象。

如果没有三级缓存,直接在二级缓存中存储原始对象,那么当需要代理时,注入的就会是原始对象而不是代理对象,这显然不符合 AOP 的预期。

通过三级缓存,Spring 实现了延迟创建代理对象的功能,只有在发生循环依赖时才会提前创建代理,否则会在 Bean 初始化完成后再创建代理,这符合 Spring 的设计理念。

无法解决的循环依赖情况

Spring 的循环依赖解决方案并非万能,以下情况无法解决:

  1. 构造器注入的循环依赖

    @Component
    public class A {
    private B b;

    复制代码
     @Autowired
     public A(B b) {
         this.b = b;
     }

    }

    @Component
    public class B {
    private A a;

    复制代码
     @Autowired
     public B(A a) {
         this.a = a;
     }

    }

这种情况下,Spring 会抛出BeanCurrentlyInCreationException异常,因为构造器注入需要在实例化阶段就获取依赖,而此时依赖对象还未创建。

原型 Bean 的循环依赖

原型 Bean(@Scope("prototype"))每次请求都会创建新实例,Spring 不会缓存原型 Bean,因此无法解决其循环依赖。

非单例 Bean 的循环依赖

除了单例 Bean 外,其他作用域的 Bean(如 request、session)的循环依赖也无法被 Spring 解决。

如何避免循环依赖?

虽然 Spring 能解决单例 Bean 的循环依赖,但在设计上应尽量避免循环依赖,这通常意味着代码结构可以优化:

  1. 重构代码:将相互依赖的部分提取到第三个类中
  2. 使用 @Lazy 注解:延迟初始化其中一个 Bean
  3. 使用 setter 注入代替构造器注入:构造器注入更适合强制依赖,但可能导致循环依赖
相关推荐
mjy_1111 分钟前
Linux下的软件编程——文件IO
java·linux·运维
用户6757049885026 分钟前
3分钟,手摸手教你用OpenResty搭建高性能隧道代理(附完整配置!)
后端
进阶的小名9 分钟前
@RequestMapping接收文件格式的形参(方法参数)
java·spring boot·postman
coding随想18 分钟前
网络世界的“快递站”:深入浅出OSI七层模型
后端·网络协议
skeletron201132 分钟前
🚀AI评测这么玩(2)——使用开源评测引擎eval-engine实现问答相似度评估
前端·后端
shark_chili43 分钟前
颠覆认知!这才是synchronized最硬核的打开方式
后端
就是帅我不改1 小时前
99%的Java程序员都写错了!高并发下你的Service层正在拖垮整个系统!
后端·架构
Apifox1 小时前
API 文档中有多种参数结构怎么办?Apifox 里用 oneOf/anyOf/allOf 这样写
前端·后端·测试
似水流年流不尽思念1 小时前
如何实现一个线程安全的单例模式?
后端·面试
楽码1 小时前
了解HMAC及实现步骤
后端·算法·微服务