spring源码笔记(六) ----spring使用三级缓存解决循环依赖问题

本文主要内容分以下几个方面阐述:

1:什么是循环依赖?

2:什么是三级缓存?每一级缓存中都存储的是什么内容?

3:spring是如何解决循环依赖问题的,为什么使用三级缓存而不是二级缓存来解决循环依赖问题?

 

1:什么是循环依赖?

举个简单实例,当有两个类A类,和B类。

public class A{
    private B b;
}

 

public class B{
    private A a;
}

A类中有一个B类字段,而B类中有一个A类字段。此时,我们要创建一个A类型的对象时,需要为它B类型的字段赋值一个B类型对象。故我们要创建一个B类型对象实例,而想创建一个B类型实例,又需要为它的字段类型为A的字段赋值一个A类型的实例。如此反复,就称之为循环依赖。

当AB两类交付spring进行管理的时候,类中字段的赋值由spring完成,此时,spring就要解决这种循环依赖的问题。如何解决的呢?

spring使用三级缓存解决循环依赖问题。要知道三级缓存的具体应用,我们首先要了解什么是三级缓存。

2:spring中的三级缓存

所谓三级缓存,是spring中的“上帝”类:DefaultListableBeanFactory中的三个(继承自父类)Map类型字段;

我们每启动一个Spring环境,其实就是创建了一个DefaultListableBeanFactory类的实例。它关于三级缓存的三个字段分别是:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);singletonObjects 单例池,用来缓存走完全部创建流程的单例对象,包括已经完成依赖注入,完成各种代理如AOP代理(如果有的话)、BeanPostProcessor代理(如果有的话)的单例对象。

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);  singletonFactories 单例工厂,用来临时缓存一个还没有完成创建的半成品对象。创建流程完成以后,该处缓存将移除Bean,并存储到单例池中。

private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); 早期单例对象,也是用来临时缓存一个还没有完成创建的半成品对象,此处的缓存值只在极特殊场景下会使用到,一般情况下使用上边两个缓存就能解决循环依赖问题。但是要想完全解决循环依赖问题,必须要有三级缓存同时协作

3:spring是如何解决循环依赖问题的

要解释清楚这个问题,我们要有一些预备知识。

      第一个背景知识:Bean实例化流程:

Bean的实例化流程在我的博文spring源码笔记(五)---Bean的实例化流程分析 中有详细解释,建议大家作为背景知识先大概做一个了解。对彻底了解三级缓存的运行机制很有必要。概要来说,有以下几个步骤:

第一步:通过无参构造创建Bean的实例。

第二步:将该Bean对应的工厂对象(注意不是对象本身)存储到singletonFactories 单例工厂缓存中。

第三步:对该Bean中需要注入的属性进行依赖注入。

第四步:依次调用BeanPostProcessor的实现类,前置目标方法, Bean的init方法,依次调用BeanPostProcessor的后置目标方法。(由于spring中的AOP实现也是一个BeanPostProcessor接口的子类,故此处也是的AOP代理时机(不存在循环依赖情况下),循环依赖发生时会有不同);

第五步:将完成依赖注入,并且完成BeanPostProcessor处理的对象,存储到单例池中。

        第二个背景知识:依赖注入会导致Bean的创建流程跳转到其他Bean的创建流程中

这一点在我上边给出的链接的文章中也做了详细的解释,大家请着重看给出的流程图中标星的地方。这里专门拿出来强调,是因为这也是解决循环依赖必须要了解的前提。

在了解了这些背景知识之后,我们开始学习循环依赖的解决。

循环依赖的解决:

我们给出存在循环依赖问题的两个类:

public class Husband {
	private Wife wife;

	public void speak() {
		System.out.println("I`m husband!");
	}

	public Wife getWife() {
		return wife;
	}

	@Autowired
	public void setWife(Wife wife) {
		this.wife = wife;
	}

}
public class Wife {
	private Husband husband;

	public Husband getHusband() {
		return husband;
	}
	
	@Autowired
	public void setHusband(Husband husband) {
		this.husband = husband;
	}
	
	public void speak() {
		System.out.println("I`m wife!");
	}
}

 

 

4:尝试使用一级缓存解决循环依赖问题。

我们用上述两个类举例,尝试仅仅使用一层缓存来解决。这里直接省去两个临时缓存,保留单例池缓存singletonObjects 。下面给出调用流程。

我们假设Husband类先开始创建:

 

可见,使用一层缓存出现循环依赖问题是没有办法解决的。

5:尝试使用两层缓存解决循环依赖问题。

spring中使用最频繁的两层缓存是,工厂缓存objectFactories,和单例池缓存singtonObjects,我们使用这两层缓存尝试解决循环依赖问题。

注意,早期对象缓存earlySingletonObjects 好像从语义上更像是用来存储和获取早期对象的,许多人相当然的认为,最常用的应该是它和单例池缓存singtonObjects,但其实并不是。下面给出流程。

我们依然假设Husband类先开始创建:

 

 

以上为Husband和Wife两个类都不存在AOP代理的情况下, 使用两层缓存解决循环依赖问题。问题可以顺利解决。那么,存在AOP代理的情况下如何呢?是否依然能解决?

是的,如果是单纯是两个类之间相互发生循环依赖。 即使进行了AOP代理,两层循环依然能够解决!

假设在HUsBand类先于Wife类创建的条件下:

如果wife类发生AOP代理,那么流程与上述相同,因为根据前文提到的创建流程,wife类的AOP代理发生在DI之后,创建流程并没有打乱。

