Spring循环依赖如何解决的?

一、什么是循环依赖

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

第一种情况:自己依赖自己的直接依赖

请添加图片描述

第二种情况:两个对象之间的直接依赖

请添加图片描述

第三种情况:多个对象之间的间接依赖

请添加图片描述

前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

二、循环依赖的N种场景

spring中出现循环依赖主要有以下场景:请添加图片描述

1、单例的setter注入

这种注入方式应该是spring用的最多的,代码如下:

@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了。

2、spring内部有三级缓存

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

下面用一张图告诉你,spring是如何解决循环依赖的:

请添加图片描述
细心的朋友可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {
    }
}
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Service
public class TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {
    }
}

TestService1依赖于TestService2TestService3,而TestService2依赖于TestService1,同时TestService3也依赖于TestService1

按照上图的流程可以把TestService1注入到TestService2,并且TestService1的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1注入到TestService3的流程如图:
请添加图片描述
TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。

这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。
请添加图片描述
还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?

答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

针对这种场景spring是怎么做的呢?

答案就在AbstractAutowireCapableBeanFactorydoCreateBean方法的这段代码中:
在这里插入图片描述
它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

3、多例的setter注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

很多人说这种情况spring容器启动会报错,其实是不对的,我非常负责任的告诉你程序能够正常启动。

为什么呢?

其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了preInstantiateSingletons方法。。
标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean

而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。

如何让他提前初始化bean呢?

只需要再定义一个单例的类,在它里面注入TestService1

@Service
public class TestService3 {

    @Autowired
    private TestService1 testService1;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

4、构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,看看如下代码:

@Service
public class TestService1 {

    public TestService1(TestService2 testService2) {
    }
}
@Service
public class TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?
请添加图片描述
从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

5、单例的代理对象setter注入

这种注入方式其实也比较常用,比如平时使用:@Async注解的场景,会通过AOP自动生成代理对象。

@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

从前面得知程序启动会报错,出现了循环依赖:

为什么会循环依赖呢?

答案就在下面这张图中:
请添加图片描述
说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:
在这里插入图片描述
问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异常。
如果这时候把TestService1改个名字,改成:TestService6,其他的都不变。

@Service
public class TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

再重新启动一下程序,神奇般的好了。

what? 这又是为什么?

这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以TestService1TestService2先加载,而改了文件名称之后,TestService2TestService6先加载。

为什么TestService2TestService6先加载就没问题呢?

答案在下面这张图中:
请添加图片描述
这种情况testService6中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

6、DependsOn循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

@DependsOn(value = "testService2")
@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

程序启动之后,执行结果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中本来如果TestService1TestService2都没有加@DependsOn注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?

答案在AbstractBeanFactory类doGetBean方法的这段代码中:
在这里插入图片描述
它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

三、出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是spring默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:
在这里插入图片描述

1、生成代理对象产生的循环依赖

这类循环依赖问题解决方法很多,主要有:

