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

相关推荐
TechTrek10 分钟前
Spring Boot 4.0正式发布了
java·spring boot·后端·spring boot 4.0
WYiQIU29 分钟前
面了一次字节前端岗,我才知道何为“造火箭”的极致!
前端·javascript·vue.js·react.js·面试
飞梦工作室29 分钟前
企业级 Spring Boot 邮件系统开发指南:从基础到高可用架构设计
java·spring boot·后端
qq_3168377530 分钟前
uniapp 观察列表每个元素的曝光时间
前端·javascript·uni-app
haiyu柠檬32 分钟前
在Spring Boot中实现Azure的SSO+VUE3前端配置
java·spring boot·后端
小夏同学呀32 分钟前
在 Vue 2 中实现 “点击下载条码 → 打开新窗口预览 → 自动唤起浏览器打印” 的功能
前端·javascript·vue.js
芳草萋萋鹦鹉洲哦32 分钟前
【vue】导航栏变动后刷新router的几种方法
前端·javascript·vue.js
zero13_小葵司37 分钟前
JavaScript性能优化系列(八)弱网环境体验优化 - 8.3 数据预加载与缓存:提前缓存关键数据
javascript·缓存·性能优化
q***721941 分钟前
springBoot 和springCloud 版本对应关系
spring boot·后端·spring cloud
努力学算法的蒟蒻44 分钟前
day20(11.21)——leetcode面试经典150
面试