关于我对Spring循环依赖的思考(一)

简介: 关于我对Spring循环依赖的思考

前言

在今天,依然有许多人对循环依赖有着争论,也有许多面试官爱问循环依赖的问题,更甚至是在Spring中只问循环依赖,在国内,这彷佛成了Spring的必学知识点,一大特色,也被众多人津津乐道。而我认为,这称得上Spring框架里众多优秀设计中的一点污渍,一个为不良设计而妥协的实现,要知道,Spring整个项目里也没有出现循环依赖的地方,这是因为Spring项目太简单了吗?恰恰相反,Spring比绝大多数项目要复杂的多。同样,在Spring-Boot 2.6.0 Realease Note中也说明不再默认支持循环依赖,如要支持需手动开启(以前是默认开启),但强烈建议通过修改项目来打破循环依赖。

本篇文章我想来分享一下关于我对循环依赖的思考,当然,在这之前,我会先带大家温故一些关于循环依赖的知识。

依赖注入

由于循环依赖是在依赖注入的过程中发生的,我们先简单回顾一下依赖注入的过程。

案例:

@Component
public class Bar {
}
@Component
public class Foo {
    @Autowired
    private Bar bar;
}
@ComponentScan(basePackages = "com.my.demo")
public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        context.getBean("foo");
    }
}

以上为一个非常简单的Spring入门案例,其中Foo注入了Bar, 该注入过程发生于context.getBean("foo")中。

过程如下:

1、通过传入的"foo", 查找对应的BeanDefinition, 如果你不知道什么是BeanDefinition,那你可以把它理解成封装了bean对应Class信息的对象,通过它Spring可以得到beanClass以及beanClass标识的一些注解。

2、使用BeanDefinition中的beanClass,通过反射的方式进行实例化,得到我们所谓的bean(foo)。

3、解析beanClass信息,得到标识了Autowired注解的属性(bar)

4、使用属性名称(bar),再次调用context.getBean('bar'),重复以上步骤

5、将得到的bean(bar)设值到foo的属性(bar)中

以上为简单的流程描述

什么是循环依赖

循环依赖其实就是A依赖B, B也依赖A,从而构成了循环,从以上例子来讲,如果bar里面也依赖了foo,那么就产生了循环依赖。

image-20220528105342065

Spring是如何解决循环依赖的

getBean这个过程可以说是一个递归函数,既然是递归函数,那必然要有一个递归终止的条件,在getBean中,很显然这个终止条件就是在填充属性过程中有所返回。那如果是现有的流程出现Foo依赖Bar,Bar依赖Foo的情况会发生什么呢?

1、创建Foo对象

2、填充属性时发现Foo对象依赖Bar

3、创建Bar对象

4、填充属性时发现Bar对象依赖Foo

5、创建Foo对象

6、填充属性时发现Foo对象依赖Bar....

foo_bar

很显然,此时递归成为了死循环,该如何解决这样的问题呢?

添加缓存

我们可以给该过程添加一层缓存,在实例化foo对象后将对象放入到缓存中,每次getBean时先从缓存中取,取不到再进行创建对象。

缓存是一个Map,key为beanName, value为Bean,添加缓存后的过程如下:

1、getBean('foo')

2、从缓存中获取foo,未找到,创建foo

3、创建完毕,将foo放入缓存

4、填充属性时发现Foo对象依赖Bar

5、getBean('bar')

6、从缓存中获取bar,未找到,创建bar

7、创建完毕,将bar放入缓存

8、填充属性时发现Bar对象依赖Foo

9、getBean('foo')

10、从缓存中获取foo,获取到foo, 返回

11、将foo设值到bar属性中,返回bar对象

12、将bar设置到foo属性中,返回

以上流程在添加一层缓存之后我们发现确实可以解决循环依赖的问题。

多线程出现空指针

你可能注意到了, 当出现多线程情况时,这一设计就出现了问题。

我们假设有两个线程正在getBean('foo')

1、线程一正在运行的代码为填充属性,也就是刚刚将foo放入缓存之后

2、线程二稍微慢一些,正在运行的代码是:从缓存中获取foo

此时,我们假设线程一挂起,线程二正在运行,那么它将执行从缓存中获取foo这一逻辑,这时你就会发现,线程二得到了foo,因为线程一刚刚将foo放入了缓存,而且此时foo还没有被填充属性!

如果说线程二得到这个还没有设值(bar)的foo对象去使用,并且刚好用了foo对象里面的bar属性,那么就会得到空指针异常,这是不能为允许的!

那么我们又当如何解决这个新的问题呢?

加锁

解决多线程问题最简单的方式便是加锁。

我们可以在【从缓存获取】前加锁,在【填充属性】后解锁

如此,线程二就必须等待线程一完成整个getBean流程之后才在缓存中获取foo对象。

我们知道加锁可以解决多线程的问题,但同样也知道加锁会引起性能问题。

试想,加锁是为了保证缓存里的对象是一个完备的对象,但如果当缓存里的所有对象都是完备的了呢?或者说有部分对象已经是完备了的呢?

假设我们有A、B、C三个对象

1、A对象已经创建完毕,缓存中的A对象是完备的

2、B对象还在创建中,缓存中的B对象有些属性还没填充完毕

3、C对象还未创建

此时我们想要getBean('A'), 那我们应该期望什么?我们是否期望直接从缓存中获取到A对象返回?或者还是等待获取锁之后才能得到A对象?

很显然我们更加期望直接获取到A对象返回就可以了,因为我们知道A对象是完备的,不需要去获取锁。

但以上的设计也很显然无法达到该要求。


目录
相关文章
|
29天前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
13天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
30 2
|
3月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
223 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
2月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
66 1
|
3月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
45 4
|
4月前
|
存储 缓存 Java
面试问Spring循环依赖?今天通过代码调试让你记住
该文章讨论了Spring框架中循环依赖的概念,并通过代码示例帮助读者理解这一概念。
面试问Spring循环依赖?今天通过代码调试让你记住
|
4月前
|
缓存 Java Spring
spring如何解决循环依赖
Spring框架处理循环依赖分为构造器循环依赖与setter循环依赖两种情况。构造器循环依赖不可解决,Spring会在检测到此类依赖时抛出`BeanCurrentlyInCreationException`异常。setter循环依赖则通过缓存机制解决:利用三级缓存系统,其中一级缓存`singletonObjects`存放已完成的单例Bean;二级缓存`earlySingletonObjects`存放实例化但未完成属性注入的Bean;三级缓存`singletonFactories`存放创建这些半成品Bean的工厂。
|
4月前
|
Java Spring 容器
循环依赖难破解?Spring Boot神秘武器@RequiredArgsConstructor与@Lazy大显神通!
【8月更文挑战第29天】在Spring Boot应用中,循环依赖是一个常见问题。当两个或多个Bean相互依赖形成闭环时,Spring容器会陷入死循环。本文通过对比@RequiredArgsConstructor和@Lazy注解,探讨它们如何解决循环依赖问题。**@RequiredArgsConstructor**:通过Lombok生成包含final字段的构造函数,优先通过构造函数注入依赖,简化代码但可能导致构造函数复杂。**@Lazy**:延迟Bean的初始化,直到首次使用,打破创建顺序依赖,增加灵活性但可能影响性能。根据具体场景选择合适方案可有效解决循环依赖问题。
151 0
|
5月前
|
缓存 Java 开发者
Spring循环依赖问题之Spring循环依赖如何解决
Spring循环依赖问题之Spring循环依赖如何解决
|
4月前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作