  • 使用@Lazy注解,延迟加载
  • 使用@DependsOn注解,指定加载先后关系
  • 修改文件名称,改变循环依赖类的加载顺序

2、使用@DependsOn产生的循环依赖

这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

3、多例循环依赖

这类循环依赖问题可以通过把bean改成单例的解决。

4、构造器循环依赖

这类循环依赖问题可以通过使用@Lazy注解解决。

<think>嗯,用户现在想了解Spring框架中循环依赖解决方案。之前我已经回答过Spring Bean的生命周期,并且用户提到了相关的站内引用,不过他们现在更关注循环依赖处理。首先,我需要回忆Spring是如何处理循环依赖的,尤其是三级缓存机制。 用户提供的引用里提到,Spring通过三级缓存解决循环依赖,特别是singleton作用域的bean。三级缓存包括singletonObjects、earlySingletonObjects和singletonFactories。可能需要在回答中详细解释这三个缓存的作用,以及它们如何协作。比如,当创建Bean A时,它会被放入三级缓存中,如果它依赖Bean B,而Bean B又依赖Bean A,这时三级缓存允许提前暴露半成品的Bean A,供Bean B完成初始化。 另外,用户提到的Spring Boot 2.6.0之后对循环依赖处理更严格,可能需要说明版本变化带来的影响,比如默认禁止循环依赖,开发者需要显式配置才能允许。这时候应该建议用户优先考虑重构代码,而不是依赖技术手段解决,因为循环依赖可能暗示设计问题。 还要注意用户之前的问题中提到了Bean的生命周期,所以在解释循环依赖解决方案时,可能需要结合Bean的实例化和属性注入阶段,说明在哪个阶段三级缓存被使用。比如,在实例化后、属性注入前,将对象工厂放入三级缓存,这样当发生循环依赖时,可以提前获取到未完全初始化的bean实例。 用户可能还会关心不同作用域的bean是否支持循环依赖,比如原型作用域的bean,Spring无法解决,这时候需要说明原因,并建议避免这种情况。此外,@Lazy注解的使用场景和限制也需要提到,它如何延迟依赖的解析,从而打破循环,但可能带来的潜在问题。 需要确保回答结构清晰,分点说明解决方案、注意事项和常见方法,同时结合代码示例,如使用@Lazy或setter注入的例子。最后,相关问题部分可以引导用户深入理解三级缓存、设计模式的应用以及版本差异的影响。</think>以下是Spring框架解决循环依赖的核心方案和关键细节分析: --- ### Spring循环依赖解决方案 #### 核心机制:三级缓存 Spring通过三级缓存解决**singleton作用域**的循环依赖问题: $$ \begin{aligned} 1. &\text{一级缓存(完全初始化): singletonObjects} \\ 2. &\text{二级缓存(半成品对象): earlySingletonObjects} \\ 3. &\text{三级缓存(对象工厂): singletonFactories} \end{aligned} $$ #### 解决流程(以A↔B循环依赖为例) ```mermaid flowchart TD A[创建Bean A] --> B[实例化A对象] B --> C[将A对象工厂放入三级缓存] C --> D[注入B属性] D --> E{检查B是否在缓存中} E --> |未找到| F[开始创建Bean B] F --> G[实例化B对象] G --> H[将B对象工厂放入三级缓存] H --> I[注入A属性] I --> J{从三级缓存获取A对象工厂} J --> K[通过工厂获取A的早期引用] K --> L[将A升级到二级缓存] L --> M[完成B的初始化] M --> N[将B放入一级缓存] N --> O[返回B到A的注入点] O --> P[完成A的初始化] P --> Q[将A放入一级缓存] ``` #### 关键特性 1. **仅支持单例模式** 原型(prototype)作用域的Bean无法解决循环依赖[^1] 2. **构造器注入限制** 若循环依赖通过构造器注入发生,Spring会直接抛出`BeanCurrentlyInCreationException`[^2] 3. **自动代理冲突规避** 通过`SmartInstantiationAwareBeanPostProcessor`接口处理AOP代理对象的循环依赖 --- ### 常见解决方案对比 | 方法 | 实现方式 | 适用场景 | |---------------------|----------------------------------|----------------------------| | @Lazy延迟加载 | 在依赖项添加@Lazy注解 | 简单循环且无需立即初始化的场景 | | Setter/Field注入 | 替换构造器注入为属性注入 | 大多数单例Bean循环依赖场景 | | 接口分离重构 | 提取公共接口解耦依赖关系 | 设计优化型解决方案 | | 上下文分离 | 使用@DependsOn明确初始化顺序 | 复杂依赖链场景 | --- ### Spring Boot 2.6+新特性 1. **严格模式默认开启** 启动时检测到循环依赖会直接报错,需通过配置开启兼容模式: ```properties spring.main.allow-circular-references=true ``` 2. **设计优先原则** 官方建议重构代码而不是依赖技术方案解决循环依赖 --- #### 代码示例 ```java // 使用@Lazy解决构造器循环依赖 @Service public class ServiceA { private final ServiceB serviceB; public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; } } // Setter注入方案 @Service public class ServiceB { private ServiceA serviceA; @Autowired public void setServiceA(ServiceA serviceA) { this.serviceA = serviceA; } } ``` --- ### 相关问题 1. **三级缓存如何避免并发问题?** 2. **为什么原型作用域的Bean不能解决循环依赖?** 3. **Spring如何检测循环依赖的存在?** 4. **循环依赖对事务管理有什么影响?** [^1]: Spring通过提前暴露对象引用的方式解决单例Bean的循环依赖问题 [^2]: Spring Boot 2.6+版本对循环依赖采取更严格的设计规范
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java亮小白1997

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值