• 基础 | Spring - [单例创建过程]


    §1 相关类与容器

    • SpringApplication 是 springboot 应用的启动类的引导类
      在这里插入图片描述

    • run()SpringApplication 中,我们主要使用的方法

    • PostProcessor 是 springboot 在初始化过程中的重要组件,包含多种类型,如 BeanFactoryPostProcessorsBeanPostProcessors 分别用来完成 bean 工厂和 bean 的初始化动作
      一些与 springboot 整合的框架也会频繁使用类似的方式,比如 KafkaListenerAnnotationBeanPostProcessor

    • AbstractApplicationContext 在 springboot 中扮演重要角色,大部分启动流程实际是通过这个类完成的

    • ConfigurableListableBeanFactory 是 springboot 实例工厂的接口

      • 其默认实现为 DefaultListableBeanFactory,主要依赖它来实例化对象

      • DefaultListableBeanFactory 继承自 AbstractBeanFactory
        关键方法 doGetBeancreateBean 在这个抽象类的中

        • doGetBean 在此类中实现,方法定义了从尝试直接获取对象,到创建对象的流程

        • doGetBean 在此类中定义,在子类 AbstractAutowireCapableBeanFactory 中实现,用于真正创建实例

          public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
             	implements AutowireCapableBeanFactory {
          
          • 1
          • 2
    • DefaultSingletonBeanRegistry 是 Spring 用于单例模式的 bean 注册器
      singletonObjects earlySingletonObjectssingletonFactoriesDefaultSingletonBeanRegistry 中的 3 个容器,如下代码

      • 在单例 SpringBean 的构建过程中,这三个容器作为缓存使用
      • singletonFactories 工厂缓存,作为三级缓存,存放 beanName<–>bean factory
      • earlySingletonObjects 早期预览缓存,作为二级缓存,存放 beanName<–>early bean(没有完全创建完)
      • singletonObjects 完全缓存,作为一级缓存,存放 beanName<–>bean(完全创建完)
      /** Cache of singleton objects: bean name to bean instance. */
      //1级, 已经完全实例化好了的 bean
      private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
      
      /** Cache of early singleton objects: bean name to bean instance. */
      //2级, 没有完全实例化好的 bean 
      private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
      
      /** Cache of singleton factories: bean name to ObjectFactory. */
      //3级, bean 的工厂
      private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    §2 过程

    在这里插入图片描述
    在这里插入图片描述

    进入 SpringApplication.run,通过 refreshContext 刷新容器
    在这里插入图片描述
    然后进入 AbstractApplicationContext.refresh
    在这里插入图片描述
    这一步骤中注册 spring bean 的后置处理器
    依赖注入,基于其中一个 spring bean 的后置处理器 实现
    在这里插入图片描述

    然后进入 AbstractApplicationContext.finishBeanFactoryInitialization ,完成 spring bean 工厂的初始化
    在这里插入图片描述
    此方法的最后一步,就是初始化所有非懒加载的单例对象
    在这里插入图片描述
    这里的 beanFactoryDefaultSingletonBeanRegistry
    从下图位置 preInstantiateSingletons 开始,springboot 开始实例化非懒加载的单例 beans
    为了调试方便,这里使用 条件断点,条件为 bean 的 name,是我们想关注的那几个(否则会有很多 springboot 自己的对象)
    在这里插入图片描述
    1 处,判断当前这个 beanName 对应的是个单例的对象,不是就 pass
    2 处,判断当前这个 beanName 对应的是个 工厂,肯定不是,走 else
    3 处,正常单例无误,开始获取(获取不到肯定就得创造)
    在这里插入图片描述
    路过若干个重载的 getBean 最终来到 AbstractBeanFactory.doGetBean
    在红框位置,检查当前 Bean 是否存在于缓存中,但此时的 bean 还没有标记为创建中,因此被拒绝,如下图红叉处
    在这里插入图片描述
    在这里插入图片描述

    回到 AbstractBeanFactory.doGetBean 后检查有没有在当前工厂里已经定义了,这里其实是从父工厂里进行检查
    看上面的红框,父工厂都是 null ,跳过了
    看下面的红框,如果不是只校验类型不校验引用,会将当前 bean 标记为 创建中
    在这里插入图片描述

    接下来检查 bean 是否有 depend on 的限制,当前案例肯定是没有的,跳过
    在这里插入图片描述

    接下来是三种对象的类型的构建,分别是 单例原型其他
    单例,2 处进入创建实例的逻辑,但需要注意,这个逻辑只是定义在红线的匿名内部类中,实际调用时机是 3 处的方法内

    在这里插入图片描述

    原型,可见多了一个 beforePrototypeCreation 方法 和后面的 afterPrototypeCreation
    在这里插入图片描述

    其他,从 1 处可见,在这种情况下,springboot 是懵逼的,所以一言不合就找机会扔异常
    同时,从 2 处可见,在这种情况下,4 处提供的 getObject 方法是按照 原型 的过程创建对象的
    可以理解为这种创建过程是对 原型 的微调和别名
    在这里插入图片描述

    可以看到默认的其他类型 socpes 如下图所示,可以理解为什么以 原型为模板,因为不可能只有一个 请求 吧
    在这里插入图片描述
    而 其他 scope 和原型的区别在于黄圈的 3 处,这里对于每一种 scope 定义自己 getObject 的行为
    这个行为命名为 get 方法,并在 Scope.get 中 调用绿圈 4 处定义的 getObject 的逻辑(即 2 处的逻辑)

    当前场景肯定是执行 单例 分支,即上面 单例截图 3 处
    来到 DefaultSingletonBeanRegistry.getSingleton 可见 单例截图 的 2 处其实就是下图 1 处的入参,并在下图 2 处被使用

    DefaultSingletonBeanRegistry.getSingleton
    注意上图的 beforeSingletonCreation 更下面还有一个 afterSingletonCreation 可见 beforeXXCreationcreateBeanafterXXCreation 是 springboot 创建实例的套路
    同时此方法将正在创建的 bean 标记为 创建中的单例( 放进 singletonsCurrentlyInDestruction ),这里就是上面 getSingleton 没有通过的地方 ,点此跳转,第二次就不会被此条件拒绝
    在这里插入图片描述
    DefaultSingletonBeanRegistry 类上文说过,是实例化单例对象的重要类,里面有三个 map,作为三级缓存
    为了方便观察,添加下面的变量
    在这里插入图片描述
    从此位置,即 DefaultSingletonBeanRegistry.getSingleton的 2 处 (点击跳转)
    在这里插入图片描述
    进入上文 单例截图,即 AbstractBeanFactory.doGetBean 的 2 处(点击跳转)
    进而进入 AbstractAutowireCapableBeanFactory 接下来是一大串这个类中逻辑
    从上文AbstractBeanFactory.doGetBean 会进入此类的 createBean 中,可以发现其中有个 doCreateBean

    一般,doXX 方法是 XX 方法中的干货
    因为一些比较周边的逻辑确实算是 XX 逻辑的一部分,但是这些逻辑没有实际解决问题(比如参数校验啥的) ,在这些逻辑中间通常夹着实际解决问题的逻辑(可以简称为干货逻辑)
    而这两种逻辑混在一起并且很长时,阅读者很难区分二者,这时,需要将干货逻辑抽取方法,单独存在
    但是,这个干货方法的作用实际上就是 XX,于是常规套路命名为 doXX 以作区分(和强调)

    在这里插入图片描述

    首先在 doCreateBean 中,尝试创建对象
    在这里插入图片描述
    获取 bean 的类型,随后通常在下图 2 处尝试是否指定了非空参构造器,如果没有,在 1 处用空参构造器创建对象并返回
    在这里插入图片描述
    在这里插入图片描述
    然后在 doCreateBean 中,判断当前是否可能存在循环依赖
    如果可能(正创建的 bean 是单例,系统参数运行并且已经标记为创建中),则需要将当前 bean 加入工厂缓存,如下图红框
    在这里插入图片描述
    具体逻辑如下
    在这里插入图片描述
    此时可以从以前的表达式看到 AService 已经进入工厂缓存
    在这里插入图片描述

    上图中有个从早期预览缓存删除的逻辑,这是因为在循环依赖场景,标记了 bean 为创建中,但还没有创建出实例(没在完全缓存中),同时系统参数允许,就从这里快速获得,同时将单例放到此缓存中,从注释看,涉及到一个 full singleton lock
    在这里插入图片描述

    接下来 springboot 会尝试给刚刚创建的 AService 的 bean 填充属性(populate 有输入数据的意思)
    在这里插入图片描述
    进入此方法,先尝试获取属性值,现在当然没有
    在这里插入图片描述
    判断是否有创建 bean 过程中需要使用的 beanPostProcessor
    在这里插入图片描述
    从下面的监控里,可以看到有 4 个,其中明显红框处是当前场景真正需要的

    • 使用 @Resource 时,通过 CommonAnnotationBeanPostProcessor 完成注入
    • 使用 @Autowired 时,通过 AutowiredAnnotationBeanPostProcessor 完成注入
      在这里插入图片描述

    循环至 CommonAnnotationBeanPostProcessor,进入 postProcessProperties ,如图中步骤
    这里发现 bean,即实例化出来的 AService 中有个 BServiceb 变量
    并通过下面的 inject 方法注入
    在这里插入图片描述
    InjectionMetadata.inject 通过其内部类 InjectedElement 完成注入
    在这里插入图片描述
    在这里,获取用于注入的资源,后面如果获取不到就创建,整个过程经过 CommonAnnotationBeanPostProcessor 内部类 ResourceElement.getResourceToInjectCommonAnnotationBeanPostProcessor.getResourceCommonAnnotationBeanPostProcessor.autowireResourceDefaultListableBeanFactory.resolveDependency
    最终进入 DefaultListableBeanFactory.doResolveDependency,如下面系列截图
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    最终到达 DefaultListableBeanFactory.doResolveDependency
    在这里插入图片描述
    doResolveDependency 会在获取注入类型后尝试获取建议值
    在这里插入图片描述
    如下图,这个建议值是在注解上获取的,可能对应 @Resoutce("xxx") 的场景
    在这里插入图片描述
    在下图取得了需要注入的对象的类型
    在这里插入图片描述
    这里有两种可能性:单实现多实现
    在这里插入图片描述
    一般情况都是单实现,进入 DependencyDescriptor.resolveCandidate 方法
    在这里插入图片描述
    进去后看到一个有点眼熟的东西,继续向下,又一次进入 AbstractBeanFactory.doGetBean
    这里已经是创建被依赖的对象的 bean 了
    在这里插入图片描述
    在这里插入图片描述
    路过这里
    在这里插入图片描述
    又一次通过 DefaultSingletonBeanRegistry.getSingleton 调用 AbstractBeanFactory.doGetBean 的匿名内部类,进入其子类 AbstractAutowireCapableBeanFactory.createBean
    在这里插入图片描述
    随后又进入其中的 doCreateBean,并在红框处创建实例
    在这里插入图片描述
    并判断当前是不是可能出现循环依赖,如果是,继续扔到工厂缓存中,如下图 1 处
    在这里插入图片描述
    在这里插入图片描述

    然后给当前 bean 填充属性,如上图 2 处,注意 此时的当前 bean 已经是 BService

    在这里插入图片描述

    有一次循环至 CommonAnnotationBeanPostProcessor,进入 postProcessProperties,通过 InjectionMetadata.inject 与其内部类 InjectedElement 注入
    在这里插入图片描述
    在这里插入图片描述

    尝试注入时,继续获取被注入值的资源

    在这里插入图片描述
    和创建 AService 时一样,路过 CommonAnnotationBeanPostProcessor 内部类 ResourceElement.getResourceToInjectCommonAnnotationBeanPostProcessor.getResourceCommonAnnotationBeanPostProcessor.autowireResourceDefaultListableBeanFactory.resolveDependency
    最终进入 DefaultListableBeanFactory.doResolveDependency,执行下图代码
    在这里插入图片描述
    第三次通过 getBean,此时又是获取 AService,如下图
    在这里插入图片描述
    在这里插入图片描述

    但这时,AService 已经被标记为创建中,于是通过下图 √ 处的校验,并在 1、2、3 处依次查找各级缓存
    如果是在工厂缓存中找到的 bean ,就对它做一次缓存升级,从 工厂缓存 提升到 早期预览缓存
    在这里插入图片描述
    如此图效果
    在这里插入图片描述
    而回到 AbstractBeanFactory.doGetBean 时,第二次 getBean( AService ) 已经获取到了它的 bean
    在这里插入图片描述
    于是跳过了上次 doGetBean 时走的大段创建过程,到最后一步,从这里返回
    在这里插入图片描述
    回到了 BService 执行了一半的 DefaultListableBeanFactory.doResolveDependency
    在这里插入图片描述

    回到了 CommonAnnotationBeanPostProcessor.autowireResource 并在后面注册依赖
    在这里插入图片描述
    在这里插入图片描述
    从下面变量看,都注册完成了
    在这里插入图片描述
    继续运行,回到了 InjectionMetadata.InjectedElement.inject 通过红框完成注入(红框底层是 Unsafe 类)
    在这里插入图片描述
    在这里插入图片描述

    此时,回到 AbstractAutowireCapableBeanFactory.populateBean 应用属性值(但不是注入的意思,之前已经注入成功了)
    在这里插入图片描述

    继续向后,完成 BServicecreateBean
    在这里插入图片描述

    即完成了 DefaultSingletonBeanRegistry.getSingleton 中 这一步
    在这里插入图片描述
    并在 DefaultSingletonBeanRegistry.getSingleton 最后添加 BService 进完成缓存
    在这里插入图片描述
    Bservice 进入完成缓存,然后删除 工厂缓存(还删了早期预览缓存,并增加了注册单例,但现在不关心)
    在这里插入图片描述

    在这里插入图片描述

    退出 AbstractBeanFactory.doGetBean 完成 BService 的创建,并回到 DependencyDescriptor.resolveCandidate 方法

    在这里插入图片描述

    在这里插入图片描述
    继续,就回到了 DependencyDescriptor.resolveCandidate 并取得了填充 AService 属性 b 的 BService bean
    在这里插入图片描述
    然后和注册 AService 一样,注册 BService 为依赖,接着完成注入
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    最后将 AService 加入完成缓存,完成单例
    在这里插入图片描述
    在这里插入图片描述
    需要注意理解的是,此时,DefaultSingletonBeanRegistry.preInstantiateSingletons 只是在下图的循环中完成了 AService 的创建 这一轮循环,下面会轮到 BService 的创建 的轮次,但 BService 早就创建完成了
    在这里插入图片描述

    §3 总结

    springboot 开始实例化非懒加载单例 bean
    springboot 实例化 A getBean 的轮次开始

    A doGetBean
    A 从缓存中获取失败,因为 A 没有标记为创建中
    A 实例化
    A 标记为创建中,进入 工厂缓存
    A 填充属性,通过 postProcessor
    获取 A 需要填充的字段,并尝试解决它
    尝试解决它 A 需要填充的字段的类型
    尝试解决它 A 需要填充的字段的资源(Resource),DependencyDescriptor.resolveCandidate
    触发 B 的 getBean

    B doGetBean
    B 从缓存中获取失败,因为 B 没有标记为创建中
    B 实例化
    B 标记为创建中,进入 工厂缓存
    B 填充属性,通过 postProcessor
    获取 B 需要填充的字段,并尝试解决它
    尝试解决它 B 需要填充的字段的类型
    尝试解决它 B 需要填充的字段的资源(Resource),DependencyDescriptor.resolveCandidate
    触发 A 的 getBean

    A doGetBean
    A 从缓存中获取成功,返回
    B 需要填充的字段的资源(Resource) 获取成功
    回到 postProcessor,完成 B 中 a 的注入
    B 添加到完成缓存,完成 B 的单例注册
    B 创建完成(实例化完成)

    ==回到 A 触发的 B 的 getBean ==(已完成)
    A 需要填充的字段的资源(Resource)已经有了,刚刚 getBean 返回的就是
    回到 postProcessor,完成 A 中 b 的注入
    A 添加到完成缓存,完成 B 的单例注册
    A 创建完成(实例化完成)

    springboot 实例化 A getBean 的轮次结束
    springboot 实例化 B getBean 的轮次结束

    B doGetBean
    B 从缓存中获取成功

    springboot 实例化 B getBean 的轮次结束

  • 相关阅读:
    SIP没有摘机消息可以通话吗
    选择结构——分段函数练习题2
    2023学生党护眼台灯怎么样选择?盘点公认好用的护眼台灯
    死锁(知识体系架构和详细解释)
    【10】leetcode note
    scrm系统哪款好?快鲸scrm私域流量运营专家
    [附源码]java毕业设计线上图书销售管理系统
    Docker连接Mysql
    Java读取单个大文件的json数据避免内存溢出
    go~在阿里mse上使用redis.call
  • 原文地址:https://blog.csdn.net/ZEUS00456/article/details/126655676