Spring通过三级缓存(singletonFactories、earlySingletonObjects、singletonObjects)支持单例Bean的setter注入循环依赖,先实例化后设值,允许提前暴露半成品对象;构造器注入因依赖必须前置而无法解决循环依赖,需配合@Lazy代理或重构设计。

Spring 能解决单例 Bean 的循环依赖,关键在于它不靠“等对象完全造好再注入”,而是允许在对象刚实例化、还没完成属性赋值时,就提前暴露出来供其他 Bean 使用。Setter 注入和代理对象正是配合这一机制落地的两种实用手段。
为什么 Setter 注入能绕过循环依赖
构造器注入要求所有依赖在创建对象前就必须就位,一旦 A 依赖 B、B 又依赖 A,就会卡死在“谁先造”的死结里。而 Setter 注入把依赖关系拆成两步:先调用无参构造器生成空壳对象,再通过 set 方法填充依赖。这给了 Spring 缓冲空间——A 实例化后暂存起来,即使还没设 B,也能先被 B 拿去用;B 完成后再回头给 A 补上 B 的引用。
- 必须是单例(prototype 不支持)
- 两个 Bean 都要用 setter 或 @Autowired 标注在字段/方法上,不能混用构造器注入
- Spring 内部靠三级缓存协作:A 刚 new 出来就放进三级缓存(ObjectFactory),B 查找 A 时从这里取,升级到二级缓存,最后 A 填完属性再进一级缓存
@Lazy 注解如何配合构造器注入破局
如果非要用构造器注入(比如强调不可变性),@Lazy 是最直接的补救方式。它不是延迟整个 Bean 的创建,而是延迟该依赖的解析时机——A 构造时并不立刻要一个真实的 B 实例,而是一个“将来才初始化的 B 代理”。等 A 真正调用 B 的方法时,B 才被创建,此时 A 已经在容器里了,B 就能顺利拿到 A。
- 只对单例有效,且仅作用于被标注的依赖参数
- 注意:@Lazy 加在字段或构造器参数上都行,但加在类上会影响整个 Bean 的加载时机
- 副作用:调用被 @Lazy 的 Bean 时会有首次初始化开销,不适合高频短生命周期场景
代理对象在循环依赖中的真实角色
很多人误以为“用了 AOP 就没法循环依赖”,其实恰恰相反:Spring 的三级缓存中,第三级 singletonFactories 存的不是对象本身,而是一个 ObjectFactory,它的作用就是按需生成代理对象。当 A 和 B 循环依赖,且其中某个需要事务或切面增强时,Spring 会在这个工厂里提前准备好代理逻辑。B 拿到的不是原始 A,而是 A 的代理(可能已织入事务拦截器),但这个代理背后仍指向同一个早期 A 实例,保证了引用一致性。
- 代理对象不是为解决循环依赖而生,但它是循环依赖能安全落地的前提
- 如果 B 依赖的是 A 的代理(比如 A 加了 @Transactional),那 A 在三级缓存里暴露的就必须是能生成该代理的工厂,而不是裸对象
- 手动使用 AopContext.currentProxy() 获取当前代理,在循环依赖场景下要格外小心线程与作用域
比技术更关键的设计提醒
Setter 注入和 @Lazy 是应急方案,不是设计常态。真正健康的代码结构,应该从源头减少耦合:
- 用接口隔离依赖:A 依赖 IAction 接口,B 实现它,A 就不再和 B 类名强绑定
- 引入中介者或事件机制:A 发布“任务开始”事件,B 订阅处理,双方不再持有对方引用
- 检查是否真需要双向引用:B 持有 A 的 ID 或名称就够了,不一定非要持 A 实例
- 把共用逻辑抽成独立 Service,让 A 和 B 都依赖它,打破闭环

















