Spring 是如何解决循环依赖问题

Spring 框架通过 三级缓存 机制来解决循环依赖问题。循环依赖是指两个或多个 Bean 相互依赖,形成一个闭环,例如 Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A。Spring 通过提前暴露未完全初始化的 Bean 来解决这个问题。

以下是 Spring 解决循环依赖的详细机制和流程:


1. Spring Bean 的生命周期

在理解循环依赖之前,需要了解 Spring Bean 的生命周期。Spring Bean 的创建过程主要包括以下步骤:

  1. 实例化:通过构造函数或工厂方法创建 Bean 的实例。
  2. 属性填充 :将依赖的 Bean 注入到当前 Bean 中(通过 @Autowired@Resource 等注解或 XML 配置)。
  3. 初始化 :调用初始化方法(如 @PostConstructInitializingBeanafterPropertiesSet 方法)。
  4. 销毁:在容器关闭时调用销毁方法。

循环依赖通常发生在 属性填充 阶段。


2. 三级缓存机制

Spring 通过三级缓存来解决循环依赖问题。三级缓存分别是:

  1. 一级缓存(Singleton Objects):存储完全初始化好的单例 Bean。
  2. 二级缓存(Early Singleton Objects):存储提前暴露的未完全初始化的 Bean(仅实例化,未填充属性)。
  3. 三级缓存(Singleton Factories) :存储 Bean 的工厂对象(ObjectFactory),用于生成提前暴露的 Bean。

3. 解决循环依赖的流程

以下是一个典型的循环依赖场景:

  • Bean A 依赖 Bean B。
  • Bean B 依赖 Bean A。

Spring 解决循环依赖的流程如下:

步骤 1:创建 Bean A
  1. Spring 开始创建 Bean A,调用构造函数实例化 Bean A。
  2. 将 Bean A 的工厂对象(ObjectFactory)放入三级缓存。
  3. 开始填充 Bean A 的属性,发现 Bean A 依赖 Bean B。
步骤 2:创建 Bean B
  1. Spring 开始创建 Bean B,调用构造函数实例化 Bean B。
  2. 将 Bean B 的工厂对象(ObjectFactory)放入三级缓存。
  3. 开始填充 Bean B 的属性,发现 Bean B 依赖 Bean A。
步骤 3:解决 Bean B 的依赖
  1. Spring 从三级缓存中获取 Bean A 的工厂对象,生成 Bean A 的早期引用(未完全初始化的 Bean A)。
  2. 将 Bean A 的早期引用放入二级缓存,并从三级缓存中移除 Bean A 的工厂对象。
  3. 将 Bean A 的早期引用注入到 Bean B 中。
  4. Bean B 完成属性填充和初始化,成为一个完全初始化的 Bean。
  5. 将 Bean B 放入一级缓存。
步骤 4:完成 Bean A 的创建
  1. Spring 将 Bean B 注入到 Bean A 中。
  2. Bean A 完成属性填充和初始化,成为一个完全初始化的 Bean。
  3. 将 Bean A 放入一级缓存,并从二级缓存中移除 Bean A 的早期引用。

4. 代码示例

以下是一个简单的循环依赖示例:

java 复制代码
@Component
public class BeanA {
    @Autowired
    private BeanB beanB;
}

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

Spring 会通过三级缓存机制解决 BeanABeanB 之间的循环依赖。


5. 解决循环依赖的限制

Spring 的循环依赖解决方案有以下限制:

  1. 仅支持单例 Bean:Spring 只能解决单例作用域(Singleton)的 Bean 的循环依赖。原型作用域(Prototype)的 Bean 无法解决循环依赖。

  2. 构造函数注入无法解决循环依赖:如果循环依赖是通过构造函数注入的,Spring 无法解决。因为构造函数注入需要在实例化时完成依赖注入,而三级缓存机制无法提前暴露未完全初始化的 Bean。

    java 复制代码
    @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;
        }
    }

    上述代码会抛出 BeanCurrentlyInCreationException 异常。

  3. 避免复杂的循环依赖:虽然 Spring 可以解决简单的循环依赖,但复杂的循环依赖可能会导致代码难以维护和理解,应尽量避免。


6. 如何避免循环依赖

  1. 使用 Setter 注入或字段注入:避免使用构造函数注入。

  2. 重新设计代码:通过重新设计类之间的关系,消除循环依赖。

  3. 使用 @Lazy 注解 :延迟加载依赖的 Bean。

    java 复制代码
    @Component
    public class BeanA {
        private final BeanB beanB;
    
        @Autowired
        public BeanA(@Lazy BeanB beanB) {
            this.beanB = beanB;
        }
    }

7. 缓存状态的变化

以下是缓存状态的变化过程:

步骤 一级缓存 (singletonObjects) 二级缓存 (earlySingletonObjects) 三级缓存 (singletonFactories)
创建 BeanA 实例后 BeanA 的工厂对象
创建 BeanB 实例后 BeanABeanB 的工厂对象
解决 BeanB 依赖后 BeanA 的早期引用 BeanB 的工厂对象
BeanB 创建完成后 BeanB BeanA 的早期引用
BeanA 创建完成后 BeanABeanB

总结

Spring 通过三级缓存机制解决了单例 Bean 的循环依赖问题,但构造函数注入和原型 Bean 的循环依赖无法解决。在实际开发中,应尽量避免循环依赖,保持代码的清晰和可维护性。如果无法避免,可以使用 Setter 注入或 @Lazy 注解来解决。

相关推荐
bing_1585 分钟前
Spring Boot 项目中什么时候会抛出 FeignException?
java·spring boot·后端
Java&Develop36 分钟前
springboot + mysql8降低版本到 mysql5.7
java·spring boot·后端
sg_knight38 分钟前
从单体架构到微服务:架构演进之路
java·spring boot·spring·spring cloud·微服务·云原生·架构
武昌库里写JAVA1 小时前
MacOS Python3安装
java·开发语言·spring boot·学习·课程设计
eternal__day1 小时前
Spring Cloud:构建云原生微服务架构的最佳工具和实践
java·spring cloud·微服务·云原生·架构
cdut_suye1 小时前
【Linux系统】从 C 语言文件操作到系统调用的核心原理
java·linux·数据结构·c++·人工智能·机器学习·云计算
forestsea1 小时前
Maven 插件参数注入与Mojo开发详解
java·maven·mojo
荔枝吻2 小时前
【抽丝剥茧知识讲解】引入mybtis-plus后,mapper实现方式
java·sql·mybatis
在未来等你2 小时前
互联网大厂Java求职面试:构建高并发直播平台的架构设计与优化
java·spring boot·微服务·kubernetes·高并发·分布式系统·直播平台
傻小胖2 小时前
json-server的用法-基于 RESTful API 的本地 mock 服务
后端·json·restful