Spring 依赖注入与循环依赖总结

 更新时间:2025年08月21日 11:02:37   作者:xz在努力  
这篇文章给大家介绍Spring 依赖注入与循环依赖总结篇,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

1. Spring 三级缓存解决循环依赖

适用场景:字段注入(@Autowired 直接加在字段上)或 Setter 注入(@Autowired 加在 Setter 方法上)。

解决流程

  1. 一级缓存(singletonObjects):存放完全初始化好的单例 Bean。
  2. 二级缓存(earlySingletonObjects):存放提前暴露的半成品 Bean(已实例化但未初始化)。
  3. 三级缓存(singletonFactories):存放生成 Bean 的 ObjectFactory(用于延迟创建代理对象)。 🔹 关键点:Spring 先创建 Bean 的实例(调用无参构造器),再通过缓存逐步解决依赖,避免死锁。

举例:

个人理解就是,以UserService为例,先创建UserService单例实例放在一级缓存中,如果检测到如@Transition等注解,将单例实例包装成工厂存到三级缓存中,到UserService初始化时发现自注入,分别从一二三级缓存中查找(告诉我查找什么),发现三级缓存中包含UserService 的工厂,然后调用改工厂生成代理对象,并且该代理对象中有对UserService为原始对象的引用,而不是对代理对象的引用,然后生成代理对象存到二级缓存中,并注入到原始对象自注入的UserService中(此时原始对象不是有对代理对象的引用吗?),然后继续完成初始化如@PostConstruct,然后将代理对象放到一级缓存中(此时原始对象还在吗?),最后清楚二级三级缓存中的UserService代理对象和工厂。

你的理解基本正确,但有些细节需要澄清和补充。让我们以 UserService 为例,梳理 Spring 解决循环依赖(自注入)的完整流程,并明确每一步缓存的状态和对象引用关系。

1. 创建UserService原始对象

  • 动作:Spring 调用 UserService 的构造函数,创建一个原始对象(非代理)。
  • 缓存状态
    • 一级缓存(单例池):无 UserService
    • 二级缓存(早期暴露对象):无 UserService
    • 三级缓存(对象工厂):无 UserService

2. 将原始对象包装成工厂,存入三级缓存

  • 动作
    • 检测到 UserService 需要代理(如被 @Transactional 标记),Spring 将原始对象包装成一个 ObjectFactory 工厂,存入三级缓存。
    • 此时对象尚未初始化(未填充属性、未执行 @PostConstruct)。
  • 缓存状态
    • 一级缓存:无 UserService
    • 二级缓存:无 UserService
    • 三级缓存UserService 的 ObjectFactory

3. 处理UserService的自注入

  • 动作
    • Spring 开始初始化 UserService,发现它依赖自身(如通过 @Autowired 自注入)。
    • 依次从一级、二级、三级缓存查找:
      1. 一级缓存:未找到(因为 UserService 未完全初始化)。
      2. 二级缓存:未找到。
      3. 三级缓存:找到 UserService 的 ObjectFactory,调用它生成对象。
  • 生成代理对象
    • 工厂调用会创建 UserService 的代理对象(如 JDK 动态代理或 CGLIB 代理)。
    • 关键点:代理对象内部持有对原始对象的引用(即 target 属性指向原始对象)。
  • 缓存更新
    • 将代理对象存入二级缓存(此时原始对象仍在创建中)。
    • 三级缓存中的工厂不会被移除(直到 UserService 完全初始化)。
  • 注入依赖
    • 将代理对象注入到原始对象的自依赖字段中(此时原始对象持有代理对象的引用)。

4. 继续初始化原始对象

  • 动作
    • 完成属性填充(如其他依赖注入)。
    • 执行 @PostConstruct 初始化方法。
    • 此时原始对象已完全初始化,但仍是原始对象(未被代理替换)。
  • 缓存状态
    • 一级缓存:无 UserService(因为尚未放入)。
    • 二级缓存UserService 的代理对象。
    • 三级缓存UserService 的 ObjectFactory

5. 将代理对象提升到一级缓存

  • 动作
    • Spring 将二级缓存中的代理对象移动到一级缓存(单例池)。
    • 清除二级缓存和三级缓存中的 UserService 相关条目。
  • 缓存状态
    • 一级缓存UserService 的代理对象。
    • 二级缓存:无 UserService
    • 三级缓存:无 UserService

关键问题解答

  • 原始对象的去向
    • 原始对象被代理对象持有(通过 target 属性引用),但对外暴露的永远是代理对象
    • 原始对象不会被销毁,它作为代理的目标对象长期存在。
  • 自注入的引用关系
    • 原始对象中注入的是代理对象(通过三级缓存的工厂提前暴露)。
    • 代理对象内部持有原始对象的引用(形成“代理 → 原始对象 → 代理”的引用链)。
  • 最终状态
    • 一级缓存中是代理对象,原始对象仅作为代理的目标存在。
    • 所有依赖 UserService 的 Bean 获得的都是代理对象。

