不加 @Lazy 一定会发生循环依赖,启动失败吗?
不加 @Lazy 一定会发生循环依赖,启动失败吗?
绝对不是。
不加@Lazy注解,并不代表一定会发生循环依赖或启动失败。@Lazy只是解决特定问题的“药”,而不是所有项目的“饭”。
为了让你彻底放心,我们需要把**“循环依赖”和“启动失败”**这两个概念拆开来看。
🚫 情况一:根本没有循环依赖(最常见)
如果你的代码设计是清晰的树状或链状结构,比如:
- Controller -> Service -> Dao
- A -> B -> C
在这种情况下,完全不需要@Lazy。Spring 会按照顺序依次创建 C,然后 B,最后 A。一切运行完美,没有任何报错。
✅ 情况二:有循环依赖,但 Spring 能自动搞定(单例 + 字段注入)
这是 Spring 最强大的地方。如果你有以下场景:
- A 和 B 互相依赖(
A注入B,B注入A) - 都是单例(
@Scope("singleton"),默认就是) - 使用字段注入(
@Autowired在属性上) 或 Setter 注入
结论:不加@Lazy也能正常启动!
原理:
Spring 的三级缓存机制非常强大。它允许先创建一个“半成品”的 A(还没填属性),放入三级缓存;然后去创建 B,B 需要 A 时,Spring 从三级缓存里把那个“半成品 A”拿出来给 B 用。等 B 创建好了,再回头把完整的 B 塞给 A。
注意:这种情况下,虽然不加
@Lazy能跑通,但如果你的项目升级到了 Spring Boot 2.6+,默认配置可能会禁止这种循环引用,这时你就需要在配置文件里开启spring.main.allow-circular-references=true,或者还是得加@Lazy。
❌ 情况三:不加 @Lazy 必死无疑(必须加的场景)
只有在以下几种“硬骨头”场景下,不加@Lazy才会导致启动报错:
1. 构造器注入导致的循环依赖
@ServicepublicclassA{privatefinalBb;// 构造器注入:A 创建时必须要有 BpublicA(Bb){this.b=b;}}@ServicepublicclassB{privatefinalAa;// 构造器注入:B 创建时必须要有 ApublicB(Aa){this.a=a;}}结果:这是一个死锁。A 等 B 出生,B 等 A 出生。Spring 的三级缓存也救不了(因为对象还没实例化,没法放入缓存)。
解法:必须在其中一个构造参数上加@Lazy。
2. 多例(Prototype)作用域的循环依赖
@Service@Scope("prototype")// 多例publicclassA{@AutowiredBb;}@Service@Scope("prototype")// 多例publicclassB{@AutowiredAa;}结果:Spring 的三级缓存只支持单例 Bean 的循环依赖。对于多例,Spring 每次都要创建新对象,无法缓存“半成品”,所以直接报错。
解法:加@Lazy。
3. 自己注入自己(自调用)
@ServicepublicclassUserService{@AutowiredprivateUserServiceuserService;// 自己注入自己}结果:这本质上也是一种循环依赖。如果不加@Lazy,Spring 会陷入无限递归创建中。
解法:必须加@Lazy。
📊 总结对照表
| 场景 | 是否构成循环依赖? | 不加@Lazy会怎样? | 备注 |
|---|---|---|---|
| 普通单向依赖(A->B) | 否 | ✅正常启动 | 绝大多数项目的常态 |
| 单例 + 字段注入(A<->B) | 是 | ✅通常能启动 | Spring 三级缓存自动解决(但在 Boot 2.6+ 需配置允许) |
| 构造器注入(A<->B) | 是 | ❌启动报错 | 必须加@Lazy破局 |
| 多例 Bean(A<->B) | 是 | ❌启动报错 | 必须加@Lazy破局 |
| 自注入(A->A) | 是 | ❌启动报错 | 必须加@Lazy破局 |
所以,只有当你确实遇到了上述几种“死锁”情况,或者你想优化启动性能(延迟加载重量级 Bean)时,才需要考虑@Lazy。
