当前位置: 首页 > 科技观察

为什么Spring需要三级缓存来解决循环依赖,而不是二级缓存?

时间:2023-03-20 20:30:22 科技观察

前言在日常使用spring框架的开发中,bean之间的循环依赖太频繁了。Spring帮我们解决了循环依赖问题,这是我们开发者察觉不到的。下面详细分析一下spring。如何解决bean之间的循环依赖,为什么要用三级缓存而不用二级缓存?Bean的生命周期首先你需要了解spring中bean的生命周期和spring中bean的加载过程,这样才能更清楚的知道spring是如何解决循环依赖的。另外,Spring系列面试题及答案已经全部整理完毕。微信搜索Java技术栈,后台发送:面试,网上可以看。我们在spring的BeanFactory工厂中列举了很多接口,代表了bean的生命周期。我们主要记住红圈圈出来的接口,结合spring的源码看看这些接口主要调用在哪里。AbstractAutowireCapableBeanFactory类doCreateBean方法是创建bean的开始。我们可以看到首先需要实例化bean,也就是在堆中为这个对象开辟一块内存空间。createBeanInstance方法中的逻辑大概是利用反射生成实例对象。这意味着该对象还没有填充属性,也就是没有注入@Autowired注解的属性。我们可以看到,第二步是填充bean的成员属性。populateBean方法中的逻辑大致就是使用注入属性的注解会被注入,如果在注入过程中发现注入的对象还没有生成,就会运行生成要注入的对象。第三步是调用initializeBean方法来初始化bean,也就是调用我们上面提到的接口,可以看到initializeBean方法,首先调用的是使用到的Aware接口的方法。让我们看一下调用invokeAwareMethods方法中的Aware接口的方法。我们可以知道,如果我们实现BeanNameAware、BeanClassLoaderAware、BeanFactoryAware这三个Aware接口,就会依次调用setBeanName()。,setBeanClassLoader(),setBeanFactory()方法,然后查看applyBeanPostProcessorsBeforeInitialization的源码,发现如果有类实现了BeanPostProcessor接口,就会执行postProcessBeforeInitialization方法。这里需要注意的是,如果多个类都实现了BeanPostProcessor接口,那么多个实现类都会执行postProcessBeforeInitialization方法,可以看到for循环是顺序执行的。还有一点需要注意的是,如果A类加载到spring容器中,A类也会重写BeanPostProcessor接口的postProcessBeforeInitialization方法。这时候注意A类的postProcessBeforeInitialization方法不会执行,因为类A还没有加载,还没有完全放入spring的singletonObjects一级缓存中。整理了最新的面试题,大家可以使用Java面试库小程序在线刷新题型。再看一个关注点,可以看到ApplicationContextAwareProcessor也实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization方法,在方法中调用了invokeAwareInterfaces方法,invokeAwareInterfaces方法也说如果实现了很多Aware接口,对应的就会依次执行方法,值得注意的是ApplicationContextAware接口的setApplicationContext方法,再看invokeInitMethods源码发现,如果实现了InitializingBean接口,重写afterPropertiesSet方法,就会调用afterPropertiesSet方法,而是否指定init-method最后会被调用,可以通过label传递,或者@Bean注解的initMethod规范,最后看一个applyBeanPostProcessorsAfterInitialization源码图,发现和前面的差不多后ProcessBeforeInitialization方法,它也是循环遍历实现BeanPostProcessor的接口实现类,执行postProcessAfterInitialization方法。整个bean的生命执行过程如上图所示,在哪调用了哪个接口方法,以及方法执行过程。最后对bean的生命过程做一个流程图总结。三级缓存解决了循环依赖。上一节做了一个bean生命周期的整体流程分析,对于spring如何解决循环依赖有很大的帮助。前面我们分析填充属性的时候,如果发现spring没有生成属性,就会运行生成属性对象实例。最新的面试题已经整理完毕,大家可以在Java面试库小程序中在线刷题。我们可以看到,在填充属性的时候,spring会通过ObjectFactory半成品提前暴露出实例化的bean。为什么叫半成品是因为此时实例化了bean对象,但是没有填充属性,所以是一个不完整的bean实例对象spring使用了singletonObjects、earlySingletonObjects、singletonFactories三级缓存来解决问题。所谓缓存,其实就是三个Map,可以看到三级缓存中存储的对象。这里重点关注二级缓存earlySingletonObjects和三级缓存singletonFactory。一级缓存可以忽略。前面我们说过,第一个实例化的bean会通过ObjectFactory半成品提前暴露在三级缓存中。singletonFactory是传入的匿名内部类,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法。下面看看循环依赖中如何获取其他半成品的实例对象。我们假设有这样一个场景,AService依赖BService,BService又依赖AService1。首先实例化AService,实例化通过ObjectFactory半成品暴露给三级缓存。B服务3。在加载BService的过程中,通过ObjectFactory半成品实例化暴露给三级缓存。4.填充属性AService时,此时,可以从三级缓存中获取半成品ObjectFactory。拿到ObjectFactory对象后,调用ObjectFactory的.getObject()方法,最终会调用getEarlyBeanReference()方法。getEarlyBeanReference方法的主要逻辑大致描述了如果bean被AOP切面代理了,则返回beanProxy对象,如果没有被代理,则返回原来的bean实例。这个时候我们会发现可以拿到bean实例(属性没填),然后从三级缓存中取出,放到二级缓存earlySingletonObjects中。这时候B注入了一个半成品的实例A对象,但是随着B初始化完成,A会继续进行后续的初始化操作,最后B会注入一个完整的A实例,因为它们是同一个对象记忆。以下是重点。我们发现二级缓存似乎有点多余。好像可以去掉。只有一级和三级缓存才能解决循环依赖问题???只要两个缓存确实可以解决循环依赖的问题,但是有个前提,bean没有被AOP代理。如果bean是AOP代理的,那么只有两个缓存是解决不了问题的。我们看下bean被AOP代理的场景,发现AService的testAopProxy被AOP代理了,看看传入的匿名内部类的getEarlyBeanReference返回的是什么对象。如果你想成为一名架构师,建议你看看这张架构师地图,少走弯路。发现singletonFactory.getObject()返回的是一个AService代理对象,还是CGLIB代理的。再看一个,再执行singletonFactory.getObject(),看返回的是不是同一个AService的代理对象。我们会发现再次执行singleFactory.getObject()方法是一个新的代理对象,这样会出问题,因为AService是单例的,每次执行singleFactory.getObject()方法都会生成一个新的代理对象.假设这里只有一级和三级缓存,每次从三级缓存中获取singleFactory对象时,执行getObject()方法生成一个新的代理对象。这是不可能的,因为AService是单例的,所有这里我们都需要使用二级缓存来解决这个问题。将执行singleFactory.getObject()生成的对象放到二级缓存中,然后去二级缓存中获取。无需再次执行singletonFactory.getObject()方法生成新的代理对象,保证代理对象永远只有一个。还有一点需要注意,既然singleFactory.getObject()返回的是代理对象,那么注入的也应该是代理对象。我们可以看到注入的确实是CGLIB代理的AService对象。所以如果没有AOP,二级缓存确实可以解决循环依赖的问题。如果加上AOP,二级缓存无法解决。不可能每次执行singleFactory.getObject()方法对象都为我生成一个新的代理,所以我们需要使用另一个缓存来保存生成的代理对象。综上所述,我们首先讲了bean的加载过程。了解bean的加载过程,对于spring如何解决循环依赖问题很有帮助。后面我们会分析为什么spring需要用到三级缓存来解决循环依赖问题,而不是二级缓存。大家可以去网上试试AOP的情况,实践一下就明白为什么二级缓存不能解决AOP代理的场景了。在工作中,我一直认为编程代码不是最重要的,而是在工作中养成的编程思维。