理解误区:原始对象和代理对象之间相互引用就是循环依赖因为引用都是唯一的对象。

2. 无法用三级缓存解决的场景

构造器循环依赖(Constructor Circular Dependency):

@Service public class A { 
    private final B b; 
    public A(B b) { 
        this.b = b; 
    } // 构造器依赖 B 
}
@Service public class B { 
    private final A a;
    public B(A a) { 
    this.a = a; 
    } // 构造器依赖 A 
}
  • 原因:构造器注入必须在实例化时完成依赖注入,无法提前暴露半成品 Bean。
  • 结果:直接抛出 BeanCurrentlyInCreationException

多例(Prototype)作用域的循环依赖

  • Spring 不处理多例 Bean 的循环依赖,直接报错。

3.@Autowired指定构造器注入

  • 默认行为:如果类只有一个构造器,Spring 会自动用它注入依赖。
  • 多构造器时:需要用 @Autowired 明确指定哪个构造器生效:
@Service 
public class UserService { 
    private final OrderService orderService; 
    @Autowired // 显式指定带参构造器 
    public UserService(OrderService orderService) { 
    this.orderService = orderService; 
    } 
    public UserService() {} // 无参构造器(默认不生效)
}
  • 优点:强制依赖、不可变(final 字段)、易于测试。

4.@Value注入配置

  • 默认行为:如果配置不存在,Spring 会抛出 IllegalArgumentException
  • 解决方案
    1. 设置默认值(推荐):
@Value("${api.key:default-value}") 
private String apiKey; // 配置缺失时使用 "default-value"
  1. 允许 null
@Value("${api.key:#{null}}") 
private String apiKey; // 配置缺失时注入 nul
  1. 手动检查(复杂场景):
private String apiKey; 
public UserService(Environment env) { 
    this.apiKey = env.getProperty("api.key", "default-value"); 
}

总结对比表

场景能否解决循环依赖推荐方案
字段/Setter 注入✅(三级缓存)可选依赖、快速开发
构造器注入❌(直接报错)强制依赖、核心业务逻辑
@Value 注入配置不涉及循环依赖加默认值(${key:default}
多例 Bean 循环依赖❌(直接报错)避免多例 Bean 互相依赖

最佳实践

  1. 优先用构造器注入(强制依赖 + 不可变性)。
  2. 循环依赖改用 Setter/字段注入 或 @Lazy
  3. @Value 务必设置默认值,避免因配置缺失导致启动失败。

到此这篇关于Spring 依赖注入与循环依赖总结的文章就介绍到这了,更多相关Spring 依赖注入与循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java字符流缓冲区详解

    java字符流缓冲区详解

    这篇文章主要为大家详细介绍了java字符流缓冲区的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • 深入解析Java编程中的boolean对象的运用

    深入解析Java编程中的boolean对象的运用

    这篇文章主要介绍了Java编程中的boolean对象的运用,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-10-10
  • Java将Object转换为数组的代码

    Java将Object转换为数组的代码

    这篇文章主要介绍了Java将Object转换为数组的情况,今天在使用一个别人写的工具类,这个工具类,主要是判空操作,包括集合、数组、Map等对象是否为空的操作,需要的朋友可以参考下
    2022-09-09
  • Java封装数组实现在数组中查询元素和修改元素操作示例

    Java封装数组实现在数组中查询元素和修改元素操作示例

    这篇文章主要介绍了Java封装数组实现在数组中查询元素和修改元素操作,结合实例形式分析了java针对数组元素查询、修改的封装操作实现技巧,需要的朋友可以参考下
    2020-03-03
  • Java 8函数式接口之Consumer用法示例详解

    Java 8函数式接口之Consumer用法示例详解

    这篇文章主要为大家介绍了Java 8函数式接口之Consumer用法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • SpringBoot3集成RocketMq场景分析

    SpringBoot3集成RocketMq场景分析

    RocketMQ因其架构简单、业务功能丰富、具备极强可扩展性等特点被广泛应用,比如金融业务、互联网、大数据、物联网等领域的业务场景,这篇文章主要介绍了SpringBoot3集成RocketMq,需要的朋友可以参考下
    2023-08-08
  • 详解Java中的final关键字

    详解Java中的final关键字

    这篇文章主要给大家介绍了关于Java中final关键字的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-06-06
  • 举例说明Java中代码块的执行顺序

    举例说明Java中代码块的执行顺序

    这篇文章主要介绍了举例说明Java中代码块的执行顺序,包括静态属性和非静态属性以及构造函数等相关的执行先后,需要的朋友可以参考下
    2015-07-07
  • Eclipse项目有红感叹号的解决方法

    Eclipse项目有红感叹号的解决方法

    这篇文章主要为大家详细介绍了Eclipse项目有红感叹号的解决方法,给出了Eclipse项目有红感叹号的原因,以及如何解决?,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • 解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

    解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

    这篇文章主要介绍了解决spring @ControllerAdvice处理异常无法正确匹配自定义异常的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06

最新评论