Spring Boot 2.6.0+ 循环依赖问题及解决方案
目录
背景
Spring Boot 2.6.0+ 默认禁用循环依赖,若单例 Bean 间存在字段 /setter 注入循环(如 A 依赖 B,B 依赖 A),启动时会报 BeanCurrentlyInCreationException
。
解决方案
1. 配置文件开启循环依赖(侵入性最低,临时方案)
-
原理:关闭 Spring 2.6+ 的循环依赖检查,恢复旧版三级缓存处理逻辑。
-
适用场景:快速验证业务,临时解决问题(不推荐长期用于生产)。
-
操作:在application.yml或application.properties 中添加:
spring: main: allow-circular-references: true # 允许循环依赖
-
特点:无需修改代码,仅改配置即可生效。
2. @Lazy
延迟注入(侵入性低,推荐优先尝试)
-
原理:对循环依赖的一方标记
@Lazy
,Spring 会生成代理对象延迟初始化,避免启动时循环阻塞。 -
适用场景:单例 Bean 间的字段 /setter 注入循环依赖(匹配字段注入场景)。
-
示例代码
// A 中对 B 加 @Lazy(仅需一方添加) @Service public class A { @Lazy // 关键:延迟 B 的初始化 @Autowired private B b; } @Service public class B { @Autowired // B 依赖 A 无需加 @Lazy private A a; }
-
注意:避免在
@PostConstruct
等初始化方法中提前调用依赖,否则会触发代理对象提前初始化。
3. 手动从容器获取(ApplicationContextAware
,侵入性中等)
-
原理:让一方实现
ApplicationContextAware
,手动从容器获取依赖,避免直接注入。 -
适用场景:循环依赖关系简单,不希望修改注入方式。
-
示例代码
@Service public class A implements ApplicationContextAware { private ApplicationContext context; private B b; // 不直接注入,手动获取 @Override public void setApplicationContext(ApplicationContext context) { this.context = context; } // 用到 B 时再获取 public void doA() { if (b == null) { b = context.getBean(B.class); // 手动获取 B } b.doB(); } } @Service public class B { @Autowired // B 正常注入 A private A a; public void doB() { /* ... */ } }
4. 接口隔离 / 中间层解耦(侵入性高,推荐长期方案)
-
原理:抽离循环双方的共同逻辑到中间组件,打破直接依赖(A→C,B→C)。
-
适用场景:重构代码,从根源消除循环依赖(生产环境推荐)。
-
示例代码
// 1. 中间层 C 封装共同逻辑 @Service public class C { public void commonLogic() { /* 原本 A 和 B 互相调用的逻辑 */ } } // 2. A 依赖 C,不再依赖 B @Service public class A { @Autowired private C c; public void doA() { c.commonLogic(); } // 调用 C 而非 B } // 3. B 依赖 C,不再依赖 A @Service public class B { @Autowired private C c; public void doB() { c.commonLogic(); } // 调用 C 而非 A }
5. 事件驱动(ApplicationEvent
,侵入性中等,适合通知场景)
-
原理:通过 Spring 事件机制解耦,A 和 B 不直接依赖,而是通过发布 / 监听事件交互。
-
适用场景:循环依赖是 “通知 / 回调” 场景(如 A 完成后通知 B,反之亦然)。
-
示例代码
// 1. 定义事件 public class AEvent extends ApplicationEvent { public AEvent(Object source) { super(source); } } public class BEvent extends ApplicationEvent { public BEvent(Object source) { super(source); } } // 2. A 发布事件并监听 B 事件 @Service public class A implements ApplicationListener<BEvent> { @Autowired private ApplicationEventPublisher publisher; public void doA() { publisher.publishEvent(new AEvent(this)); // 发布 A 完成事件 } @Override public void onApplicationEvent(BEvent event) { /* 处理 B 事件 */ } } // 3. B 发布事件并监听 A 事件 @Service public class B implements ApplicationListener<AEvent> { @Autowired private ApplicationEventPublisher publisher; public void doB() { publisher.publishEvent(new BEvent(this)); // 发布 B 完成事件 } @Override public void onApplicationEvent(AEvent event) { /* 处理 A 事件 */ } }
总结
- 临时解决:优先用 配置开启循环依赖 或
@Lazy
延迟注入。 - 长期优化:推荐 接口隔离 或 事件驱动,从设计上消除循环依赖。
版本差异补充
- Spring Boot 2.6.0 之前:
框架默认允许循环依赖,通过 三级缓存 自动处理单例 Bean 的字段 /setter 注入循环(构造器注入仍报错)。核心是通过缓存提前暴露未完全初始化的 Bean,打破初始化循环。 - Spring Boot 2.6.0 之后:
默认禁用循环依赖,需通过上述方案显式处理(推荐从设计上消除循环依赖)