Spring循环依赖解决核心思想

文章详细阐述了Spring框架如何处理循环依赖问题,包括普通解决方法、Spring的特殊处理以及AOP下的循环依赖解决方案。通过数据结构如单例池、早期对象和对象工厂,Spring能够在bean创建过程中打破循环并确保AOP的正确应用。同时,文章提到了Spring无法解决的特定循环依赖场景,如原型bean的循环依赖和构造器依赖。

前言

循环依赖基本是一个会必然出现的问题,所以spring是必须解决的,初看起来很复杂,但是如果分开理解就不会过于复杂了,本篇文章主要是讲解spring中循环依赖的解决方案

循环依赖

比如有以下的两个类,如下

public class A {
    @Autowire
    private B b;
}

public class B {
    @Autowire
    private A a;
}

普通的循环依赖解决

如果不是在spring中,我们可以很容易的解决这个问题,如下


B b = new B();
A a = new A();

b.a = a;
a.b = b;

只需要进行如此的设置就可以解决了,但是如果在spring中那么这个流程就会变得复杂,主要是spring中bean都是有生命周期的,如下

spring中的循环依赖

解设A先创建的,有如下的过程

1. 初始化A
2. 填充A的属性,从单例池获取B
3. bean后置处理器的前置方法
4. bean后置处理器的后置方法
5. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.1. 初始化B
2.2. 填充B的属性,从单例池获取A
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

如上面所示,如果不进行任何处理,那么就会陷入死循环,spring肯定是需要进行处理的,那么是如何处理的,我们就一步步往下面看

spring循环依赖解决

首先,可以先说下解决的核心点,主要有以下的数据结构与作用

Map<String, Object> singletonObjects: 单例池,存储了成熟的bean
Map<String, Object> earlySingletonObjects: 早期未经过完整bean周期的bean
Map<String, ObjectFactory<?>> singletonFactories:用于解决是否需要aop返回对应的对象的
Set<String> singletonsCurrentlyInCreation:当前正在创建的bean集合
Map<Object, Object> earlyProxyReferences:是否已经提前进行aop了

下面就慢慢进行分析整个过程了

打破循环依赖

在上面的问题中,就是出现了死循环的依赖,那么最简单的方法就是加入一个map打破这个循环即可,如下

解设A先创建的,有如下的过程

1. 初始化A->zxcMap<a,A的原始对象>
2. 填充A的属性,从单例池获取B
3. bean后置处理器的前置方法
4. bean后置处理器的后置方法
5. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.1. 初始化B
2.2. 填充B的属性,从单例池获取a->zxcMap获取到了
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

如上,只要添加一个zxcMap就可以打破这个循环了,那么是不是就解决这个问题了呢?如果不考虑aop的话,确实是已经解决了,但是如果出现了aop就有可能出现问题了

aop下循环的问题

解设在实例化A到第4. bean后置处理器的后置方法的时候,A需要进行aop,那么最后放到单列池的就是AOP的代理对象,但B从zxcMap获取的对象是原始对象,那么很明显就有问题了

aop循环解决方案一

可以直接这样 1. 初始化A->aop-> zxcMap<a,A的代理对象>,这样就可以解决了,但是如果这样bean的生命周期就有问题了,因为不是所有bean都需要提前进行aop的,所以就有问题了

aop循环解决方案二

那么就不能这样做,我们需要确认是否发生了循环依赖以后再提前aop,那么如何弄,只需要进行下面的操作

解设A先创建的,有如下的过程

0. createset放置a正在创建中
1. 初始化A->zxcMap<a,A的原始对象>
2. 填充A的属性,从单例池获取B
3. bean后置处理器的前置方法
4. bean后置处理器的后置方法
5. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.0  createset放置b正在创建中
2.1. 初始化B                                     ->a原始对象    
2.2. 填充B的属性,从单例池获取a->createset是否在创建a->提前aop->a的代理对象
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

如上,只要在a创建的时候放到一个set,在b获取a的时候就可以判断是否需要提前进行aop了,但是这里又引出了新的问题,假设此时A还和C进行了相互依赖,如下

public class A {
    @Autowire
    private B b;
    @Autowire
    private C c;
}

public class B {
    @Autowire
    private A a;
}
public class C {
    @Autowire
    private A a;
}

那么过程会变为如下

解设A先创建的,有如下的过程

0. createset放置a正在创建中
1. 初始化A->zxcMap<a,A的原始对象>
2. 填充A的属性,从单例池获取B
3. 填充A的属性,从单例池获取C
4. bean后置处理器的前置方法
5. bean后置处理器的后置方法
6. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.0  createset放置b正在创建中
2.1. 初始化B                                     ->a原始对象    
2.2. 填充B的属性,从单例池获取a->createset是否在创建a->提前aop->a的代理对象
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

