原文来自于: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 方法把依赖"塞进去"。
整个流程大概是这样的:
- Spring 先用无参构造方法创建
BeanA
的实例(但里面还没注入 BeanB)。 - 再用无参构造方法创建
BeanB
的实例(同理,也没注入 BeanA)。 - 然后它回过头来,用 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 是急救药,但最好的解法永远是重构。