当前位置: 首页 > news >正文

Spring 循环依赖

A 需要 B,而 B 又需要 A,形成一个循环依赖。

1. Spring 如何解决循环依赖?

Spring 通过三级缓存机制解决了循环依赖问题,具体步骤如下:

  1. 实例化阶段

    • 当容器创建一个 Bean 时,首先会调用其无参构造函数完成实例化(即分配内存,但未进行属性注入)。
    • 实例化完成后,将该 Bean 的“早期引用”(尚未完全初始化的对象)放入三级缓存中。
  2. 属性注入阶段

    • 在为当前 Bean 注入属性时,如果发现依赖的 Bean 尚未完全初始化,则从三级缓存中获取其“早期引用”,完成属性注入。
  3. 初始化阶段

    • 属性注入完成后,执行 Bean 的初始化方法(如 @PostConstruct 或 InitializingBean.afterPropertiesSet())。
    • 初始化完成后,将该 Bean 从三级缓存移动到一级缓存,并清除二级和三级缓存。

三级缓存的具体作用

  1. 一级缓存(singletonObjects

    • 存储完全初始化后的 Bean 实例。
    • 当一个 Bean 完成所有属性注入和初始化后,它会被放入一级缓存中,供后续依赖使用。
  2. 二级缓存(earlySingletonObjects

    • 存储“早期引用”(尚未完全初始化的 Bean 实例)。
    • 如果某个 Bean 在初始化过程中被其他 Bean 依赖,则会从二级缓存中获取其“早期引用”,完成属性注入。
  3. 三级缓存(singletonFactories

    • 存储 Bean 的工厂对象(ObjectFactory),用于动态生成代理对象或其他扩展功能。
    • 三级缓存的主要作用是支持 AOP 动态代理等复杂场景。例如,当一个 Bean 需要被代理时,Spring 会在三级缓存中通过工厂动态生成代理对象。

三级缓存的工作流程

以下是一个典型的循环依赖解决过程:

  1. 实例化阶段

    • 创建 Bean 的实例(调用无参构造函数),但不进行属性注入。
    • 将该 Bean 的工厂对象(ObjectFactory)放入三级缓存中。
  2. 属性注入阶段

    • 如果当前 Bean 需要依赖另一个 Bean,而该 Bean 尚未完全初始化,则从三级缓存中获取其工厂对象,并生成“早期引用”。
    • 将生成的“早期引用”放入二级缓存中,供当前 Bean 使用。
  3. 初始化阶段

    • 属性注入完成后,执行 Bean 的初始化方法(如 @PostConstruct 或 InitializingBean.afterPropertiesSet())。
    • 初始化完成后,将该 Bean 从二级缓存移动到一级缓存,并清除三级缓存和二级缓存中的相关记录。

2.为什么需要三级缓存?一级和二级不够吗?

一级缓存的问题
  • 一级缓存只能存储完全初始化后的 Bean。如果在初始化过程中发生循环依赖,无法从一级缓存中获取尚未完全初始化的 Bean。
二级缓存的问题
  • 二级缓存可以存储“早期引用”,但如果涉及动态代理(如 AOP),则需要额外的机制来生成代理对象。二级缓存无法动态生成代理对象,因此需要三级缓存。
三级缓存的优势
  • 三级缓存通过工厂模式动态生成代理对象,确保在复杂的依赖关系中,Bean 的状态一致且正确。
  • 三级缓存的设计使得 Spring 能够支持更多的扩展功能(如 AOP 动态代理)。

三级缓存的好处和坏处

好处
  1. 支持复杂的依赖关系

    三级缓存允许在 Bean 的生命周期中动态生成代理对象(如 AOP 动态代理),从而支持更复杂的场景。
  2. 保证线程安全

    三级缓存的设计使得在多线程环境下,Bean 的创建和依赖注入更加安全。
  3. 灵活性

    三级缓存提供了一个灵活的机制,可以在不同阶段对 Bean 的状态进行控制。
坏处
  1. 增加复杂性

    三级缓存的引入增加了代码的复杂性,理解和维护成本较高。
  2. 性能开销

    虽然三级缓存的时间复杂度仍然是 O(1)O(1),但多级缓存的管理会增加一定的性能开销。
  3. 潜在问题

    如果开发者滥用循环依赖,可能导致系统设计不合理,甚至出现难以调试的问题。

3.Spring Boot 高版本为什么要手动开启循环依赖?

背景

在 Spring Boot 2.6 及更高版本中,默认情况下禁用了循环依赖(spring.main.allow-circular-references=false)。这是因为循环依赖虽然可以通过三级缓存解决,但它通常是设计上的问题,可能会导致以下问题:

  1. 代码耦合度过高

    循环依赖通常意味着类之间的职责划分不清晰,容易导致代码难以维护。例如,两个类互相依赖可能表明它们的职责没有很好地分离。
  2. 隐藏设计缺陷

    循环依赖可能掩盖了系统设计中的不合理之处。长期来看,这种设计会导致架构变得复杂且难以扩展。
  3. 测试困难

    循环依赖的 Bean 在单元测试中可能更难模拟和测试,因为它们的依赖关系过于紧密。
  4. 性能问题

    循环依赖的解决需要额外的缓存管理和状态检查,这会增加一定的性能开销。
如何手动开启循环依赖?

在 application.properties 或 application.yml 中设置以下配置:

spring.main.allow-circular-references=true

或者通过代码方式启用:

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MyApplication.class);
        app.setAllowCircularReferences(true);
        app.run(args);
    }
}

4.循环依赖的实际案例分析

案例 1:简单的循环依赖

假设有两个类 A 和 B

@Component
public class A {
    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }
}

