看一遍就理解:Spring 的循环依赖问题!

spring的循环依赖问题,大家应该司空见惯了吧~ 这是一个非常经典的问题

  • 什么是循环依赖?

  • 循环依赖就一定是问题嘛?

  • spring 的循环依赖一定有问题嘛?

  • spring 是如何解决循环依赖问题的呢?

  • 为什么一定要三级缓存?二级缓存行不行?

1. 什么是循环依赖?

循环依赖指两个或多个Bean之间互相依赖形成闭环。比如:

@Service
public class BeanA {
    @Autowired
    private BeanB beanB;
}

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

像这种,A依赖B,而B又反过来依赖于A,就是典型的循环依赖。

2.循环依赖就一定是问题嘛?

循环依赖一定就有问题嘛?

其实不一定的。比如这个例子:

//类A依赖了属性b
class A{
 public B b;
}

//类B依赖了属性a
class B{
 public A a;
}

这个也算是循环依赖,但是它却不是问题~ 我们可以正常创建的:

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

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

3. spring 的循环依赖一定有问题嘛?

我们聊得比较多的,都是spring的循环依赖。

那么,spring的循环依赖一定会有问题嘛?其实不是的,比如回答这个例子:

@Service
public class BeanA {
    @Autowired
    private BeanB beanB;
}

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

spring容器启动运行,并不会报错!那就是不存在spring的循环依赖问题嘛?

不是的,这个例子,之所有不报错,是因为Spring 默认情况下,(单例 + setter 注入)的循环依赖问题,已经解决了。 但是呢,构造器注入循环依赖,还是有问题的。

我们看这个例子,构造器注入:

@Service
public class ServiceA {
    private final ServiceB serviceB;
    // 构造器注入 ServiceB
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;

    // 构造器注入 ServiceA
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

启动后,马上的就报错了:

Description:

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

┌─────┐
|  serviceA defined in file [SpringCircularDependency\target\classes\com\example\springcirculardependency\service\ServiceA.class]
↑     ↓
|  serviceB defined in file SpringCircularDependency\target\classes\com\example\springcirculardependency\service\ServiceB.class]
└─────┘

图片

为什么构造器注入循环依赖无法解决?

其实, 字段注入 /setter 注入的方式,通过提前暴露到三级缓存,来解决循环依赖问题啦~

  • 字段注入 /setter 注入的流程是:实例化 Bean→提前暴露到三级缓存→注入依赖(依赖可通过缓存获取)。

而构造器注入的流程是:需要依赖才能实例化 Bean,此时三级缓存中还没有任何关于该 Bean 的信息,无法提前暴露,因此它的循环依赖问题还存在~~

4. setter 注入方式,spring是如何解决循环依赖问题的呢?

我们提到,setter 注入,通过暴露三级缓存来解决spring三级缓存的问题,那么,它是如何解决的呢?

4.1 三级缓存作用

Spring 在DefaultSingletonBeanRegistry中定义了三个关键缓存(本质是 HashMap),它们是处理循环依赖的核心:

// 一级缓存:存储完全初始化完成的单例Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:存储提前暴露的“半成品”Bean(已实例化但未初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存:存储Bean的实例工厂,用于生成半成品Bean
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

三级缓存的作用

  • singletonObjects:最终可用的 “成品” Bean,已完成属性注入和初始化

  • earlySingletonObjects:临时存储的 “半成品” Bean,仅完成实例化(new 出对象),未完成属性注入和初始化

  • singletonFactories:存储生成半成品 Bean 的工厂,用于在需要时提前暴露 Bean

4.2 循环依赖处理流程

假设,以 A 依赖 B、B 依赖 A 为例,看看 Spring 如何通过三级缓存解决循环依赖~

1.A 的创建流程
  • 步骤 1:Spring 尝试创建 A,先检查一级缓存(无),标记 A 为 “正在创建”

  • 步骤 2:实例化 A(执行new ServiceA()),此时 A 是半成品(无属性值)

  • 步骤 3:将 A 的工厂放入三级缓存:

DefaultSingletonBeanRegistry.addSingletonFactory
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory); // 放入三级缓存
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}
  • 步骤 4:给 A 注入属性时发现依赖 B,暂停 A 的创建,转去创建 B

