从构造器注入到 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 是急救药,但最好的解法永远是重构。

相关推荐
泽虞3 小时前
《Qt应用开发》笔记p3
linux·开发语言·数据库·c++·笔记·qt·面试
鹏多多3 小时前
前端音频兼容解决:音频神器howler.js从基础到进阶完整使用指南
前端·javascript·音视频开发
前端架构师-老李3 小时前
12、electron专题(electron-builder)
前端·javascript·electron
IT_陈寒4 小时前
JavaScript性能飞跃:5个V8引擎优化技巧让你的代码提速300%
前端·人工智能·后端
艾小码4 小时前
这份超全JavaScript函数指南让你从小白变大神
前端·javascript
骑士雄师4 小时前
Java 泛型中级面试题及答案
java·开发语言·面试
Victor3564 小时前
Redis(61)Redis的连接数上限是多少?
后端
Victor3564 小时前
Redis(60) Redis的复制延迟如何优化?
后端
reembarkation4 小时前
vue 右键菜单的实现
前端·javascript·vue.js