从构造器注入到 setter:Spring 循环依赖的常见场景解析

原文来自于:zha-ge.cn/java/114

从构造器注入到 setter:Spring 循环依赖的常见场景解析

某天早晨刚点开IDE,脑袋刚从枕头上拔出来,后端小李气呼呼冲我抱怨:"为啥我俩Bean老死活注入不进去?!"我心说,又是经典老梗,循环依赖,谁还没栽过跟头。于是,故事就这么拉开了帷幕......

循环依赖的魔幻开局

什么是循环依赖?比如说,BeanA需要BeanB,BeanB又眼巴巴要BeanA。这就有点像《西游记》里唐僧非要悟空陪,悟空却得师傅说了算,不管谁先出场都尴尬。

Spring容器会帮我们管理Bean,但碰到循环依赖,容器也会头大:"要不你俩等会儿,我佛系操作下?"所以,问题就在于怎么让这些彼此期待的Bean都能顺利出生。

咱们先梳理下最常见的套路:

  • 构造器注入:用@Autowired标在构造方法上,要求Bean构造时一次性搞定所有依赖。
  • Setter注入:Bean建好之后,再慢悠悠用set方法把依赖塞进去。

DAY1:勇闯构造器注入的死胡同

"构造器注入最优雅啊,可一碰循环依赖就瞬间狗带!"小李嚷嚷着。

我验证了下,果然:

java 复制代码
@Component
class BeanA {
    @Autowired
    public BeanA(BeanB b) { this.b = b; }
    // ...
}
@Component
class BeanB {
    @Autowired
    public BeanB(BeanA a) { this.a = a; }
    // ...
}

两个Bean你瞪我、我瞪你,Spring刚准备造BeanA,发现得预先造BeanB,可BeanB又赖着BeanA先出来。这死局,比99乘法表还死板。

DAY2:Setter注入拯救世界!

"那要不把构造器注入换成Setter试试?"------老司机都懂,Setter注入在Spring默认单例模式下,是可以解决大部分循环依赖的。

代码也很亲民:

java 复制代码
@Component
class BeanA {
    private BeanB b;

    @Autowired
    public void setBeanB(BeanB b) { this.b = b; }
    // ...
}
@Component
class BeanB {
    private BeanA a;

    @Autowired
    public void setBeanA(BeanA a) { this.a = a; }
    // ...
}

Setter 注入的"神来之笔"

一换成 Setter 注入,神奇的事情就发生了: Spring 不再要求两个 Bean 在构造时就要齐活儿,而是先造好空壳对象,再用反射调用 setter 方法把依赖"塞进去"。

整个流程大概是这样的:

  1. Spring 先用无参构造方法创建 BeanA 的实例(但里面还没注入 BeanB)。
  2. 再用无参构造方法创建 BeanB 的实例(同理,也没注入 BeanA)。
  3. 然后它回过头来,用 setter 把 BeanB 注入到 BeanA,再把 BeanA 注入到 BeanB

就这样,两个 Bean 都能顺利出生,互相引用,循环依赖的问题就这么"水到渠成"地解决了。

这也是为什么几乎所有 Spring 源码里,凡是有潜在循环依赖的地方,都会推荐使用 Setter 注入,而不是构造器注入的原因。


踩坑瞬间:还有解不了的循环依赖!

刚学会 Setter 注入那会儿,我还挺得意:"原来循环依赖也就这点事嘛!" 结果没两天,另一个场景又把我整破防了:原型(prototype)作用域 + 构造器注入

比如这样👇:

java 复制代码
@Component
@Scope("prototype")
class BeanA {
    @Autowired
    public BeanA(BeanB b) {}
}

@Component
@Scope("prototype")
class BeanB {
    @Autowired
    public BeanB(BeanA a) {}
}

这下就算你改成 Setter 注入也没用,Spring 会直接甩你一个异常:

BeanCurrentlyInCreationException: Error creating bean with name 'beanA'

为啥?因为原型 Bean 不会被 Spring 容器缓存起来,"三级缓存"策略在这儿压根派不上用场。它们每次创建都是"全新造一遍",所以永远等不到对方先出生------这就是原型 Bean 的死穴


其他几种解法(老司机经验)

后来我也陆续总结了几种"翻车场景"的解法,给你做个避坑清单:

  • 拆解依赖链:有时候循环依赖是设计出了问题,考虑引入第三个 Bean 来中转,解开环形依赖。
  • 使用 @Lazy 延迟注入 :在依赖处加上 @Lazy,让 Bean 的注入推迟到第一次真正使用时再发生,从而绕开初始化死循环。
java 复制代码
@Component
class BeanA {
    @Autowired
    public BeanA(@Lazy BeanB b) { this.b = b; }
}
  • 使用接口回调/事件机制:避免直接互相引用,用事件发布者和监听器的模式代替。
  • 重构为无状态 Bean:很多时候循环依赖根本没必要存在,如果你只是为了共享数据,可以用参数传递或上下文获取。

经验启示

这一路踩坑下来,我总结了几条血泪经验:

  • 构造器注入优雅,但一旦涉及循环依赖,容易死锁。
  • Setter 注入在单例 Bean 下能轻松解决大部分循环依赖,是首选方案。
  • 原型作用域、构造器注入、静态初始化块这些"高危组合"下,Spring 的三级缓存策略失效,得另想办法。
  • 代码设计才是根本解决之道:如果两个 Bean 非得互相依赖,可能是职责划分本身有问题。

最后一句

循环依赖就是 Spring 世界里的"死循环副本",新手第一次遇到时总是手忙脚乱,老手见得多了就知道,有时候不是 Spring 不行,而是设计得太绕。

一句话记住这篇文章的精华:

构造器注入容易出事,Setter 注入能救大多数场景,@Lazy 是急救药,但最好的解法永远是重构。

相关推荐
橙子家1 天前
Serilog 日志库简单实践(二):控制台与调试 Sinks(.net8)
后端
007php0071 天前
某游戏大厂 Java 面试题深度解析(四)
java·开发语言·python·面试·职场和发展·golang·php
Mr.Jessy1 天前
Web APIs学习第一天:获取 DOM 对象
开发语言·前端·javascript·学习·html
午安~婉1 天前
javaScript八股问题
开发语言·javascript·原型模式
想不明白的过度思考者1 天前
Rust——异步递归深度指南:从问题到解决方案
开发语言·后端·rust
西西学代码1 天前
Flutter---个人信息(5)---持久化存储
java·javascript·flutter
芝麻开门-新起点1 天前
flutter 生命周期管理:从 Widget 到 State 的完整解析
开发语言·javascript·ecmascript
ConardLi1 天前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
芒克芒克1 天前
ssm框架之Spring(上)
java·后端·spring
冴羽1 天前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化