spring-framework核心功能包含了ioc(依赖翻转的实现)。为了将实现和抽象进一步抽离,通过框架来实现对java对象的创建和注入,使实现的切换、对象的注入变得简单,大大降低代码的耦合度。
三级缓存是个啥?
spring的三级缓存,是基于ioc的思想的实现,三级缓存的存在是用于解决循环依赖,以及spring内置AOP产生依赖注入问题的解决。
啥是依赖注入?
在创建某个对象的时候,这个对象可能有一些属性字段需要同步的给它进行初始化操作,来实现该对象整体属性的填充。我们使用spring容器大部分时候是在使用它创建一堆一堆的单例bean,而单例bean有大多扮演提供业务的角色,多个业务之间本身可能存在关联依赖。因此单例bean之间相互引用是不可避免的,单例bean之间的相互引用spring需要帮助我们做对应依赖引用的填充,这个填充的过程就是依赖注入。
依赖注入的前提是:①系统存在至少两个的单例bean要创建②各个单例bean之间由于业务的需要一定存在相互的引用
啥是循环依赖?
将对象的实现交于spring来管理,那么被管理对象的创建、被管理对象内部的字段初始化、被管理对象的其余初始化执行逻辑,均交由spring容器来操作。我们假设现在spring要管理两个A、B类的实例,那么可能存在三种情况:
①A、B均没有没有待注入的对象。这是最简单的创建情况,spring扫描到后顺序的进行反射创建,放入单例池。
②A需要注入B,而B没有待注入对象;或者A没有待注入对象,而B需要注入A。两种情况都是待注入对象要先创建成功,另外一个对象才能继续创建。
②A需要注入B,而B也需要注入A。此时循环依赖就产生了。
啥是AOP?
AOP是面向切面编程思想的简称,指在不破坏原有代码结构和逻辑的情况下,对某类代码进行和主业务无关的功能附加,附加方式就是基于原来的对象进行一个代理,将源对象作为target注入到代理对象中,然后通过代理对象重写源对象方法,在源对象方法调用的前后做一些事,代理的方式分继承和实现这里不详细展开。如:埋点日志、鉴权、效率统计等等场景可能会使用到该技术。
springframework内置了AOP功能,因此在单例对象创建时,会对需要AOP的对象形成代理对象,并放入spring单例池。
为什么需要三级缓存来解决IOC的循环依赖问题?
单例bean是如何创建的
三级缓存的由来要从spring单例bean的创建流程说起。 spring创建单例bean的核心步骤是(注意这里描述的本不全,只是为了说明循环依赖问题,顺序列出了部分流程):扫描 -> 从单例池中获取bean(如果获取不到) -> 反射创建单例bean -> 填充单例bean的属性字段(依赖注入阶段) -> 放入单例缓存池中 -> 从缓存池中取bean去调用。
从上面的步骤来看,似乎存放单例bean很简单,只用一个集合即可。但是spring为了支持循环依赖的注入场景。我们光使用一个集合来缓存单例bean显然是不够的,仍然以A、B两个单例的实例化过程举例(其中A需要注入B,B需要注入A)。在反射实例化A之后,为了填充A对象依赖的B属性,那么spring会在A依赖注入阶段去创建B对象,而B对象也需要注入A,但是从单例缓存池中拿不到A,因为此时A正在创建(所以并没有把A放到单例池中),那么在B对象的依赖注入阶段,又需要去创建A。此时不难发现,程序陷入了死循环。
如何解决循环依赖时的死循环?
一个集合不行,我们就借助两个集合。一个是单例池,用于存放最终的单例对象,最终的单例对象是指这个对象中的所有需要依赖注入的属性字段均已被填充。另一个是早期单例对象池,用于存放通过反射创建完成的单例对象,此时这个单例对象的任何属性均没有被填充或者初始化。
这样的话流程将变成:扫描 -> 从单例池中拿(拿不到) -> 从早期单例对象池中拿(拿不到)-> 反射创建单例bean -> 放入早期单例池中 -> 填充单例bean的属性字段(依赖注入阶段) -> 放入单例缓存池中,并删除早期单例池中的该对象 -> 从缓存池中取bean去调用。
这样做的依据是,java中的对象赋值是引用赋值,因此只需要把引用给到被注入对象,那么后续这个注入对象如内部如何填充依赖,被注入对象都能感知到。最后属性填充完毕,需要将早期单例池中的对象移动到单例池中去,因此涉及到对早期单例池对象删除。
这样看,两个集合似乎已经能解决了循环依赖的问题,其实则不然。需要再重申一下,三级缓存的存在是用于解决循环依赖,以及spring内置AOP产生依赖注入问题的解决。AOP代理后产生的对象最终是要放入单例池的,但是AOP产生代理对象的逻辑是在属性填充之后,通过BeanProcessor的after方法产生的(这块不详细讲bean的后置处理器了,可以理解为在单例bean初始化前后,这些BeanProcessor处理器可以通过before或者after去修饰正在创建的bean,AOP代理实际上就是在针对正在创建Bean的修饰从而产生了新的代理对象)。此时如果只有两个集合会有一个问题。早期单例池中,放入的是直接通过反射创建出来的原始对象,但是最终通过BeanProcessor修饰后的Bean变成了一个代理对象,那么依赖注入时注入的对象将是原始对象,单例池中的对象是针对原始对象的代理,是两个不同的对象,由此被注入的对象调用注入的对象的方法,实际上就是调用了原始没有被AOP的对象的方法,代理逻辑会失效,更是打破了单例池的功能定义。
三级缓存解决循环依赖+代理问题
问题解决的核心在于,填充bean时,判断如果存在循环依赖,就把存在循环依赖的bean拿出来判断是否需要提前代理,判断逻辑执行完毕后无论是代理或是不代理,都会将这个对象放到早期单例池中以保证获取注入对象的唯一性,此时代理对象是被放入了早期单例池中,然后再注入到对象的字段属性中。而当需要被代理的对象执行到对应的AOP的BeanProcessor时,会判断一下是否已经提前代理了,如果已经提前代理,那就跳过代理逻辑直接返回,这样就能保证注入的对象和单例池中的对象均为代理对象。为了保证对象能被提前代理,则需要另一个集合去保存原始对象,在出现循环依赖的时候,将它从这个集合中掏出来,然后判断是否需要进行提前代理,这个就是第三个集合存在的意义,用于解决循环依赖+aop代理导致的注入依赖和单例池对象不一致的问题。
此时会发现,aop代理的逻辑在bean创建的生命周期中的嵌入,似乎是spring的定制化开发。也的确如此,因此开发人员自定义的BeanProcessor如果涉及到对存在循环依赖的且需要被springAOP的bean进行再次代理,仍然会出现单例池中对象与依赖注入对象不一致的问题,但spring针对这种情况已经做了判断,会直接报错。解决该问题的核心就是打破循环依赖,能不循环依赖的时候,尽量不要有循环注入的可能发生,解决思路有:①将可能导致循环依赖的方法逻辑抽出到新的bean中,在旧的bean中再引入这个bean;②或者通过@Lazy注解规避,通过延迟加载来打破创建时的循环依赖。