美文网首页
Spring核心之bean生命周期和三级缓存

Spring核心之bean生命周期和三级缓存

作者: 上善若泪 | 来源:发表于2022-03-03 10:09 被阅读0次

    在使用spring框架的日常开发中,bean之间的循环依赖太频繁了,spring已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下spring是如何解决bean之间循环依赖,为什么要使用到三级缓存,而不是二级缓存?

    1 bean生命周期

    首先大家需要了解一下beanspring中的生命周期,beanspring的加载流程,才能够更加清晰知道spring是如何解决循环依赖的。

    Spring IOCBean的生命周期大致分为四个阶段:

    • 实例化(Instantiation
    • 属性赋值(Populate
    • 初始化(Initialization
    • 销毁(Destruction

    1.1 bean生命周期中重要接口

    在这里插入图片描述

    我们在springBeanFactory工厂列举了很多接口,代表着bean的生命周期,我们主要记住的是圈红线圈出来的接口, 再结合spring的源码来看这些接口主要是在哪里调用的

    1.2 创建bean

    在这里插入图片描述

    AbstractAutowireCapableBeanFactory类的doCreateBean方法是创建bean的开始,我们可以看到首先需要实例化这个bean,也就是在堆中开辟一块内存空间给这个对象,createBeanInstance方法里面逻辑大概就是采用反射生成实例对象,进行到这里表示对象还并未进行属性的填充,也就是@Autowired注解的属性还未得到注入

    1.2.1 两个阶段

    Spring将管理的一个个的依赖对象称之为Bean,这从xml配置文件中也可以看出。
    Spring IOC容器就好像一个生产产品的流水线上的机器,Spring创建出来的Bean就好像是流水线的终点生产出来的一个个精美绝伦的产品。既然是机器,总要先启动,Spring也不例外。因此Bean的一生从总体上来说可以分为两个阶段:

    • 容器启动阶段
    • Bean实例化阶段

    容器的启动阶段做了很多的预热工作,为后面Bean的实例化做好了充分的准备,我们首先看一下容器的启动阶段都做了哪些预热工作。

    1.2.2 容器启动阶段

    1.2.2.1 配置元信息

    我们说Spring IOC容器将对象实例的创建与对象实例的使用分离,我们的业务中需要依赖哪个对象不再依靠我们自己手动创建,只要向Spring要,Spring就会以注入的方式交给我们需要的依赖对象。

    既然我们将对象创建的任务交给了Spring,那么Spring就需要知道创建一个对象所需要的一些必要的信息。而这些必要的信息可以是Spring过去支持最完善的xml配置文件,或者是其他形式的例如properties的磁盘文件,也可以是现在主流的注解,甚至是直接的代码硬编码。总之,这些创建对象所需要的必要信息称为配置元信息。

    <bean id="role" class="com.wbg.springxmlbean.entity.Role">
        <!-- property元素是定义类的属性,name属性定义的是属性名称 value是值
        相当于:
        Role role=new Role();
        role.setId(1);
        role.setRoleName("高级工程师");
        role.setNote("重要人员");-->
        <property name="id" value="1"/>
        <property name="roleName" value="高级工程师"/>
        <property name="note" value="重要人员"/>
    </bean>
    

    1.2.2.2 BeanDefination

    我们大家都知道,在Java世界中,万物皆对象,散落于程序代码各处的注解以及保存在磁盘上的xml或者其他文件等等配置元信息,在内存中总要以一种对象的形式表示,就好比我们活生生的人对应到Java世界中就是一个Person类。
    Spring选择在内存中表示这些配置元信息的方式就是BeanDefination,这里我们只是需要知道配置元信息被加载到内存之后是以BeanDefination的形存在的

    1.2.2.3 BeanDefinationReader

    大家肯定很好奇,我们是看得懂Springxml配置文件中一个个的Bean定义,但是Spring是如何看懂这些配置元信息的呢?这个就要靠我们的BeanDefinationReader了。

    不同的BeanDefinationReader就像葫芦兄弟一样,各自拥有各自的本领。如果我们要读取xml配置元信息,那么可以使用XmlBeanDefinationReader。如果我们要读取properties配置文件,那么可以使用PropertiesBeanDefinitionReader加载。而如果我们要读取注解配置元信息,那么可以使用 AnnotatedBeanDefinitionReader加载。我们也可以很方便的自定义BeanDefinationReader来自己控制配置元信息的加载。假如我们的配置元信息现有的不能满足,那么我们可以自定义From BeanDefinationReader

    总的来说,BeanDefinationReader的作用就是加载配置元信息,并将其转化为内存形式的BeanDefination,存在某一个地方

    1.2.2.4 BeanDefinationRegistry

    执行到这里,总算不遗余力的将存在于各处的配置元信息加载到内存,并转化为BeanDefination的形式,这样我们需要创建某一个对象实例的时候,找到相应的BeanDefination然后创建对象即可。那么我们需要某一个对象的时候,去哪里找到对应的BeanDefination呢?

    这种通过Bean定义的id找到对象的BeanDefination的对应关系或者说映射关系又是如何保存的呢?这就引出了BeanDefinationRegistry了。
    Spring通过BeanDefinationReader将配置元信息加载到内存生成相应的BeanDefination之后,就将其注册到BeanDefinationRegistry中,BeanDefinationRegistry就是一个存放BeanDefination的大篮子,它也是一种键值对的形式,通过特定的Bean定义的id,映射到相应的BeanDefination

    1.2.2.5 BeanFactoryPostProcessor

    BeanFactoryPostProcessor是容器启动阶段Spring提供的一个扩展点,主要负责对注册到BeanDefinationRegistry中的一个个的BeanDefination进行一定程度上的修改与替换。

    例如我们的配置元信息中有些可能会修改的配置信息散落到各处,不够灵活,修改相应配置的时候比较麻烦,这时我们可以使用占位符的方式来配置。例如配置Jdbc的DataSource连接的时候可以这样配置:

    <bean id="dataSource"  
        class="org.apache.commons.dbcp.BasicDataSource"  
        destroy-method="close">  
        <property name="maxIdle" value="${jdbc.maxIdle}"></property>  
        <property name="maxActive" value="${jdbc.maxActive}"></property>  
        <property name="maxWait" value="${jdbc.maxWait}"></property>  
        <property name="minIdle" value="${jdbc.minIdle}"></property>  
      
        <property name="driverClassName"  
            value="${jdbc.driverClassName}">  
        </property>  
        <property name="url" value="${jdbc.url}"></property>  
      
        <property name="username" value="${jdbc.username}"></property>  
        <property name="password" value="${jdbc.password}"></property>  
    </bean> 
    

    BeanFactoryPostProcessor就会对注册到BeanDefinationRegistry中的BeanDefination做最后的修改,替换$占位符为配置文件中的真实的数据。

    至此,整个容器启动阶段就算完成了,容器的启动阶段的最终产物就是注册到BeanDefinationRegistry中的一个个BeanDefination了,这就是Spring为Bean实例化`所做的预热的工作。让我们再通过一张图的形式回顾一下容器启动阶段都是搞了什么事吧。

    image.png

    1.2.3 Bean实例化阶段

    需要指出,容器启动阶段与Bean实例化阶段存在多少时间差,如果我们选择懒加载的方式,那么直到我们伸手向Spring要依赖对象实例之前,其都是以BeanDefinationRegistry中的一个个的BeanDefination的形式存在,也就是Spring只有在我们需要依赖对象的时候才开启相应对象的实例化阶段。

    而如果我们不是选择懒加载的方式,容器启动阶段完成之后,将立即启动Bean实例化阶段,通过隐式的调用所有依赖对象的getBean方法来实例化所有配置的Bean并保存起来。

    1.2.3.1 对象创建策略

    对象的创建采用了策略模式,借助我们前面BeanDefinationRegistry中的BeanDefination,我们可以使用反射的方式创建对象,也可以使用CGlib字节码生成创建对象。
    同时我们可以灵活的配置来告诉Spring采用什么样的策略创建指定的依赖对象。Spring中Bean的创建是策略设计模式的经典应用。这个时候,内存中应该已经有一个我们想要的具体的依赖对象的实例了,但是故事的发展还没有我们想象中的那么简单。

    1.2.3.2 BeanWrapper——对象的外衣

    Spring中的Bean并不是以一个个的本来模样存在的,由于Spring IOC容器中要管理多种类型的对象,因此为了统一对不同类型对象的访问,Spring给所有创建的Bean实例穿上了一层外套,这个外套就是BeanWrapper

    BeanWrapper实际上是对反射相关API的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化,我们要获取某个对象的属性,调用某个对象的方法,现在不需要在写繁杂的反射API了以及处理一堆麻烦的异常,直接通过BeanWrapper就可以完成相关操作

    1.3 属性填充

    上一步包裹在BeanWrapper中的对象还是一个少不经事的孩子,需要为其设置属性以及依赖对象

    • 基本类型属性 : 如果配置元信息中有配置,那么将直接使用配置元信息中的设置值赋值即可,即使基本类型的属性没有设置值,那么得益于JVM对象实例化过程,属性依然可以被赋予默认的初始化零值。
    • 引用类型属性 : Spring会将所有已经创建好的对象放入一个Map结构中,此时Spring会检查所依赖的对象是否已经被纳入容器的管理范围之内,也就是Map中是否已经有对应对象的实例了。如果有,那么直接注入,如果没有,那么Spring会暂时放下该对象的实例化过程,转而先去实例化依赖对象,再回过头来完成该对象的实例化过程。
      在这里插入图片描述

    我们可以看到第二步就是填充bean的成员属性,populateBean方法里面的逻辑大致就是对使用到了注入属性的注解就会进行注入,如果在注入的过程发现注入的对象还没生成,则会跑去生产要注入的对象,第三步就是调用initializeBean方法初始化bean,也就是调用我们上述所提到的接口

    1.4 初始化bean

    在这里插入图片描述

    可以看到initializeBean方法中,首先调用的是使用的Aware接口的方法,我们具体看一下invokeAwareMethods方法中会调用Aware接口的那些方法

    1.4.1 Aware相关接口

    在这里插入图片描述

    我们可以知道如果我们实现了BeanNameAwareBeanClassLoaderAwareBeanFactoryAware三个Aware接口的话,会依次调用setBeanName(), setBeanClassLoader(), setBeanFactory()方法,再看applyBeanPostProcessorsBeforeInitialization源码

    1.4.2 BeanPostProcessors相关接口

    在这里插入图片描述

    发现会如果有类实现了BeanPostProcessor接口,就会执行postProcessBeforeInitialization方法,这里需要注意的是:如果多个类实现BeanPostProcessor接口,那么多个实现类都会执行postProcessBeforeInitialization方法,可以看到是for循环依次执行的,还有一个注意的点就是如果加载A类到spring容器中,A类也重写了BeanPostProcessor接口的postProcessBeforeInitialization方法,这时要注意A类的postProcessBeforeInitialization方法并不会得到执行,因为A类还未加载完成,还未完全放到springsingletonObjects一级缓存中。

    再看一个注意的点

    在这里插入图片描述 图片

    可以看到ApplicationContextAwareProcessor也实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization方法,方法里面并调用了invokeAwareInterfaces方法,而invokeAwareInterfaces方法也写着如果实现了众多的Aware接口,则会依次执行相应的方法,值得注意的是ApplicationContextAware接口的setApplicationContext方法,再看一下invokeInitMethods源码

    1.4.3 InitializingBean接口

    在这里插入图片描述

    发现如果实现了InitializingBean接口,重写了afterPropertiesSet方法,则会调用afterPropertiesSet方法,最后还会调用是否指定了init-method,可以通过标签,或者@Bean注解的initMethod指定,最后再看一张applyBeanPostProcessorsAfterInitialization源码图

    1.4.4 BeanPostProcessors接口后置方法

    在这里插入图片描述

    发现跟之前的postProcessBeforeInitialization方法类似,也是循环遍历实现了BeanPostProcessor的接口实现类,执行postProcessAfterInitialization方法。整个bean的生命执行流程就如上面截图所示,哪个接口的方法在哪里被调用,方法的执行流程。

    1.5 bean生命周期总结

    最后,对bean的生命流程进行一个流程图的总结


    在这里插入图片描述

    或者看简单版本:


    在这里插入图片描述

    点击了解bean作用域和线程安全问题

    2 三级缓存

    2.1 引言

    上面对bean的生命周期做了一个整体的流程分析,对spring如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring中生成,则会跑去生成属性对象实例。

    在这里插入图片描述

    我们可以看到填充属性的时候,spring会提前将已经实例化的bean通过ObjectFactory半成品暴露出去,为什么称为半成品是因为这时候的bean对象实例化,但是未进行属性填充,是一个不完整的bean实例对象
    实例化 Bean 之后,会往 singletonFactories 塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean

    在这里插入图片描述

    spring利用singletonObjects, earlySingletonObjects, singletonFactories三级缓存去解决的,所说的缓存其实也就是三个Map

    2.2 三级缓存各个存放对象

    三级缓存各个存放对象:

    • 一级缓存singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
    • 二级缓存earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
    • 三级缓存singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存

    这三个 map 是如何获取配合的:

    1. 获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2。
    2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
    3. singletonFactories (三级缓存)通过BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中。
    4. 如果三个缓存都没找到,则返回 null


      在这里插入图片描述

    可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects和三级缓存singletonFactory,一级缓存可以进行忽略。前面我们讲过先实例化的bean会通过ObjectFactory半成品提前暴露在三级缓存中

    在这里插入图片描述

    singletonFactory是传入的一个匿名内部类,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。

    2.3 解决循环依赖条件

    Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

    • 必须是单例
      依赖的 Bean 必须都是单例
      因为原型模式都需要创建新的对象,不能跟用以前的对象
    • 不能全是构造器注入
      依赖注入的方式,必须不全是构造器注入,且 beanName字母顺序在前的不能是构造器注入
      在 Spring 中创建 Bean 分三步:
      实例化,createBeanInstance,就是 new 了个对象
      属性注入,populateBean, 就是 set 一些属性值
      初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等
      明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。
      如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B 。但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。
      为什么找不到?因为 A 还没 new 完呢,所以找到不完整的 A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖
    • 一个set注入,一个构造器注入能否成功
      假设我们 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
      我们来分析下:实例化 A 之后,可以在 map 中存入 A,开始为 A 进行属性注入,发现需要 B,此时 new B,发现构造器需要 A,此时从 map 中得到 A ,B 构造完毕。
      B 进行属性注入,初始化,然后 A 注入 B 完成属性注入,然后初始化 A。
      整个过程很顺利,没毛病
      假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
      我们来分析下:实例化 A,发现构造函数需要 B, 此时去实例化 B。
      然后进行 B 的属性注入,从 map 里面找不到 A,因为 A 还没 new 成功,所以 B 也卡住了,然后就 失败
      看到这里,仔细思考的小伙伴可能会说,可以先实例化 B 啊,往 map 里面塞入不完整的 B,这样就能成功实例化 A 了啊
      确实,思路没错但是 Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面

    现在我们总结一下:

    • 如果循环依赖都是构造器注入,则失败
    • 如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟BeanName的字母序有关系

    2.4 循环依赖示例说明

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

    1. AService首先实例化,实例化通过ObjectFactory半成品暴露在三级缓存中
    2. 填充属性BService,发现BService还未进行过加载,就会先去加载BService
    3. 再加载BService的过程中,实例化,也通过ObjectFactory半成品暴露在三级缓存
    4. 填充属性AService的时候,(从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在, 把A放⼊⼆级缓存,同时删除三级缓存中的A ,此时,B已经实例化并且初始化完成,把B放入⼀级缓存)这时候能够从三级缓存中拿到半成品的ObjectFactory
      在这里插入图片描述
      拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果beanAOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例
    5. 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存
    6. 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象

    2.5 是否可以移除二级缓存

    我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题

    只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个beanAOP进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下beanAOP进行了切面代理的场景

    在这里插入图片描述

    我们发现AServicetestAopProxyAOP代理了,看看传入的匿名内部类的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()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象

    转载于:
    https://mp.weixin.qq.com/s/iCaBsXInpbNc8tiV-qRM4g
    https://mp.weixin.qq.com/s/JVhRgiEcaNf6KLQehQwx1Q

    相关文章

      网友评论

          本文标题:Spring核心之bean生命周期和三级缓存

          本文链接:https://www.haomeiwen.com/subject/jsmyrrtx.html