Bean 的作用域有哪些?Spring 是如何解决循环依赖的?

结论先行:常用作用域有 singleton(默认,容器内唯一实例)、prototype(每次获取都新建)、request/session/application(Web 场景)。循环依赖指 A 依赖 B、B 又依赖 A;Spring 依靠“三级缓存”只能解决 singleton + 字段/setter 注入的循环依赖,prototype 与构造器注入无法解决,需要靠 @Lazy 等设计手段拆环。

作用域对比表

作用域实例个数销毁管理注意点
singleton容器内 1 个容器关闭时统一销毁默认;注意线程安全
prototype每次获取 1 个容器不负责销毁由调用方管理生命周期
request每次请求 1 个请求结束销毁需要 Web 环境
session每个会话 1 个会话结束销毁需要 Web 环境

三级缓存解决循环依赖

// DefaultSingletonBeanRegistry 三级缓存语义(简化示意)
Map<String, Object> singletonObjects;              // 一级:成品单例
Map<String, Object> earlySingletonObjects;         // 二级:早期半成品
Map<String, ObjectFactory<?>> singletonFactories;  // 三级:生产早期引用的工厂

流程:创建 A,实例化后放入三级缓存 → 填充属性时发现需要 B → 创建 B → B 填充属性时发现需要 A → 从三级缓存取出 A 的 ObjectFactory 得到“早期引用”注入给 B → B 完成并放入一级缓存 → A 继续装配完成。

为什么必须要有第三级

  • AOP 场景下,Bean 可能在“属性填充”阶段就需要被代理:三级缓存用 ObjectFactory 延迟决定“给原始对象还是给代理”;
  • 若省成两级,无法区分普通早期引用与需要提前代理的情况,可能导致代理重复创建或注入的不是同一个代理实例。

无法解决的情况与对策

  • 构造器注入的循环依赖:实例化阶段就要对方,此时还没有早期引用可用;
  • prototype 作用域的 Bean:不进缓存,无法提前暴露;
  • 对策:@Lazy 注入代理占位,或调整依赖方向、拆分 Bean 从结构上断环。

常见追问 / 记忆点

  • 追问:三级缓存能简化成两级吗?答:语义上会破坏 AOP 单例代理的“只代理一次”保证,Spring 保留三级是刻意的设计取舍。
  • 追问:Spring Boot 2.6+ 还允许循环依赖吗?答:默认改为启动即报错(spring.main.allow-circular-references=false),官方不建议依赖容器解环。
  • 记忆点:singleton + setter/字段注入可解,构造器/prototype 必炸;“能拆环就不靠容器”才是更优架构答案。
笔记加载中…