Spring如何解决循环依赖问题

举报
林欣 发表于 2025/04/27 10:42:05 2025/04/27
【摘要】 在 Spring 框架中,循环依赖(Circular Dependency) 是指两个或多个 Bean 互相依赖对方,导致 Spring 无法直接完成实例化和注入的场景。例如:Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A。Spring 通过三级缓存机制和提前暴露对象引用的策略高效解决了大部分循环依赖问题,但部分场景仍需开发者主动规避。以下是详细解析: 一、循环依赖...

在 Spring 框架中,循环依赖(Circular Dependency) 是指两个或多个 Bean 互相依赖对方,导致 Spring 无法直接完成实例化和注入的场景。例如:Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A。Spring 通过三级缓存机制提前暴露对象引用的策略高效解决了大部分循环依赖问题,但部分场景仍需开发者主动规避。以下是详细解析:


一、循环依赖的分类与Spring的解决能力

Spring 能解决的循环依赖仅限于 单例作用域(Singleton) 的 Bean,且依赖方式为 Setter 注入字段注入(需通过三级缓存)。以下情况无法解决:

  1. 构造器注入:若循环依赖通过构造器参数传递,Spring 会直接抛出 BeanCurrentlyInCreationException
  2. 原型作用域(Prototype):因每次请求都会创建新实例,无法提前暴露对象引用。
  3. AOP 代理对象:若 Bean 被 AOP 代理(如 @Transactional),代理对象的创建时机可能导致循环依赖失败。

二、Spring解决循环依赖的核心机制:三级缓存

Spring 通过三级缓存(实际是三个 Map)协同工作,按需提前暴露对象引用,确保依赖链能完整初始化。以下是详细流程:

1. 三级缓存的作用

缓存级别 数据结构 作用
一级缓存(singletonObjects) ConcurrentHashMap<String, Object> 存储完全初始化后的 Bean 实例(最终输出)。
二级缓存(earlySingletonObjects) ConcurrentHashMap<String, Object> 存储提前暴露的“半成品”Bean(仅对象引用,未完成依赖注入)。
三级缓存(singletonFactories) ConcurrentHashMap<String, ObjectFactory> 存储 ObjectFactory 回调函数,用于按需生成“半成品”Bean(支持 AOP 代理的延迟创建)。

2. 完整解决流程(以 Bean A → Bean B → Bean A 为例)

  1. 初始化 Bean A

    • Spring 创建 Bean A 的原始对象(new A()),但未设置属性。
    • 将 Bean A 的 ObjectFactory 放入三级缓存(singletonFactories)。
    • 暴露 Bean A 的“半成品”引用到二级缓存(earlySingletonObjects)。
  2. 依赖注入 Bean B

    • Spring 发现 Bean A 需要 Bean B,开始初始化 Bean B。
    • 创建 Bean B 的原始对象,同样将其 ObjectFactory 放入三级缓存。
  3. Bean B 依赖 Bean A

    • Spring 发现 Bean B 需要 Bean A,此时:
      • 先检查一级缓存(未完成)。
      • 再检查二级缓存(找到 Bean A 的“半成品”引用)。
      • 直接使用二级缓存中的 Bean A 引用,完成 Bean B 的初始化。
  4. 完成 Bean A 的初始化

    • Bean B 初始化完成后,Spring 继续完成 Bean A 的依赖注入(将 Bean B 注入 Bean A)。
    • 将 Bean A 的完整实例移至一级缓存,并清理二级/三级缓存中的临时数据。

三、代码示例与日志验证

以下是一个典型的循环依赖场景及 Spring 的解决日志:

1. 示例代码

@Component
public class BeanA {
    @Autowired
    private BeanB beanB;

    public void print() {
        System.out.println("BeanA initialized with BeanB: " + beanB);
    }
}

@Component
public class BeanB {
    @Autowired
    private BeanA beanA;

    public void print() {
        System.out.println("BeanB initialized with BeanA: " + beanA);
    }
}

2. 启动日志分析

启动应用时,Spring 的日志会显示类似以下流程:

Creating shared instance of singleton bean 'beanA'
Creating shared instance of singleton bean 'beanB'
Eagerly caching bean 'beanA' to allow for resolving potential circular references
Injecting dependency on bean 'beanB' into bean 'beanA'
Injecting dependency on bean 'beanA' into bean 'beanB' (using early reference)
Finished creating instance of bean 'beanB'
Finished creating instance of bean 'beanA'
  • 关键点:日志中 Eagerly cachingusing early reference 表明 Spring 通过三级缓存解决了循环依赖。

四、无法解决的循环依赖场景

1. 构造器注入的循环依赖

@Component
public class BeanA {
    private final BeanB beanB;

    @Autowired
    public BeanA(BeanB beanB) {  // 构造器注入
        this.beanB = beanB;
    }
}

@Component
public class BeanB {
    private final BeanA beanA;

    @Autowired
    public BeanB(BeanA beanA) {  // 构造器注入
        this.beanA = beanA;
    }
}

错误日志

Error creating bean with name 'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?

原因:构造器注入要求在对象创建时即完成所有依赖注入,而此时 Bean B 尚未实例化,导致死锁。

2. 原型作用域的循环依赖

@Scope("prototype")
@Component
public class BeanA {
    @Autowired
    private BeanB beanB;
}

@Scope("prototype")
@Component
public class BeanB {
    @Autowired
    private BeanA beanA;
}

错误日志

Error creating bean with name 'beanA': Scope 'prototype' is not active for the current thread

原因:原型作用域的 Bean 每次请求都会新建实例,无法提前暴露引用。


五、最佳实践与建议

  1. 优先使用 Setter 注入或字段注入
    避免构造器注入导致的循环依赖(除非业务必须)。

  2. 重构代码消除循环依赖

    • 将共享逻辑提取到第三个 Bean 中(如 ServiceC),由 Bean A 和 Bean B 共同依赖。
    • 使用事件驱动(如 ApplicationEvent)替代直接依赖。
  3. 避免过度依赖三级缓存
    三级缓存是 Spring 的内部实现,开发者应通过设计而非依赖框架特性解决问题。

  4. 警惕 AOP 代理的影响
    若 Bean 被 AOP 代理(如 @Transactional),代理对象的创建时机可能导致循环依赖失败。此时可:

    • 改用 @Lazy 延迟初始化。
    • 将其中一个 Bean 改为 @Lazy 注入。

六、总结

  • Spring的解决能力:通过三级缓存和“半成品”引用暴露,Spring 能高效解决单例作用域下 Setter/字段注入的循环依赖。
  • 无法解决的场景:构造器注入、原型作用域、AOP 代理冲突等需开发者主动规避。
  • 设计优先:循环依赖本质上是设计问题,建议通过重构代码(如引入中间层)彻底消除,而非依赖框架特性。

通过理解 Spring 的底层机制和遵循最佳实践,开发者既能利用框架的能力,又能避免潜在问题。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。