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 之后
    默认禁用循环依赖,需通过上述方案显式处理(推荐从设计上消除循环依赖)
posted @ 2025-08-04 22:54  进击的小蔡鸟  阅读(363)  评论(0)    收藏  举报