2. B 的创建与循环依赖解决
  • 步骤 1:创建 B,同样实例化后放入三级缓存,准备注入属性时发现依赖 A

  • 步骤 2:查询 A 的缓存:一级缓存无→二级缓存无→从三级缓存获取 A 的工厂,通过工厂生成 A 的半成品,放入二级缓存,删除三级缓存的 A 工厂

  • 步骤 3:B 成功注入 A 的半成品,完成初始化后放入一级缓存

  • 步骤 4:回到 A 的创建流程,从一级缓存获取 B 的成品,注入 A 中

  • 步骤 5:A 完成初始化,放入一级缓存,删除二级缓存的 A 半成品

我们可以看下关键源码,这段缓存查询逻辑:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先查一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    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) {
                    singletonObject = singletonFactory.getObject(); // 生成半成品
                    this.earlySingletonObjects.put(beanName, singletonObject); // 放入二级缓存
                    this.singletonFactories.remove(beanName); // 移除三级缓存
                }
            }
        }
    }
    return singletonObject;
}

5. 为什么一定要三级缓存?二级缓存行不行?

、 有些伙伴可能会有疑问,直接用二级缓存存储半成品 Bean 不行吗?

原因是AOP 代理。如果 A 需要被代理,三级缓存的工厂会生成代理对象而非原始对象。假设直接存原始对象到二级缓存,后续代理会导致注入的对象不一致。通过工厂延迟生成代理对象,保证了循环依赖注入的是最终代理对象。

### Spring 框架中的环形依赖解决方案及其工作原理 #### 理解环形依赖的概念 当两个或多个bean相互之间存在直接或间接的依赖关系时,就会形成环形依赖。例如A依赖于B,而B又反过来依赖于A。这种情况下如果不加以处理,则可能导致无限递归调用最终引发栈溢出错误。 #### Spring 处理环形依赖的工作机制 对于单例作用域内的bean,在默认情况下Spring容器能够自动检测并尝试解决简单的环形单例bean之间的依赖问题。具体来说: - 当遇到第一个被请求创建的对象(比如`Bean A`)时,如果发现它有未满足的属性注入需求指向另一个尚未完成初始化过程的目标对象(`Bean B`); - 此刻并不会立即去真正构建那个目标对象而是先返回给前者一个早期暴露出来的半成品版本——即提前曝光(early exposure)状态下的代理实例; - 接着继续按照正常的流程来组装剩余部分直至整个生命周期结束得到完整的`Bean A`; - 同样的逻辑也适用于后续其他涉及相同链条上的组件直到全部装配完毕为止[^2]。 #### 提前曝光与三级缓存机制 为了实现上述功能,Spring内部维护了一个所谓的“三级缓存”,其中就包含了用于应对可能出现的循环引用情况的数据结构: 1. **一级缓存** (`singletonObjects`) :存储已经完全初始化好的单例bean。 2. **二级缓存** (`earlySingletonObjects`) : 存放处于正在创建过程中但是可以安全使用的临时占位符(proxy placeholder),也就是前面提到过的那种预发布版/半成品形态; 3. **三级缓存**( `singletonFactories`):记录了从工厂方法获取到实际对象之前的中间产物,以便将来可能需要用到的时候可以直接转换成真正的实体而不必重新走一遍全流程[^4]。 一旦某个bean成功完成了自己的构造函数执行以及所有前置处理器(post-processors)的操作之后,便会正式加入到最上层的一级缓存里供外部访问使用;与此同时也会清理掉对应位置处存在于另外两层缓冲区里的残留数据项以防混淆视听造成不必要的麻烦。 #### 如何预防和解决问题 尽管Spring提供了一套相对完善的内置机制用来缓解大部分常规场景下所面临的挑战,但在某些特殊情形中仍然可能会碰到无法绕过的技术壁垒。此时就需要开发者采取额外措施来进行干预调整,常见的做法包括但不限于改变bean的作用范围(scope),利用懒加载(lazy initialization)特性延迟初始化时间点,或是重构代码减少不必要的双向关联程度等等。 ```java // 示例:通过设置lazy-init="true" 属性开启懒加载模式 <bean id="exampleBean" class="com.example.ExampleClass" lazy-init="true"/> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值