Spring源码篇(十三)循环依赖

文章目录

  • 什么是循环依赖
  • 解决方法
  • 源码
    • [1. 实例化](#1. 实例化)
    • [2. 加入第三级缓存](#2. 加入第三级缓存)
    • [3. 依赖注入](#3. 依赖注入)
    • [4. 递归并加入第二缓存](#4. 递归并加入第二缓存)
    • [5. 回到上层依赖注入](#5. 回到上层依赖注入)
    • 逻辑图
  • 问题

什么是循环依赖

假设你有一个Spring项目,然后你创建了类AService,和BService,他们的关系是这样的

java 复制代码
@Component
public class AService {

    @Autowired
    private BService bService;
}


@Component
public class BService {

    @Autowired
    private AService aService;
}

他们相互依赖,根据我们之前说的Spring机制,在AService实例化和初始化时需要进行注入,也就是getBean创建BService,同时BService也同样做了相同的事,就会形成循环依赖。

我们退一步讲,不用Spring,正常的一个项目中,这样写,是不是就有问题。

解决方法

但是,你在项目中并没有遇到,或者你无意间粗心的写出了这样的代码,但是项目依然正常运行,甚至你都没发现,这是因为Spring针对这个场景做了设计。

Spring中他采用了三级缓存来解决这个问题。

一级缓存:单例池singletonObjects

二级缓存:半成品单例earlySingletonObjects

三级缓存:单例工厂singletonFactories

这个原理,其实不难,可以理解为对象引用的操作。即当AService实例化后, 将当前实例对象放入缓存(这里我们不管几级缓存),这时还没有初始化,然后初始化时,依赖注入getBean("BService"),这时BService要注入AService,它就从缓存中取,缓存中有这个实例化好的AService,然后它注入到BService中,同样也放入缓存,返回实例对象,然后回到AService注入的过程,它已经可以拿到BService实例对象了,然后再设置到属性中。

就是这样的一个引用原理,在Spring中它用了三级缓存来解决。

它的一个简易流程如下:

  1. 实例化A
  2. 放入第三级缓存
  3. 依赖注入B,B没有
  4. 实例化B
  5. 放入第三级缓存
  6. 依赖注入A,A没有
  7. 从三级缓存取出A
  8. 将A放入第二级缓存,并从第三级移除
  9. 注入B
  10. 返回B,注入A

其实,这个我感觉两级缓存就可以解决循环依赖了,去掉第三级缓存:

  1. 实例化A
  2. 放入第二级缓存
  3. 依赖注入B,B没有
  4. 实例化B
  5. 放入第二级缓存
  6. 依赖注入A,A没有
  7. 从二级缓存取出A
  8. 注入B
  9. 返回B,注入A

源码

缓存管理类:DefaultSingletonBeanRegistry

单例的管理都由他管理,那么注入的逻辑都会涉及这个类,可以先了解一下。

接下来,我们开始循环依赖解决的源码流程。

1. 实例化

位置:org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

这里实例化AService

这一段,是从缓存中获取单例Bean的逻辑,可以看到:

  1. 单例池获取
  2. 单例池不存在,且正在创建该Bean时,从二级缓存获取
  3. 二级缓存不存在时,从三级缓存获取,然后存入到二级缓存,同时删除三级缓存

当第一次进入该方法时,第三级缓存singletonFactories也是空的,所以,第一次走这个方法是返回null,这不重要,你可以理解为在真正创建Bean时,先查询缓存,这是正常逻辑,就比如Redis缓存一样。

然后是下面这段:

这段过后才是真正的创建Bean的过程

这段是通过BeanDefinition也就是Bean定义信息,拿到注解@DependsOn信息,也就是依赖的Bean然后做提前实例化,你可以看到里面这行代码:getBean(dep),也就是整个方法本身,就是递归。

这段就是重点,createBean这个方法是创建Bean,但它是被作为一个回调方法参数,该方法主要做验证和添加到单例池,真正的创建是createBean,信息并不多,自行查看

位置:org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)

重点看createBean,位置:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])

位置:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

2. 加入第三级缓存

这个populateBean就是依赖注入的方法,里面是xml、注解方式的注入逻辑,而initializeBean是初始化bean的必要操作,如执行Aware、@PostConstruct等方法。

addSingletonFactory就是加入第三级缓存的地方,我们可以看一下这个方法:

注意,这里只是添加第三级缓存,同时也移除了第二级缓存,AService已经在第三级缓存了。

3. 依赖注入

循环依赖是发生在依赖注入里的,所以我们进入这个方法

位置:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean

我们直接看这一段

注解@Autowired、@Resource就是在这里执行的,分别通过AutowiredAnnotationBeanPostProcessor``CommonAnnotationBeanPostProcessor两个处理器执行。

这个方法一共两行,第一行,拿到要要注入的属性元数据对象,第二行,就是执行注入。

这里他是遍历当前Bean下的注入点,这些注入点就是要注入的属性对象,这里就是BService

位置:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject

最后走到这步:

然后是:org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency

最后就是走到这,前面是验证和获取属性的类型,然后是:

这里它传入了要注入的属性beanName和类型,但是实际上,它只是通过BeanName去查找,那么重点是beanFactory.getBean(beanName);

这个方法,他是统一的,同一个,所以这里是递归。

4. 递归并加入第二缓存

那么从现在开始,又回到第一步,然后再次执行:

但是,这里同样和第一次一样,并没有缓存,然后直到走到依赖注入populateBean,然后inject,要注入AService,最后走到下面这个代码,此时按顺序从一二三级缓存查找,最后找到了AService在实例化之后加入的实例对象,最后加入到第二缓存。

java 复制代码
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 单例池查找bean
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
                // 找不到,且是正在创建的bean,那么就从二级缓存查找
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
                    // 二级缓存找不到,并且是允许提前参考的对象,那么从第三级缓存查找
                    // 第一次A类肯定没有,
                    // A类依赖注入B,B也没有
                    // B类依赖注入A,A有了,因为A实例化之后就加入了第三级缓存
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
                        // 找到之后,将第三级缓存的bean实例放入第二级缓存
						this.earlySingletonObjects.put(beanName, singletonObject);
                        // 移除第三级缓存
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

5. 回到上层依赖注入

然后返回这个方法:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject

通过反射注入

在获取到value之后,他还会做一步,就是校验这个value是否是对的,就是isTypeMatch,根据beanName和类型查找,但实际上的方法,和之前一样,类型作为入参,并没有传递下去,所以,还是通过beanName去查找(getBean

逻辑图

我简单的画了个图

问题

上面我们的流程基本没用到二级缓存,是为什么?

那么这里再引出其他问题:

  1. 代理对象作为属性,该如何做?
  2. 如果去掉第二级缓存,会如何?

我们先考虑第一个问题,假设我们的AService属性是BService,而我们的BService被代理类,我们创建一个代理类:

java 复制代码
@Component
@Aspect
public class Asp {

    @Pointcut("execution(* com.liry.plugin.BService.*(..))")
    public void pointCut() {

    }

    @Before(value = "pointCut()", argNames = "joinPoint")
    public void get(JoinPoint joinPoint) throws Throwable {
        System.out.println("proxy befor");
    }
}

那么,当AService依赖注入的时候,应该注入的是原始的Bean还是代理类?

当项目运行中,直接调用属性,如果是原始bean,它是不会生效,在Spring当中,动态代理,只是通过配置的方式,自动代理,实际的还是需要想传统的代理对象那般,通过代理对象委托给原始Bean执行业务逻辑,所以,AService依赖注入中的BService一定是一个代理对象。

好,如果只有两级缓存,在循环依赖时,出现代理Bean,就直接存入第二级缓存,会如何?

假设AService依赖BServiceBService依赖AServiceAServiceAOP了,但是属性注入时,还是使用原始Bean,但第二级缓存已经放入了代理的AService,那么BService在注入时,可以直接从第二级拿到代理的AService,没问题,反过来,也同样成立。

所以,一开始我会说"我感觉两级缓存就可以解决循环依赖了"。

那如此,为什么要第三级缓存呢?

我有看到说,这是Spring的设计原则,代理对象应该时准保好的Bean,就是对完整Bean的代理,可是,

在刚刚的场景中,A依赖B,B依赖A,当B注入A时,创建了A的代理,放入缓存,实现B的注入,而此时是A的注入过程,还没有结束,初始化没有完成,所以,也不合理。

而且它所以表现出的现象,和二级缓存是一样,都是通过对象引用更新原始Bean

相关推荐
trayvontang3 分钟前
Spring Gateway核心概念、流程及原理
spring·gateway·spring gateway
郝学胜-神的一滴4 分钟前
线程同步:并行世界的秩序守护者
java·linux·开发语言·c++·程序人生
superman超哥5 分钟前
Rust 移动语义(Move Semantics)的工作原理:零成本所有权转移的深度解析
开发语言·后端·rust·工作原理·深度解析·rust移动语义·move semantics
superman超哥16 分钟前
Rust 所有权转移在函数调用中的表现:编译期保证的零成本抽象
开发语言·后端·rust·函数调用·零成本抽象·rust所有权转移
源代码•宸19 分钟前
goframe框架签到系统项目开发(实现总积分和积分明细接口、补签日期校验)
后端·golang·postman·web·dao·goframe·补签
无限进步_24 分钟前
【C语言】堆(Heap)的数据结构与实现:从构建到应用
c语言·数据结构·c++·后端·其他·算法·visual studio
掉鱼的猫25 分钟前
灵动如画 —— 初识 Solon Graph Fluent API 编排
java·openai·workflow
初次攀爬者25 分钟前
基于知识库的知策智能体
后端·ai编程
喵叔哟25 分钟前
16.项目架构设计
后端·docker·容器·.net