循环依赖问题

报错

Description:

The dependencies of some of the beans in the application context form a cycle:

appController (field private com.yzf.service.AppService com.yzf.controller.AppController.service)

┌─────┐

| appServiceImpl defined in file [...]

↑ ↓

| aiAssistantFactorydefined in file [...]

└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

很明显这是 Spring Bean 的循环依赖问题。想看解决方法可以直接看 作为开发者,如何解决和避免循环依赖

目录

  1. 什么是循环依赖?
  2. 循环依赖的场景
  3. Spring 如何解决循环依赖?(核心)
    • 关键:三级缓存
    • 详细流程拆解
  1. 为什么需要三级缓存,二级可以吗?
  2. Spring 无法解决的循环依赖
  3. 作为开发者,如何解决和避免循环依赖?
  4. 总结

1. 什么是循环依赖?

  1. 循环依赖指的是两个或多个 Bean 之间相互依赖,形成一个闭环。
  2. 最简单的形式是:
  • A 依赖 B。
  • B 依赖 A。
  1. Spring 容器启动时,它需要创建 AB 的实例
    • 创建 A 时,发现它需要 B,于是容器会去创建 B
    • 创建 B 时,发现它需要 A,容器又会去创建 A
    • 这就形成了一个死循环。

2. 循环依赖的场景

循环依赖主要分为三种:

  1. 构造器注入的循环依赖:Spring 无法解决的。
  2. Setter、字段注入的循环依赖:Spring 已经解决的,也是我们讨论的重点。
  3. 多例prototype) 作用域的循环依赖:Spring 无法解决的。

“Spring 解决了循环依赖”,特指的是默认单例(Singleton)作用域下,通过 Setter 注入的循环依赖。