关键在于husband类发生AOP代理这种情况,这时候就有问题了!

我们从新走一遍创建流程:1、husband类调用默认构造创建实例;2、工厂类缓存到singletonFactories中,此时从工厂类中可以获取一个没有进行AOP代理的husband实例。3:husband进行DI,4:发现需要wife对象,husband创建流程挂起,转入wife创建流程中。5:wife工厂类缓存到singletonFactories中。6:wife进行DI,需要husband,将没有进行AOP代理的husband实例注入。

7:wife调用BeanPostProcessor;8:wife存储到单例池中。9:为husband进行DI,wife从缓存中取出一个实例,注入husband。10:husband的DI完成,开始调用BeanPostProcessor,由于husband进行了AOP代理。此处将会执行相关逻辑。代理过后的对象,不再是husband类型, 而是动态代理类型了!11:将动态类型的husband存储到单例池中。

发现问题没有,按照单例原则,husband在单例池缓存中,与在wife类的husband字段中应该是同一份,但是现在,husband在单例池中变为了动态类型,而在wife类中依然为没有进行AOP代理的husband!

原因就是,根据创建流程,DI在前,AOP代理在后,wife用未进行代理的对象进行了注入以后,AOP又创建出一个动态代理的husband类!

要解决这个矛盾,就要在发生循环依赖的时候,在wife还未DI之前,将husband类提前进行AOP代理!以保证一致性!DI之前嘛,一定是工厂缓存objectFactories那个阶段喽!

下边方法为spring中  BeanFactory接口的getObject()返回值;使用工厂缓存调用,getObejct()方法返回实例,就是调用如下方法。

通过在工厂缓存做了对husband类的判断,如果进行了AOP代理,那么不要等DI 完成,此处直接返回一个代理对象,这样,wife类接下类注入的husband字段自然也是经过代理的字段了。问题解决。

 

此处可见,通过两层缓存是可以解决上述AOP代理情况下循环依赖问题的。

earlySingletonObjects缓存到底有什么作用?真的是用来提高存取效率的吗?

那么,spring中的 earlySingletonObjects缓存到底有什么作用?上述情况的AOP完全没有用它的情况下也解决了,那它还有存在的意义吗?不会仅仅是用来提高存取速度的吧?

当然不是, earlySingletonObjects用来解决极少数情况下的循环依赖问题的!

下面给出具体场景:

如图中场景所示,Husband又有了一个小老婆!Husband类即与wife循环依赖,又与LeftHandWife循环依赖。

根据上一节流程描述我们知道,当Wife类需要注入husband字段之前,spring对husband缓存的实例进行了提前AOP代理,生成了动态代理对象。用动态代理的husband对wife进行注入。然后一切如平常一样,wife所有创建流程走完。

回到挂起的husband的DI阶段,接下来要干嘛?继续husband的DI呗。

spring一看,呦呵,一个老婆不够,还有一个小老婆在和husband纠缠不清。于是,husband的DI继续挂起,流程来到的小老婆类的创建中。

LeftHandWife,创建默认构造,流程一路向下,来到了LeftHandWife的DI流程中,此时如果还是只有两层缓存,那么spring只能再从工程缓存中取出一个未进行代理的husband。再进行一个代理。这时候,这个动态代理对象husband,与wife类中的husband字段的代理对象必然不是同一个!一个spring家族里边,大小老婆发现自己的老公不是同一个,能不断创建出新的。那还得了!

所有啊,spring一想,看来两层缓存是不够的,当发生了循环依赖,且同时进行了AOP代理。又有不同的地方需要这个经过代理的husband的时候,必须将这个代理过后的husband也进行一下缓存,于是,earlySingletonObjects它来了!

创作不易,转载请注明出处。谢谢大家耐心观看。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring循环依赖是指两个或多个Bean之间存在相互依赖,即A依赖B,B依赖A,或者更多的依赖关系。这种情况下,如果Spring容器无法正确处理循环依赖,就会导致程序出现错误或者无限循环的情况。为了解决这个问题Spring提供了三级缓存机制。 三级缓存机制是Spring在处理循环依赖时采用的一种缓存策略。它包括三个缓存区: 1. singletonObjects:单例对象缓存区,用于存储已经完成实例化的单例Bean对象。 2. earlySingletonObjects:早期单例对象缓存区,用于存储已经完成实例化但还未完成初始化的单例Bean对象。 3. singletonFactories:单例工厂缓存区,用于存储创建Bean实例的工厂对象。 当Spring容器创建一个Bean时,它会首先检查singletonObjects缓存区是否存在该Bean的实例,如果存在则直接返回该实例。如果不存在,则会检查earlySingletonObjects缓存区是否存在该Bean的实例,如果存在则返回该实例,并且将该实例移动到singletonObjects缓存区中。如果早期单例对象缓存区也不存在该实例,那么就需要使用singletonFactories缓存区中的工厂对象来创建一个新的Bean实例,并将该实例放入earlySingletonObjects缓存区中。 在处理循环依赖时,Spring使用三级缓存机制来解决问题。当创建Bean时,如果发现该Bean已经在earlySingletonObjects缓存区中,说明存在循环依赖,此时Spring会返回一个代理对象,该代理对象可以在后续的属性注入过程中正确地处理循环依赖关系。当所有的Bean都创建完成后,Spring会再次遍历所有的代理对象,完成循环依赖的注入过程,从而保证程序的正确性。 总之,Spring三级缓存机制是用来处理循环依赖问题的,它可以保证Bean的正确创建和属性注入过程,避免了程序出现错误或死循环的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值