在这种情况下,Spring 会通过三级缓存解决循环依赖问题。A 和 B 之间的依赖关系会在实例化阶段被正确处理。

案例 2:涉及动态代理的循环依赖

假设 A 是一个事务管理的 Bean,Spring 会为其生成动态代理对象:

@Component
public class A {
    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }

    @Transactional
    public void doSomething() {
        // 事务逻辑
    }
}

@Component
public class B {
    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }
}

在这种情况下,Spring 会通过三级缓存中的工厂对象动态生成 A 的代理对象,从而解决循环依赖问题。


5.循环依赖的替代方案

为了避免循环依赖,可以采用以下替代方案:

引入中间层

  • 将共同的逻辑抽取到一个新的服务类中,减少直接依赖。
  • @Service
    public class CommonService {
        public void commonLogic() {
            // 共同逻辑
        }
    }
    
    @Component
    public class A {
        private final CommonService commonService;
    
        @Autowired
        public A(CommonService commonService) {
            this.commonService = commonService;
        }
    }
    
    @Component
    public class B {
        private final CommonService commonService;
    
        @Autowired
        public B(CommonService commonService) {
            this.commonService = commonService;
        }
    }
    

使用事件驱动机制

  • 使用 Spring 的事件发布机制(ApplicationEvent)来解耦组件。
  • @Component
    public class A {
        @Autowired
        private ApplicationEventPublisher eventPublisher;
    
        public void doSomething() {
            eventPublisher.publishEvent(new CustomEvent(this));
        }
    }
    
    @Component
    public class B {
        @EventListener
        public void handleCustomEvent(CustomEvent event) {
            // 处理事件
        }
    }
    

使用懒加载(@Lazy 注解)

  • 通过 @Lazy 注解延迟 Bean 的初始化,从而避免循环依赖问题。
  • @Component
    public class A {
        private final B b;
    
        @Autowired
        public A(@Lazy B b) {
            this.b = b;
        }
    }
    
    @Component
    public class B {
        private final A a;
    
        @Autowired
        public B(@Lazy A a) {
            this.a = a;
        }
    }
    

重构代码逻辑

  • 如果两个类之间存在循环依赖,可能表明它们的职责划分不清晰。可以通过重构代码来消除直接依赖。
  • @Service
    public class A {
        private final CommonLogic commonLogic;
    
        @Autowired
        public A(CommonLogic commonLogic) {
            this.commonLogic = commonLogic;
        }
    
        public void doSomething() {
            commonLogic.performLogic();
        }
    }
    
    @Service
    public class B {
        private final CommonLogic commonLogic;
    
        @Autowired
        public B(CommonLogic commonLogic) {
            this.commonLogic = commonLogic;
        }
    
        public void doSomethingElse() {
            commonLogic.performLogic();
        }
    }
    
    @Component
    public class CommonLogic {
        public void performLogic() {
            // 共同逻辑
        }
    }
    

    6.最终总结

    • 循环依赖的本质

      • 循环依赖是指两个或多个 Bean 之间相互依赖,形成一个闭环。
      • Spring 通过三级缓存机制解决了循环依赖问题。
    • 三级缓存的作用

      • 一级缓存:存储完全初始化后的 Bean。
      • 二级缓存:存储“早期引用”(尚未完全初始化的 Bean)。
      • 三级缓存:存储工厂对象,用于动态生成代理对象或其他扩展功能。
    • 为什么需要三级缓存?

      • 一级缓存无法解决循环依赖,因为它只存储完全初始化后的 Bean。
      • 二级缓存无法支持动态代理等复杂场景,因此需要三级缓存。
    • Spring Boot 高版本默认禁用循环依赖的原因

      • 推动更好的设计,避免代码耦合度过高。
      • 减少潜在问题,提高系统性能。
    • 如何手动开启循环依赖?

      • 通过配置文件或代码方式启用 spring.main.allow-circular-references=true
    • 最佳实践

      • 尽量避免循环依赖,采用重构代码、引入中间层或事件驱动机制等方式解决问题。
      • 如果确实无法避免循环依赖,确保它不会导致系统设计问题,并且只在必要的场景下使用。

    相关文章:

  • python并发爬虫
  • 基于Spring Boot的个性化商铺系统的设计与实现(LW+源码+讲解)
  • 数据结构day04
  • 爱普生VG3225EFN压控晶振5G基站低噪声的解决方案
  • windows下面nginx配置及测试
  • 网络安全之vlan实验
  • 接口/UI自动化面试题
  • Springboot整合elasticsearch详解 封装模版 仓库方法 如何在linux里安装elasticsearch
  • 八股——Mysql篇
  • WebAssembly实践,性能也有局限性
  • 小白工具PDF转换 PDF转图片 超便捷软件 文件格式转换 简单好用效率高
  • 新手村:逻辑回归-理解04:熵是什么?
  • 第五天 开始Unity Shader的学习之旅之Unity中的基础光照之漫反射光照模型
  • 座舱网联融合新旗舰!移远通信48 TOPS座舱方案携AI大模型能力,赋能多域融合
  • LabVIEW时间触发协议
  • husky的简介以及如果想要放飞自我的解决方案
  • CCF-GESP 等级考试 2025年3月认证C++一级真题解析
  • 一文解读DeepSeek在工业制造领域的应用
  • Win32 / C++ ini配置文件解析类(支持简易加解密)
  • 线性代数核心概念与NumPy科学计算实战全解析
  • 绍兴市越城区建设局网站/店铺运营方案策划
  • 找网络公司建网站每年收维护费/必应搜索推广
  • 网站切片 做程序/免费站推广网站2022
  • iis做网站上传速度慢/网站搜索工具
  • 天津红桥网站建设/网站seo技术能不能赚钱
  • 比较好的做外贸网站/做推广哪个平台好