3. Spring 如何解决循环依赖?(核心)

  • 核心是 三级缓存提前暴露
  • 这三个缓存都位于 DefaultSingletonBeanRegistry 类中:
  • 一级缓存(singletonObjects
    • 类型:Map<String, Object>
    • 作用:成品缓存。存放完全初始化好的 Bean 实例。
  • 二级缓存(earlySingletonObjects
    • 类型:Map<String, Object>
    • 作用:半成品缓存。存放提前暴露的 Bean 实例,Bean 已经被实例化,但未完成属性注入和初始化
  • 三级缓存(singletonFactories
    • 类型:Map<String, ObjectFactory<?>>
    • 作用:工厂缓存。存放用于创建 Bean 的工厂对象。工厂对象的主要作用是,在需要的时候,可以生产出一个 Bean 的实例(可能是原始实例,也可能是代理实例)。这是解决 AOP 代理问题的关键。
  • 实例化?属性注入?初始化?这些是啥?这是 Bean 的生命周期。
    • 以下是 Bean 的简单的生命周期 -- 5 大步,看完再回去看 三个缓存,就不懵了

 

    详细流程拆解

    我们以来实例化(创建) AB 为例,逐步分析 Spring 的处理流程:

    1.getBean("A") 开始:

    • Spring 容器尝试获取 A
    • 检查一级缓存 :没有 A
    • 发现 A 正在实例化的标记也没有,于是开始实例化 A
    • 检查三级缓存 :没有 A
    • 检查二级缓存 :没有 A
    • 实例化 A
      • 通过反射实例化 A。此时 A 是一个“半成品”,它的 B 属性还是 null
      • 【关键步骤 1】:Spring 并不是直接将这个半成品放入二级缓存,而是创建一个对象工厂 (ObjectFactory),并将这个工厂放入三级缓存 中。
      • 这个工厂对象的 getObject() 方法会在被调用时,决定是返回原始的 A 实例还是经过 AOP 代理后的代理对象。

    2.为 A 注入属性:

    • Spring 开始填充 A 的属性,发现它需要 B
    • 于是 Spring 会去调用 getBean("B")

    3.getBean("B") 开始:

    • Spring 容器尝试获取 B
    • 检查一级缓存:没有 B
    • 开始实例化 B
    • 检查三级缓存 :没有 B
    • 检查二级缓存 :没有 B
    • 实例化 B
      • 通过反射实例化 B。此时 B 也是一个半成品
      • B 的 对象工厂 放入三级缓存

    4.为 B 注入属性:

    1. Spring 填充 B 的属性,发现它需要 A
    2. 于是 Spring 再次调用 getBean("A")

    5.再次 getBean("A")(循环依赖解决点):

    • 检查一级缓存 :没有 A(因为它还没完全创建好)。
    • 检查二级缓存 :没有 A
    • 【关键步骤 2】检查三级缓存 命中了! 之前为 A 放入的工厂在这里被找到了。
      • Spring 调用这个工厂的 getObject() 方法,获取 A早期引用(Early Reference)。
      • 早期引用(可能是原始对象,也可能是代理对象)被放入二级缓存,并从三级缓存中移除对应的工厂。
      • getBean("A") 返回这个早期引用。

    6.B 创建完成:

    • B 成功获取到了 A 的早期引用,并完成了属性注入。
    • B 继续执行后续的初始化操作(如 InitializingBean@PostConstruct 等)。
    • B 完全创建完成
    • 将完整的 B 实例放入一级缓存 ,并从二级缓存中移除(如果存在)。

    7.A 创建完成:

    • 回到第 3 步,AgetBean("B") 调用现在可以成功返回一个完整的 B 实例了。
    • A 成功完成了属性注入。
    • A 继续执行后续的初始化操作。
    • A 完全创建完成
    • 将完整的 A 实例放入一级缓存,并从二级缓存和三级缓存中移除。

    至此,AB 的循环依赖被成功解决,二者都已是完整的 Bean。

    1、按着 step 看这个图,就应该可以看明白了,比如:step1 -> step1-1 -> step2 -> step2-1

    2、放入二级缓存的没画,知道就行了

    3、细心的同学可能会考虑,最后的一级缓存B的属性A不是A的早期引用吗,会不会有什么问题?

    答:属性A指向了A的内存地址,最后A的初始化好的了,是完整的。但是早期引用在 init 方法中可能会产生空指针异常


    4. 为什么需要三级缓存,二级可以吗?

    1. 这是一个经典面试题。如果没有 AOP,二级缓存是足够的。

    流程会变成:

    1. 实例化 A,将半成品的 A 放入二级缓存。
    2. A 依赖 B,去创建 B
    3. 实例化 B,将半成品的 B 放入二级缓存。
    4. B 依赖 A,从二级缓存中拿到半成品的 A
    5. B 创建完成,放入一级缓存。
    6. A 拿到完整的 B,创建完成,放入一级缓存。

    2. 那么,为什么需要三级缓存呢?

    答案是:为了处理 AOP 代理

    如果一个 Bean 需要被 AOP 代理,那么注入到其他 Bean 中的应该是它的代理对象,而不是原始对象。但 Spring 无法提前知道一个 Bean 是否需要被代理(因为代理的逻辑,如 BeanPostProcessor,是在 Bean 完全初始化之后才执行的)。

    • 如果 Spring 在实例化后立即创建代理,并放入二级缓存,这违背了 Spring 的设计。因为 BeanPostProcessor 应该在 Bean 初始化后(populateBean之后)才执行。
    • 如果 Spring 把原始对象放入二级缓存,那么当其他 Bean 依赖它时,拿到的就是原始对象,而不是代理对象,AOP 就会失效。

    三级缓存 完美地解决了这个问题。

    它存放的不是 Bean 对象,而是一个工厂。当其他 Bean 需要依赖这个半成品 Bean 时,会调用这个工厂的 getObject() 方法。在这个方法内部,Spring 就可以判断“这个 Bean 将来是否需要被代理?”,如果需要,就返回代理对象;如果不需要,就返回原始对象。


    5. Spring 无法解决的循环依赖

    1. 构造器注入
      1. 原因:Bean 的实例化(调用构造函数)和依赖注入是原子操作。创建 A 的实例就需要 B 的实例,而创建 B 的实例就需要 A 的实例。没有机会像 Setter 注入那样,先把一个“半成品”暴露出去。这是一个无法解开的死结。
      2. 报错BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation: Is there an unresolvable circular reference?
    2. 多例 prototype 作用域
      1. 原因:Spring 不会缓存 prototype 作用域的 Bean。每次请求都会创建一个新的实例,因此无法使用三级缓存机制来提前暴露引用。这会导致无限循环创建,最终栈溢出。

     

    6. 作为开发者,如何解决和避免循环依赖?

    尽管 Spring 能解决部分循环依赖,但循环依赖通常被认为是一种代码设计缺陷。它表明类与类之间的职责划分不清。

    1.如果你是 springBoot >=2.6.0,你会发现普通的 setter、字段注入都会报循环依赖问题

    spring:
      main:
        allow-circular-references: true

    2.重构代码结构(最佳实践)

    // 重构前: A -> B -> A
    // 重构后: A -> C, B -> C
    • 这是最推荐的方式。重新思考你的设计,是否可以将公共逻辑抽取到一个新的类 C 中,让 A 和 B 都依赖 C,从而打破循环。

    3.使用 @Lazy 注解

    @Service
    public class ServiceA {
        @Autowired
        @Lazy // 关键点
        private ServiceB serviceB;
    }
    • 在注入点使用 @Lazy 注解。这会延迟该依赖的加载。Spring 会注入一个代理对象,只有当你第一次使用这个依赖(调用它的方法)时,真正的目标 Bean 才会被创建和注入。这打破了启动时的创建循环。

    4.使用 @PostConstruct

    @Service
    public class ServiceA implements ApplicationContextAware {
        private ServiceB serviceB;
    
        @Autowired
        private ApplicationContext context;
    
        @PostConstruct
        public void init() {
            // 在构造之后,手动获取依赖
            this.serviceB = context.getBean(ServiceB.class);
        }
    }
    • 将依赖注入的逻辑从构造或字段注入移到 @PostConstruct 标记的方法中。此时 Bean 本身已经实例化完成,再手动去容器中获取依赖。

    5.使用 spring 的发布订阅机制

    @Slf4j
    @Component
    public class AiAssistantFactory { 
    
        @Autowired
        private AppService appService;
    
        @Async
        @PostConstruct
        public void init() {
            log.info("=== 初始化 AiAssistant 开始 ===");
            List<App> apps = appService.list();
            apps.forEach(this::createOrUpdateAiAssistant);
            log.info("=== 初始化 AiAssistant 结束 ===");
        }
    }
    
    
    @Service
    public class AppServiceImpl extends ServiceImpl<AppMapper, App> implements AppService {
         @Override
        public Long addApp(AppAddRequest addRequest) {
            ....
            // 添加 aiAssistant
            SpringContextUtils.publishEvent(new CreateOrUpdateAiAssistantEvent(entity));
            ....
        }
    }
    
    @EventListener({CreateOrUpdateAiAssistantEvent.class})
    public void AiAssistantBuildListener(CreateOrUpdateAiAssistantEvent event) {
        log.info("===== 新增或更新 aiAssistant 事件开始 =====");
        aiAssistantFactory.createOrUpdateAiAssistant((App) event.getSource());
        log.info("===== 新增或更新 aiAssistant 事件结束 =====");
    }

    6.将 Setter 注入改为构造器注入

    • Spring 官方推荐使用构造器注入。因为它可以保证依赖的不可变性,并且能在启动时就暴露循环依赖问题(快速失败),迫使你优化设计。

    7. 总结

    特性

    描述

    核心问题

    两个或多个 Bean 在创建过程中相互等待对方完成,形成死锁。

    Spring 的解决方案

    三级缓存 + 提前暴露引用

    一级缓存 singletonObjects

    成品池,存放完全初始化的单例 Bean。

    二级缓存 earlySingletonObjects

    半成品池,存放已实例化但未初始化的 Bean 的早期引用。

    三级缓存 singletonFactories

    工厂池,存放能产生 Bean(或其代理)的工厂,用于解决 AOP 问题。

    能解决的场景

    单例(Singleton)作用域下的 Setter/Field 注入。

    不能解决的场景

    1. 构造器注入:实例化和注入是原子操作,无法提前暴露。
    2. prototype 作用域:不缓存 Bean,无法利用缓存机制。

    最佳实践

    重构代码,避免循环依赖。如果无法避免,可使用 @Lazy 作为临时解决方案。

    为何需要三级缓存

    为了在不破坏 Bean 生命周期的情况下,延迟 AOP 代理对象的创建,确保注入的是正确的代理对象。


     

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值