“解决”≠“让 Spring 兜底”,而是从根本上消除循环依赖。
下面给出 4 个层次、12 种落地做法,按“先设计、后代码、再配置、最后兜底”的顺序排列,面试或实战都能直接拿来用。
1. 设计层(根治,推荐)
做法 | 场景 | 示例 |
---|
1. 职责拆分 / 领域建模 | 两个 Service 互相调 → 抽出第三个 Service | OrderService ↔ PaymentService → 抽出 PaymentProcessor |
2. 事件驱动 / 消息 | 解耦同步调用 | Spring ApplicationEvent 或 MQ |
3. 接口下沉 | 把共同依赖抽象到 api 模块 | A 依赖 B 的接口,B 依赖 A 的接口 |
2. 代码层(低成本重构)
做法 | 代码片段 | 说明 |
---|
4. Setter/Field 注入 | @Autowired lateinit var b: B | 让 Spring 能用三级缓存 |
5. @Lazy 延迟注入 | @Lazy @Autowired val b: B | 注入代理对象,真正用时才创建 |
6. 方法参数注入 | fun doIt(b: B) | 只在方法内部用,避免字段级循环 |
7. 抽工具类 | 把公共逻辑放到 @Component Util | 两个 Service 都依赖 Util,而非彼此 |
3. 配置层(不改业务代码)
做法 | 配置示例 | 适用场景 |
---|
8. ObjectProvider / Provider | ObjectProvider<B> bProvider | 运行时按需取,打破字段级循环 |
9. @Configuration 手动装配 | @Bean fun a(b: B) = A(b) | 构造器循环时显式控制顺序 |
10. 调整作用域 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) | 多例 Bean 循环,但会牺牲单例语义 |
4. 兜底方案(Spring 自动)
做法 | 说明 | 风险 |
---|
11. 保持默认三级缓存 | 什么都不改 | 仅对单例+Setter/Field 有效 |
12. 允许循环依赖(Spring Boot 2.6+) | spring.main.allow-circular-references=true | 官方不推荐,升级可能失效 |
面试 15 秒总结
真正解决循环依赖,优先做领域拆分或事件驱动;
代码层面用 @Lazy
/ ObjectProvider
/ Setter 注入即可 90% 场景无痛落地;
构造器循环或多例 Bean 只能重构,Spring 的三级缓存只是兜底,不是银弹。