java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot循环依赖

Spring Boot循环依赖深度解析与解决方案(附详细代码)

作者:weixin_72753562

循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,下面这篇文章主要介绍了Spring Boot循环依赖深度解析与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

1. 什么是循环依赖

循环依赖是指两个或多个 Bean 之间相互依赖,形成闭环。

代码示例:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

当 Spring 容器创建 ServiceA 时,发现需要 ServiceB;转而去创建 ServiceB,又发现需要 ServiceA。这就形成了闭环。

如果不处理,理论上会陷入无限循环。 Spring 通过三级缓存机制解决了大部分单例 Bean 的循环依赖,但并非万能。

2. Spring 的底层救星:三级缓存机制

理解 Spring 如何解决循环依赖,是定位问题的关键。

2.1 核心缓存

DefaultSingletonBeanRegistry 类中定义了三个 Map:

缓存级别Map 名称存放内容
一级缓存singletonObjects完全初始化好的 Bean
二级缓存earlySingletonObjects提前暴露的 Bean 实例(未完成属性填充和初始化)
三级缓存singletonFactories生成 Bean 的工厂(可生成代理对象)

2.2 解决流程(以 ServiceA 与 ServiceB 为例)

  1. 创建 A:实例化 A(此时 A 未赋值,属性为空) → 将能获取 A(或 A 的代理)的工厂存入三级缓存。
  2. 填充 A:发现依赖 B → 去容器找 B。
  3. 创建 B:实例化 B → 将 B 的工厂存入三级缓存。
  4. 填充 B:发现依赖 A → 去容器找 A → 命中三级缓存 → 通过工厂获取 A 的早期引用 → 将 A 的早期引用升级存入二级缓存。
  5. 完成 B:B 拿到 A 的引用(此时 A 尚未初始化完毕),B 完成初始化 → 存入一级缓存。
  6. 回溯 A:A 拿到 B 的完整引用,完成初始化 → 存入一级缓存。

2.3 为何需要三级缓存?二级不行吗?

关键在于 AOP(面向切面编程)。
如果只有二级缓存,在步骤 2 中我们只能存入原始 A 对象。后续若 A 需要被动态代理,B 中注入的将是原始对象,而非代理对象,这会造成 AOP 失效。三级缓存的 ObjectFactory 能确保在暴露早期引用时,先判断是否需要创建代理对象,从而保证注入的是正确版本。

3. 循环依赖的“雷区”:什么情况下会失败

虽然三级缓存很强大,但以下几种情况它会失效,导致著名的 BeanCurrentlyInCreationException

3.1 构造器注入(Constructor Injection)

失败原因: 构造器注入要求实例化时就必须传入依赖。Bean 无法先“半成品实例化”,也就没机会存入三级缓存。

错误示例:

@Service
public class ServiceA {
    private final ServiceB serviceB;
    public ServiceA(ServiceB serviceB) { // 创建A必须先有B
        this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;
    public ServiceB(ServiceA serviceA) { // 创建B必须先有A
        this.serviceA = serviceA;
    }
}
// 启动直接报错

3.2 原型(Prototype)作用域 Bean

失败原因: Spring 不缓存原型 Bean。每次请求都创建新实例,无法利用缓存来“破环”。

错误示例:

@Scope("prototype")
@Component
public class PrototypeA {
    @Autowired
    private PrototypeB prototypeB;
}

3.3 异步代理与@Lazy的复杂组合

当循环依赖链中存在 @Async@Transactional 代理,且配置不当时,三级缓存的早期暴露可能因代理对象类型不一致而失败。

4. 解决方案详解(从优到劣)

方案一:重新设计架构(最推荐)

这是治本之策。循环依赖往往是职责划分不清晰的信号。

方案二:使用 Setter/Field 注入(简单妥协)

Spring 官方推荐构造器注入,但 Setter 注入恰好可以配合三级缓存。

只需将关键依赖改为 @Autowired 字段注入或 setter 注入即可。

方案三:@Lazy懒加载(巧妙快捷)

在其中一个依赖点使用 @Lazy,使 Spring 先注入一个代理对象,直到第一次调用代理方法时才真正去容器获取目标 Bean。

示例:

@Component
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(@Lazy ServiceB serviceB) { // 构造器注入 + @Lazy
        this.serviceB = serviceB;
    }
    // ... 使用时 serviceB.doSomething() 才会触发真实调用
}

优点: 保持构造器注入的不变性,代码改动最小。
注意: 滥用 @Lazy 可能掩盖真正的设计问题,并增加初次调用开销。

方案四:通过编程式推迟解决

使用 ObjectProviderApplicationContext 手动获取 Bean。

@Component
public class ServiceA {
    @Autowired
    private ObjectProvider<ServiceB> serviceBProvider;

    public void doSomething() {
        ServiceB b = serviceBProvider.getIfAvailable();
    }
}

方案五:@PostConstruct设置回调

在 A 中依赖 B,在 B 中使用 @PostConstruct 调用 A 的方法。这要求其中一个方向的依赖不是构造器级别的。

// 允许 A 注入 B
@Component
public class B {
    @Autowired
    private A a;
    
    @PostConstruct
    public void init() {
        a.setB(this);
    }
}

警告: 这种方式非常脆弱,破坏了不可变性,是最后的选择。

5. Spring Boot 调试与定位技巧

5.1 开启循环依赖报告

application.properties 中开启 Debug 日志:

logging.level.org.springframework.beans.factory.support.DefaultListableBeanFactory=DEBUG

启动时会在控制台看到依赖处理的详细轨迹。

5.2 使用 Actuator 分析

引入 spring-boot-starter-actuator,访问 /actuator/beans 端点,仔细查看 Bean 的 dependencies 信息。

5.3 IDE 静态检查

IntelliJ IDEA 的 Spring Bean Dependencies 图可直接可视化检测循环(Endpoints 分析)。

6. 最佳实践总结

  1. 强制使用构造器注入:即使它不支持循环依赖。这能强迫开发者规范代码设计。
  2. 从根源避免:一旦发现循环依赖,首先反思模块职责划分是否合理。
  3. @Lazy 作为工具箱:对于无法立刻重构的遗留系统,@Lazy 是有效、低风险的止痛药。
  4. 警惕混合代理:当使用 @Async 等注解时,要特别留意循环依赖的影响。

通过本学习文件,建议在实际开发中先尝试理解和重现三级缓存机制,再针对性地选择解耦方案。

到此这篇关于Spring Boot循环依赖深度解析与解决方案的文章就介绍到这了,更多相关SpringBoot循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文