Spring面试题之循环依赖与三级缓存

Spring的循环依赖问题

  • 循环依赖图形说明:
    Spring面试题之循环依赖与三级缓存_第1张图片

  • 循环依赖文字说明:
    循坏依赖问题产生的原因是,bean对象的创建实际上是细分为实例化,属性填充,初始化。创建A对象的时候,先实例化A对象,b = null,然后进行属性赋值,为A对象的b属性赋值。此时,由于Spring容器的对象默认是单例的,所以会优先去容器中选择b,如果找到了就直接赋值,没找到再创建。创建B对象的过程与创建A对象类似,在进行属性赋值的时候,去容器中查找a,但是没找到,所以创建A。整个程序进入了一个闭环,这就是我们说的循环依赖。

  • 如何解决循环依赖问题?可能大家多多少少都知道spring的三级缓存机制,那么到底什么是三级缓存?

    Spring面试题之循环依赖与三级缓存_第2张图片

  • 从源码中可以看到,一级缓存、二级缓存都是、三级缓存都是Map结构,且一级缓存与三集缓存用的是ConcurrentHashMap<>(),ConcurrentHashMap是一个线程安全的map,这里不做过多解释,有兴趣的可以去看我的这篇文章:JUC并发编程之集合安全问题

    • 一级缓存singletonObjects存放的是完整的bean对象,从该缓存中取出的bean可以直接使用
    • 二级缓存earlySingletonObjects存放的是不完整的对象,即已实例化但是未初始化的bean对象(不能直接使用),用于解决循坏依赖
    • 三集缓存singletonFactories,单例对象工厂的缓存,存放bean工厂对象,用于解决循环依赖,里面有一些lambda表达式。
  • 那么缓存的放置时间和删除时间是在什么时候?

    • 三级缓存:在createBeanInstance之后,调用addSingletonFactory方法,将对象放入三级缓存
      Spring面试题之循环依赖与三级缓存_第3张图片

      细心的同学可能会发现,在执行完addSingletonFactory.put方法之后,还执行了earlySingletonObjects.remove,也就是把对象从二级缓存中删除,为什么二级缓存中也会有对象?现在为什么又要删掉?这里先留下一个小问题。

    • 二级缓存:第一次从三级缓存中确定对象是代理对象还是普通对象的时候,调用getSingleton,从三级缓存中取出对象并放入二级缓存,同时删除三级缓存
      Spring面试题之循环依赖与三级缓存_第4张图片

    • 一级缓存:生成完整对象之后,addSingleton,放到一级缓存,同时删除二三级缓存
      Spring面试题之循环依赖与三级缓存_第5张图片

  • 下面我们通过一些面试题的回答,来解释三级缓存是如何解决循环依赖的

    Spring面试题之循环依赖与三级缓存_第6张图片

    问:Spring是如何解决循环依赖的?回答关键词:三级缓存提前暴露对象aop

    1. 先说明循环依赖问题,即形成闭环的原因:bean对象的创建细分为实例化,属性填充,初始化。
    2. 此时A对象实际上是存在的,只不过是一个只完成了实例化未完成初始化的不完整对象。而解决循坏依赖的核心在于将实例化和初始化分开操作,即提前暴露某个不完整对象的引用,在后期再进行赋值操作
    3. 当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时该对象在容器中存在两个状态:完整状态和实例化但未初始化状态,而且是同名的,为了避免调用时的错误,要分别用不同的map结构进行存储,也就是我们说的一级缓存和二级缓存。往一级缓存中存放完整的对象的时候,二级缓存中的同名不完整对象会被删除。同理,往二级缓存中存放不完整对象时,会将三级缓存中的同名对象删除。故对象的查找顺序是1,2,3。
    4. 三级缓存的value类型是ObjectFactory,是一个函数式接口存在的意义是保证容器在运行过程中的任何时候都只有一个同名的bean对象供外部调用

    问:如果只有一级缓存行不行?

    • 不行,因为完整对象和不完整的对象会放到一个map中,在进行对象获取的时候有可能获取到不完整对象,这样的对象是无法使用的

    问:如果只有二级缓存行不行?

    • 虽然只有二级缓存的时候也可以解决循环依赖问题,但是在添加aop的实现之后,报错:This means that said other beans do not user final version of the bean(没有使用最终版本的bean对象)

    问:为什么需要三级缓存?(三级缓存的作用?)

    1. 如果一个对象需要被代理,在生成代理对象之前,一定要先生成一个普通的非代理对象(底层源码中生成代理对象的方法要传入一个bean对象)

      这也是上面遗留问题的答案,为什么二级缓存中会有对象。移除二级缓存中对象的原因如3所说,要让代理对象覆盖非代理对象。

    2. 普通对象和代理对象不能同时出现在容器中,因此当一个对象需要被代理的时候,就要使用代理对象覆盖普通对象。通过getEarlyBeanReference来执行对象的覆盖过程。

      只要搞清楚这个方法的具体执行逻辑即可,doCreateBean方法中有这样一段代码
      Spring面试题之循环依赖与三级缓存_第7张图片
      在当前方法中,有可能会用代理对象替换非代理对象,如果没有三级缓存,就无法得到代理对象。换句话说,在整个容器中,包含了同名对象的代理对象和非代理对象,这是不合理的。容器中,对象都是单例的,意味着根据名称只能获取一个对象的值,此时同时存在两个对象的话,使用的时候无法判读取哪一个。谁也无法确认什么时候会调用当前对象,是在其他对象的执行过程中进行调用的,而不是人为指定的。所以必须要保证容器中任何时候都只有一个同名的对象供外部调用, 在三级缓存中,完成了一件事:让代理对象替换非代理对象的工作,确定返回的是唯一的对象

    3. 因此所有的bean对象在创建的时候都要优先放在三级缓存中,在后续的使用过程中,如果需要被代理则返回代理对象;否则,返回普通对象。

    4. 三级缓存是为了解决在aop代理过程中产生的循环依赖问题,如果没有aop的话,二级缓存足以解决循环依赖问题

      • 其实相当于是一个回调机制,当需要使用当前对象时,会判断此对象是否需要被代理实现,需要的话直接替换,不需要的话直接返回原生对象
      • spring是一个框架,跟业务完全无关,也不知道我们会不会使用aop。所以spring会走一个总体的流程,如果使用了aop就处理,没有使用就直接返回,不会有其他影响。

以上就是我自己对spring循坏依赖与三级缓存的理解,如果有错误的地方,希望大家指正。

你可能感兴趣的:(Spring,缓存,spring,java,面试)