循环依赖

128 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

循环依赖

大部分人只是将IOC容器实现成一个“存储Bean的map”,将DI实现成“通过注解+反射将bean赋给类中的field”。

很多人都忽视了DI的依赖调解的功能。而帮助我们进行依赖调解本身就是我们使用IOC+DI的一个重要原因。

Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的。

解决单例bean循环依赖

  • singletonObjects:一级缓存,存放的是实例化和初始化都完成了的单例对象,可以直接使用。
  • earlySingletonObjects:二级缓存,里面存放的是半成品(实例化,但是还未初始化),用来解决对象创建过程中的循环依赖问题
  • singletonFactories:三级缓存,里面存放的是要被实例化的对象的对象工厂,去实例化未初始化的对象。

初始化对象的属性,首先去单例池中查找,如果单例池中没有则到二级缓存中去找,二级缓存中也没有,就判断是否出现了循环依赖。如果出现了循环依赖,实际上不会去判断是否需要AOP,直接去三级缓存中查找,拿到一个lambda表达式(包含原始对象以及对象名称等信息),在执行lambda的过程中,才会去判断是否需要AOP,如果需要AOP则返回代理对象(Bean调用构造函数进行实例化后,即使属性还未填充,就可以通过三级缓存向外提前暴露依赖的引用值(提前曝光),根据对象引用能定位到堆中的对象,其原理是基于Java的引用传递),否则返回原始对象,放到二级缓存中,再从三级缓存中移除掉(因为对象是单例的,如果不移除的话,会有bug,导致重复产生代理对象。),完全初始化之后将自己放入到一级缓存中供其他使用。

一个对象只会出现了二级缓存或者三级缓存中,不能两个缓存都存在,缓存HashMap加锁。

我们假设现在有这样的场景AService依赖BService,BService依赖AService

  1. AService首先实例化,实例化通过ObjectFactory半成品暴露在三级缓存中

  2. 填充属性BService,发现BService还未进行过加载,就会先去加载BService

  3. 再加载BService的过程中,实例化,也通过ObjectFactory半成品暴露在三级缓存

  4. 填充属性AService的时候,这时候能够从三级缓存中拿到半成品的ObjectFactory

拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果bean被AOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例。

这时我们会发现能够拿到bean实例(属性未填充),然后从三级缓存移除,放到二级缓存earlySingletonObjects中,而此时B注入的是一个半成品的实例A对象,不过随着B初始化完成后,A会继续进行后续的初始化操作,最终B会注入的是一个完整的A实例,因为在内存中它们是同一个对象

如果只有一级缓存和三级缓存,我每次从三级缓存中拿到singleFactory对象,执行getObject()方法又会产生新的代理对象,这是不行的,因为AService是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了singleFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍singletonFactory.getObject()方法再产生一个新的代理对象,保证始终只有一个代理对象。

既然singleFactory.getObject()返回的是代理对象,那么注入的也是经过CGLIB代理的AService对象

构造器循环依赖

因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象说明:一种互斥的关系而非层次递进的关系,故称为三个Map而非三级缓存的缘由 完成注入;

不使用代理和用代理流程一样吗

aop情况中,注入到其他的bean的不是最终代理对象,首先明确AOP的实现是通过 postBeanProcess后置处理器,在初始化之后做代理操作的。Spring对象的加载流程:实例化,设置属性,初始化,AOP增强。如果不使用三级缓存直接使用二级缓存的话,会导致所有的Bean在实例化后就要完成AOP代理

为什么需要三级缓存

当某个 bean 进入到 2 级缓存的时候,是半成品还未完全创建好已经被别人拿去使用了,所以必须要有 3 级缓存,2 级缓存中存放的是早期的被别人使用的对象,如果没有 2 级缓存,是无法判断这个对象在创建的过程中,是否被别人拿去使用了。

3级缓存是为了解决一个非常重要的问题:早期被别人拿去使用的 bean 和最终成型的 bean 是否是一个 bean,如果不是同一个,则会产生异常!

三级缓存是为了判断循环依赖的时候,早期暴露出去已经被别人使用的 bean 和最终的 bean 是否是同一个 bean,如果不是同一个则弹出异常,如果早期的对象没有被其他 bean 使用,而后期被修改了,不会产生异常,如果没有三级缓存,是无法判断是否有循环依赖,且早期的 bean 被循环依赖中的 bean 使用了。

tips

  1. 不要对有@Configuration注解的配置类进行Field级的依赖注入。
  2. Spring Boot 2.6.0已经默认禁止了循环依赖

开启设置:spring.main.allow-circular-references=true

在 Java 语言中只有值传递,方法传参时只会传递副本信息而非原内容。且基础数据类型会直接生成到栈上,而对象或数组则会在栈和堆上都生成信息,并将栈上生成的引用,直接指向堆中生成的数据

为什么要销毁bean

手动销毁需要调用容器的close方法进行销毁

自动销毁是向jvm注册一个钩子线程,当容器进行关闭的时候会自动调用销毁的钩子线程进行销毁

单实例bean在容器创建完成前会进行创建并初始化,在容器销毁的时候进行销毁。多实例bean(scope=prototype)在第一次获取该bean实例时才会创建并初始化,且容器不负责该bean的销毁(手动调用销毁)。

在tomcat容器加载时会将所有单例的bean实例化并且加入到HashMap中。在之后需要单例bean之时直接从hashmap中取。如果hashmap中没有则从spring容器中实例化并且将其放入haspmap,而非单例bean是不会被放入hashmap中只会从spring容器中加载。