当在添加A中的c属性时又会触发C的创建

3.0  createset放置C正在创建中
3.1. 初始化C                                    ->a原始对象    
3.2. 填充C的属性,从单例池获取a->createset是否在创建a->提前aop->a的代理对象
3.3. bean后置处理器的前置方法
3.4. bean后置处理器的后置方法
3.5. 放到单例池

这种情况下会发生了a的代理对象创建了两个,所以这里也是二级缓存出现的原因,逻辑就应该变为如下

解设A先创建的,有如下的过程

0. createset放置a正在创建中
1. 初始化A->zxcMap<a,A的原始对象>
2. 填充A的属性,从单例池获取B
3. 填充A的属性,从单例池获取C
4. bean后置处理器的前置方法
5. bean后置处理器的后置方法
6. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.0  createset放置b正在创建中
2.1. 初始化B                                     ->a原始对象    
2.2. 填充B的属性,从单例池获取a->createset是否在创建a->从earlySingletonObjects获取->提前aop->a的代理对象->放到earlySingletonObjects中
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

当在添加A中的c属性时又会触发C的创建

3.0  createset放置C正在创建中
3.1. 初始化C                                    ->a原始对象    
3.2. 填充C的属性,从单例池获取a->createset是否在创建a->earlySingletonObjects获取到直接返回->提前aop->a的代理对象
3.3. bean后置处理器的前置方法
3.4. bean后置处理器的后置方法
3.5. 放到单例池

所以earlySingletonObjects的作用就出来了,他是为了解决当bean发生aop的时候代理对象不会创建多个,那么实际上这样就解决这个问题了吗,为啥需要三级map,这是因earlySingletonObjects存的不一定是代理对象因为不一定要进行aop,也有可能是原始对象,那么原始对象如何返回呢,就需要三级map的帮助

三级map存在的原因

三级map其实是一串表达式,逻辑如下

解设A先创建的,有如下的过程

0. createset放置a正在创建中
1. 初始化A->zxcMap<a,lambame>
2. 填充A的属性,从单例池获取B
3. 填充A的属性,从单例池获取C
4. bean后置处理器的前置方法
5. bean后置处理器的后置方法
6. 放到单例池

当在添加A中的b属性时又会触发B的创建

2.0  createset放置b正在创建中
2.1. 初始化B                                        
2.2. 填充B的属性,从单例池获取a->createset是否在创建a->从earlySingletonObjects->zxcMap<a,lambame>->放到earlySingletonObjects中
2.3. bean后置处理器的前置方法
2.4. bean后置处理器的后置方法
2.5. 放到单例池

zxcMap<a,lambame>的表达式逻辑如下

lambame逻辑如下

if(a需要aop) 则返回a的代理对象
else 则返回a的原始对象

所以Map<String, ObjectFactory<?>> singletonFactories就提现出三级map的作用了,而二级map的earlySingletonObjects的内容就来源于三级singletonFactories的判定,这样就清晰多了,那么最后的数据结构earlyProxyReferences有什么作用呢?如下

earlyProxyReferences的作用

正常bean的aop逻辑是在bean后置处理器的后置方法进行实现的,如果在发生循环依赖的时候进行了aop

lambame逻辑如下

if(a需要aop) 则返回a的代理对象,earlyProxyReferences.add(对象)
else 则返回a的原始对象

那么在

bean后置处理器的时候,如果earlyProxyReferences.contains(对象),那么就不需要进行再次aop
防止对同一个对象进行了多次aop处理

这样一来,spring的循环依赖就解决了

spring中的getSingle方法

这是5.2.8的源码,不同版本可能有不同的实现

@Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        //先从单例池获取
        Object singletonObject = this.singletonObjects.get(beanName);
        //如果单例池没有并且bean正在创建
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            //加锁
            synchronized (this.singletonObjects) {
                //再次从单例池获取对象
                singletonObject = this.earlySingletonObjects.get(beanName);
                //如果还是为空并且支持循环依赖
                if (singletonObject == null && allowEarlyReference) {
                    //从三级缓存中获取表达式
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        //调用方法,这里返回的可能是原始对象也可能是经过aop的代理对象
                        singletonObject = singletonFactory.getObject();
                        //放到二级缓存中。以便于上面说到C的对象中进行使用
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        //使用完了就进行移除,防止出现bug或者内存占用
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

spring解决不了的情况

有以下几种情况spring没有办法直接解决循环依赖问题

如果a和b都是原型的,那么是无法解决的

如果a和b只有一个原型,一个单例那么是可以解决的

如果a和b的构造器相互依赖对方,那只能通过@Lazy注解来解决,标记在任何一个上面即可

如果a自依赖自己那么也是可以解决的

总结

至此spring的循环依赖就说完了,其实也不能说很复杂,核心的点就是那几个东西,多看几遍就记住了

<think> 首先,用户查询的是“Spring循环依赖问题的产生原因及解决方案”。我需要基于提供的站内引用信息来回答。 回顾系统级指令: - 所有行内数学表达式使用$...$格式(但在这个主题中,可能没有数学表达式)。 - 独立公式使用$$...$$格式(同样,可能不需要)。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户提供了多个引用: - [^1]: 讨论了Spring bean的完整生命周期,以及循环依赖问题。 - [^2]: Spring通过三级缓存解决单例Bean属性注入的循环依赖问题。 - [^3]: 循环依赖的产生与业务场景相关,例如用户服务和订单服务之间的依赖。 - [^4]: 详细解释了三级缓存:一级缓存存储完整的Bean,二级缓存存储实例化但未初始化的Bean,三级缓存存储工厂。 - [^5]: 提到在populateBean阶段注入循环依赖的bean可能会导致问题。 我的回答需要结构化: - 逐步解决问题:先解释原因,再讨论解决方案。 - 保证真实可靠,基于引用信息。 关键点: 1. **产生原因**: - 循环依赖是指两个或多个Bean相互依赖对方的实例。 - 引用[^3]提到,常见于业务场景,如用户服务和订单服务:用户服务依赖订单服务查询订单,订单服务依赖用户服务获取用户信息。 - 在Spring中,当Bean在创建过程中需要注入另一个Bean,而那个Bean又需要第一个Bean时,就会发生循环依赖。 2. **解决方案**: - Spring通过三级缓存机制解决单例Bean的属性注入循环依赖。 - 引用[^2]和[^4]详细描述了这个机制: - 一级缓存 (singletonObjects):存储完全初始化的Bean。 - 二级缓存 (earlySingletonObjects):存储已实例化但未初始化的Bean(尚未属性注入)。 - 三级缓存 (singletonFactories):存储Bean工厂,用于创建Bean的早期引用。 - 过程:当创建Bean A时,它需要注入Bean B。Spring先创建A的早期版本(放入三级缓存),然后尝试注入B。如果B也需要A,Spring可以从三级缓存获取A的早期版本注入到B中,避免无限循环。 - 引用[^4]解释了缓存如何配合:工厂生成的Bean放入二级缓存,最终完成初始化后放入一级缓存。 - 引用[^5]提醒,在populateBean阶段注入依赖时,如果缓存中没有,可能会导致重新创建实例,但三级缓存机制防止了这一点。 结构化回答: - 引言:简述Spring循环依赖及其重要性。 - 产生原因: - 解释什么是循环依赖。 - 举例业务场景。 - 基于引用[^3]。 - 解决方案: - 介绍三级缓存机制。 - 详细描述每个缓存的作用。 - 解释解决过程。 - 基于引用[^2]、[^4]。 - 注意事项:引用[^2]提到,尽管Spring可以解决,但应通过设计避免循环依赖。 生成相关问题: - 在回答末尾,添加相关问题,如指令要求。 - 示例问题: 1. 三级缓存机制的具体工作流程是怎样的? 2. Spring如何处理非单例Bean的循环依赖? 3. 在实际开发中,如何避免循环依赖? 4. 循环依赖对性能有什么影响? 添加引用标识: - 在回答中,当引用具体信息时,自然地添加[^n]在句子末尾。 - 例如:“循环依赖的产生通常与具体业务场景有关[^3]。” 最终回答结构: 1. 介绍Spring循环依赖问题。 2. 产生原因。 3. 解决方案(三级缓存机制)。 4. 结论和建议。 5. 相关问题。 </think> ### Spring循环依赖问题的产生原因及解决方案 Spring框架中的循环依赖问题是指两个或多个Bean在创建过程中相互依赖对方的实例,导致初始化陷入死循环的情况。理解其原因和解决方案有助于优化应用设计和避免运行时错误。下面我将基于Spring的工作原理,逐步解释。 #### 1. **产生原因** - **核心原因**:循环依赖源于Bean之间的直接或间接相互引用。在Spring IoC容器初始化Bean时,如果Bean A依赖Bean B,而Bean B又依赖Bean A,容器在实例化过程中无法完成依赖注入,形成无限递归。这通常发生在: - **属性注入(Setter或Field注入)**:当Bean通过属性注入方式引用另一个Bean时,如果两个Bean互相引用,Spring在创建阶段就会卡住。 - **构造函数注入**:构造函数注入的循环依赖无法被Spring自动解决(因为容器必须在实例化时完成所有依赖注入)。 - **业务场景驱动**:循环依赖的产生常与具体业务逻辑相关。例如,在电商系统中: - 用户服务(UserService)需要调用订单服务(OrderService)来查询用户订单列表。 - 订单服务(OrderService)需要调用用户服务(UserService)来获取用户详细信息进行订单分类处理\[^3]。 - 根本原因在于Spring的Bean生命周期:Bean的创建分为实例化、属性注入和初始化三个阶段。如果Bean在属性注入阶段需要另一个尚未创建完毕的Bean,而那个Bean又回指回来,就会触发循环依赖问题\[^1][^5]。 #### 2. **解决方案** Spring通过**三级缓存机制**巧妙解决单例Bean(Singleton Scope)的属性注入循环依赖问题。核心思想是“提前暴露Bean的早期引用”,即在Bean尚未初始化完成时,就提供一个可用的引用供其他Bean使用。三级缓存的具体结构和工作流程如下: - **三级缓存的定义**: - **一级缓存(singletonObjects)**:存储完全初始化的Bean(已完成实例化、属性注入和初始化)。这是最终可用的Bean\[^4]。 - **二级缓存(earlySingletonObjects)**:存储仅完成实例化但未进行属性注入的Bean(即“半成品”Bean)。这些Bean可以被其他Bean临时引用\[^4]。 - **三级缓存(singletonFactories)**:存储一个工厂对象(ObjectFactory),该工厂可以生成Bean的早期引用。这样做的目的是延迟Bean的创建过程,避免直接暴露未完成的Bean\[^2][^4]。 - **解决流程**(以Bean A和Bean B相互依赖为例): 1. **创建Bean A**:Spring先实例化Bean A(但未注入属性和初始化),并将其工厂放入三级缓存。 2. **发现依赖**:在Bean A的属性注入阶段,它需要Bean B。 3. **创建Bean B**:Spring尝试创建Bean B,实例化后放入三级缓存。 4. **Bean B的依赖**:Bean B在属性注入阶段需要Bean A。 5. **获取早期引用**:Spring从三级缓存中获取Bean A的工厂,生成一个早期引用(此时Bean A还未初始化),并将该引用放入二级缓存。然后注入到Bean B中。 6. **完成Bean B**:Bean B完成属性注入和初始化后,放入一级缓存。 7. **完成Bean A**:Spring将Bean B注入Bean A,Bean A完成初始化,移入一级缓存\[^2][^4]。 - 这个过程确保即使Bean未完全初始化,也能通过二级缓存提供引用,打破循环链条。 - **关键优势**: - **只适用于单例Bean**:三级缓存机制专为Singleton Bean设计,确保在整个应用中只有一个实例。 - **避免重新创建**:通过提前暴露引用,防止在`populateBean`(属性注入阶段)时因依赖缺失而重新创建Bean实例(这可能导致额外开销或不一致)\[^5]。 - **不支持构造函数注入**:如果循环依赖发生在构造函数中,Spring无法解决(推荐改用Setter注入或重构设计)\[^2]。 - **实际代码示例**(简化版说明): ```java // Bean A 依赖 Bean B public class BeanA { private BeanB beanB; // Setter注入 public void setBeanB(BeanB beanB) { this.beanB = beanB; } } // Bean B 依赖 Bean A public class BeanB { private BeanA beanA; // Setter注入 public void setBeanA(BeanA beanA) { this.beanA = beanA; } } ``` Spring的三级缓存会在`BeanA`实例化后提供一个早期引用给`BeanB`,从而解决依赖\[^4]。 #### 3. **注意事项与最佳实践** - **局限性**:三级缓存只解决属性注入的单例循环依赖。对于原型(Prototype)Bean或多线程环境,Spring无法自动解决循环依赖开发者需通过设计重构避免。 - **设计建议**:尽管Spring提供了解决方案,但过度依赖三级缓存可能导致代码可维护性下降。推荐通过以下方式预防循环依赖: - 重构业务逻辑,例如使用接口解耦或引入第三方服务类。 - 优先使用Setter注入而非构造函数注入\[^2][^3]。 - 监控工具检测:在开发阶段使用Spring工具(如`@Lazy`注解延迟加载)来识别潜在循环\[^1]。 - 总之,Spring的三级缓存是其IoC容器的核心智慧,但良好的设计始终是首选解决方案\[^2][^3]。 ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值