SpringBoot启动流程浅析

基于SpringBoot 2.1.5

SpringBootSpring Framework的基础上增加的自动配置(约定优于配置)特性能够让开发人员更少的关注底层而带来更快的开发速度。但是由此带来的弊端是开发人员过于依赖完善的框架功能而没有去深入细节,只知其然而不知其所以然。由此记录一下最近在读的SpringBoot的源码并记录一下SpringBoot的启动流程

SpringBoot的启动逻辑在SpringApplication这个类中,通过构造一个SpringApplication并调用run方法启动SpringBoot应用程序。SpringBoot启动后的主要流程:

  1. 设置webApplicationType(web应用类型)
    webApplicationType是启动流程中一个比较重要的属性,SpringBoot根据它的类型来创建Environment对象和应用上下文对象(ApplicationContext)

  2. 准备应用上下文环境(Environment)
    根据上一步推断的webApplicationType创建不同类型的Environment,并且将用户的profile文件读取到Environment中

  3. 读取profile

  4. 创建并配置应用上下文对象(ApplicationContext)
    根据webApplicationType创建不同实现的ApplicationContext

  5. 刷新应用上下文对象(refresh)
    AbstractApplicationContext抽象类定义了上下文对象初始化核心流程,SpringBootBeanFactoryPostProcessor的方式实现包扫描、自动配置,将Bean预先加载成BeanDefinition后并实例化

  6. 后续处理
    发布应用已启动事件并且调用容器中的Runner

一、设置应用类型

当前的web应用类型webApplicationType)是在SpringApplication构造函数中设置的,设置的逻辑在WebApplicationType.deduceFromClasspath中:

   if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) 
                           && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                           && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
       return WebApplicationType.REACTIVE;
   }
   for (String className : SERVLET_INDICATOR_CLASSES) {
       if (!ClassUtils.isPresent(className, null)) {
           return WebApplicationType.NONE;
       }
   }
   return WebApplicationType.SERVLET;

可以看出SpringBoot将应用程序分为三种类型:

  1. Reactive
    Spring团队推出的Reactor编程模型的非阻塞异步Web编程框架WebFlux

  2. Servlet
    基于J2EE Servlet API的编程模型,运行在Servlet容器上

  3. None
    非Web应用程序

通过类路径中是否存在WebFlux中的DispatcherhandlerSpringMVC中的DispatcherServletServletConfigurableWebApplicationContext来推断Web应用程序类型

二、准备应用上下文环境(Environment)

EnvironmentSpringFramework中一个很重要的接口,用于存放应用程序配置信息

PropertySourceorg.springframework.core.env.PropertySource)是用来将一个对象以键值对的形式表示,Spring将多种来源的属性键值对转换成PropertySource来表示

//SpringApplication的prepareEnvironment方法
    private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();//创建环境对象
        configureEnvironment(environment, applicationArguments.getSourceArgs());//配置环境对象;主要是根据命令行参数配置profile
        listeners.environmentPrepared(environment);//发布应用环境已准备事件
        bindToSpringApplication(environment);//绑定spring.main属性到SpringApplication对象中
        if (!this.isCustomEnvironment) {
            environment = new EnvironmentConverter(getClassLoader())
                    .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());//如果用户设置的spring.main.web-application-type和spring推断的类型不一致,则使用用户设置的类型,创建对应的环境对象
        }
        ConfigurationPropertySources.attach(environment);//添加一个名为configurationProperties的PropertySource
        return environment;
    }

在这一步,SpringApplication做了:

  1. 创建Environment对象
    getOrCreateEnvironment方法中,会根据之前推断的webApplicationType(web程序类型)创建不同了实现的Environment对象

  2. 配置Environment对象

    1. 应用程序如果有命令行参数,则在Environment中添加一个与这个命令行参数相关的PropertySource
    2. 根据命令行参数中spring.profiles.active属性配置Environment对象中的activeProfile
  3. 发布ApplicationEnvironmentPreparedEvent(应用环境已准备)事件

    SpringApplication发布完这个事件后,一个类型为ConfigFileApplicationListener的监听器会监听这个事件,它会去读取用户设置的profile文件(读取profile的详细流程在下一步中)

  4. Environment中的spring.main属性绑定到SpringAppilcation对象中

    在执行到这一步时,Environment中已经包含了用户设置的profile文件属性

  5. 转换Environment对象的类型
    在上一步中,如果用户使用spring.main.web-application-type属性手动设置了应用程序的webApplicationType并且用户设置的类型与SpringApplication推断出来的不一致,则SpringApplication会将环境对象转换成用户设置的webApplicationType相关的类型

三、读取profile

在创建Environment对象前,SpringAppilcation已经将当前类路径jar包下所有spring.factories文件中的ApplicationListener加载并实例化完毕。

ApplicationListenerSpring Framework中的监听器接口,用来监听应用程序发布的事件

监听器列表中有一个类型为ConfigFileApplicationListener的监听器,当监听到ApplicationEnvironmentPreparedEvent事件时,它会从所有spring.factories中加载EnvironmentPostProcessor(环境后处理器)并执行他们的postProcessEnvironment方法(这个监听器本身也是一个环境后处理器,所以它也会执行自身的postProcessEnvironment方法,在这个方法中加载了用户设置的profile并以PropertySource的形式添加到Environment中)。

ConfigFileApplicationListener最终会构造一个Loader的内部类并调用Loader.load()方法加载profile;在Loader的构造函数中,会去加载所有spring.factories中的PropertySourceLoaderSpringBoot提供了两个PropertySourceLoader

  • PropertiesPropertySourceLoader(用来加载properties、xml文件)
  • YamlPropertySourceLoader(用来加载yml、yaml文件)

Loader.load()加载profile的伪代码:

  • 配置文件目录
    如果用户设置了spring.config.location属性(用","分隔开表示多个),则使用这个属性值作为配置文件目录;否则使用默认的目录(classpath:/,classpath:/config/,file:./,file:./config/)和spring.config.additional-location设置的并集作为配置文件目录

  • 配置文件名称
    如果用户设置了spring.config.name属性(用","分隔表示多个),则使用这个属性作为配置文件名称;否则使用application作为配置文件名

  • 文件扩展名

//此时还没有加载profile,因此两个属性的值只能通过命令行参数读取到
let profiels = spring.profiles.active和spring.profiles.include设置的值  
for(profile : profiles){//profiles是一个双向队列
    for(目录 : 配置文件目录集合){
        let 配置文件名集合 = 集合;
        for(文件名 : 配置文件名集合){
            for(loader : propertySourceLoader){
                for(文件扩展名 : loader.文件扩展名){
                    load(profile,目录 + 文件名 + "-" + profile + "." + 文件扩展名);
                }
            }
        }
    }
}
在load(加载)时,如果从当前的`profile`中读取到了`spring.profiles.active`和`spring.profiles.include`属性,会把解析出来的profile放入profiles中

通过以目录 + 文件名 + "-" + profileName + "." + 文件扩展名的组合方式加载profile,并将profilePropertySource的形式添加到Environment中。

profile属性的优先级问题:如果在多个profile中设置了同一个名称的属性,profile属性生效的规则是怎样的?

  • 同名的profile:根据文件后缀优先级 properties > xml > yml > yaml

  • 被引用的profile:例一个profile中同时有spring.profiles.activespring.profiles.include属性,则active的优先级 > include的优先级

  • 默认的profile优先级别最低

  • 如果把默认的profile当做第一级profile,在第一级profile中引用的profile(使用spring.profiles.active或者spring.profiles.include引用)当做下一级的profile,则下一级的profile(可能多个)优先级高于前一级的profile(一个),多个profile整体的优先级为第一级的profile优先级;例:
    application.yml内容:
    spring.profiles.active=p1,p2
    spring.profiles.include=p3,p4

    application-p1.yml内容:
    spring.profiles.include=p5
    p2 > p1 > p4 > p3 > default 其中p5 > p1,结果是p2 > p5 > p1 > p4 > p3 > default

四、创建并配置应用上下文对象

ApplicationContextSpring Framework中最核心的接口,用来表示一个应用的上下文;功能包括事件发布、国际化等,同时它也是一个BeanFactory

SpringApplication通过webApplicationType的类型来创建不同的ApplicationContext,以SERVLET类型的webApplicationType为例,SpringApplication会创建类型为AnnotationConfigServletWebServerApplicationContext的上下文对象;

SpringApplicationprepareContext方法中对上下文对象进行预配置,主要做了

  • 执行所有ApplicationContextInitializerinitialize方法
    这些ApplicationContextInitializer是在SpringApplication中的构造函数中加载的(通过读取spring.factories加载)
  • 发布ApplicationContextInitializedEvent(上下文已初始化)事件
  • 发布ApplicationPreparedEvent(上下文已准备)事件

五、刷新应用上下文对象

这里是ApplicationContext真正开始初始化容器和创建bean的阶段,其中bean的整个生命周期可以从这一步骤看出来;Spring Framework中的所有ApplicationContext实现都直接或间接继承自AbstracttApplicationContext,它的refresh方法描述了整个上下文的初始化逻辑

AnnotationConfigServletWebServerApplicationContext(当应用的webApplicationType为Servlet时使用)这个实现类为例

AbstractApplicationContext的refresh方法

synchronized (this.startupShutdownMonitor) {
                prepareRefresh();//(1)
                ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();//(2)
                prepareBeanFactory(beanFactory);//(3)
                try {
                    postProcessBeanFactory(beanFactory);//(4)
                    invokeBeanFactoryPostProcessors(beanFactory);//(5)
                    registerBeanPostProcessors(beanFactory);//(6)
                    initMessageSource();//(7)
                    initApplicationEventMulticaster();//(8)
                    onRefresh();//(9)
                    registerListeners();//(10)
                    finishBeanFactoryInitialization(beanFactory);//(11)
                    finishRefresh();//(12)
                }catch (BeansException ex) {
                    //。。。。。。
                }
                finally {
                    resetCommonCaches();
                }
}
1. 准备更新上下文时的预备工作:
  • 初始化PropertySource
  • 验证Enrivonment中必要的属性
2. 获取上下文的内部BeanFactory

内部BeanFactory的实现类是DefaultListableBeanFactory

3. 对BeanFactory做些预备工作:
  • 设置BeanFactoryBean类加载器、Bean表达式解析器、属性编辑器注册表
  • 添加类型为ApplicationContextAwareProcessorApplicationListenerDetectorBeanPostProcessor
  • BeanFactory在自动装配时忽略一些接口类型
  • 注册可解析的依赖(自动装配时碰到这些类型直接注入,包括BeanFactoryResourceLoaderApplicationEventPublisherApplicationContext
  • BeanFactory中注册一些单例对象,包括environmentsystemPropertiessystemEnvironment
4. 对BeanFactory进行预处理
  • 添加一个WebApplicationContextServletContextAwareProcessorBeanPostProcessor
  • 使BeanFactory自动装配时忽略ServletContextAware接口
  • BeanFactory中注册requestsession两种scope
  • 注册可解析的依赖(自动装配时碰到这些类型可以注解注入,包括ServletRequestServletResponseHttpSessionWebRequest)
5. 执行容器中的BeanFactoryPostProcessor执行到这时容器已经注册了三个BeanFactoryPostProcessor,分别为
  • SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
    ApplicationContexttInitializer初始化时注册
  • ConfigurationWarningsApplicationContextInitializer#ConfigurationWarningsPostProcessor
    ApplicationContexttInitializer初始化时注册
  • ConfigFileApplicationListener$PropertySourceOrderingPostProcessor
    ApplicationPreparedEvent事件发布时由ConfigFileApplicationListener注册

BeanDefinitionRegistryPostProcessor是一种特殊的BeanFactoryPostProcessor,可以对BeanDefinition的注册表进行预处理

  1. BeanFactory中找到已注册的BeanFactoryPostProcessor,执行其中类型为BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法

  2. 循环从BeanFactory中获取BeanDefinitionRegistryPostProcessor(从BeanDefinition注册表中获取,和上一步的来源不一样);有一个ConfigurationClassPostProcessorApplicationContext的构造函数中注册的)
    ConfigurationClassPostProcessor会执行SpringBoot的自动装配功能,将spring.factories中类型为EnableAutoConfiguration的类读取成BeanDefinition并过滤掉不满足条件的然后注册到BeanFactory中。详细步骤在下一章

    这一步骤会不断从BeanFactory中获取没有执行的BeanDefinitionRegistryPostProcessor并执行(可能用户会里面注册同类型的处理器)直到没有找到新的BeanDefinitionRegistryPostProcessor

    包扫描、自动装配的功能都在ConfigurationClassPostProcessor中完成,执行完这一步后,所有Bean都会加载成BeanDefinition放入容器中

  3. 执行他们的postProcessBeanFactory方法对BeanFactory进行后处理

6. 注册BeanPostProcessor

BeanPostProcessor:Bean生命周期的钩子,允许用户对实例化后的Bean进行操作

  1. 从BeanFactory中获取所有BeanPostProcessor

  2. BeanFactory中注册一个类型为BeanPostProcessorChecker的BeanPostProcessor

  3. 将所有BeanPostProcessor按照实现了PriorityOrderedOrdered没有实现排序接口的顺序注册所有BeanPostProcessorBeanFactory

  4. BeanFactory中注册一个类型为ApplicationListenerDetectorBeanPostProcessor

7. 初始化MessageSource(国际化相关)//忽略
8. 初始化容器事件广播器(用来发布事件)
  • 构造了一个SimpleApplicationEventMulticaster当成默认的事件广播器
9. 初始化一些特殊的Bean,主要做了:
  1. 初始化ThemeSource(跟国际化相关的接口)
  2. 创建WebServer
10. 将所有监听器注册到前两步创建的事件广播器中
11. 结束BeanFactory的初始化工作(这一步主要用来将所有的单例BeanDefinition实例化)
  1. BeanFactory中获取所有的BeanDefinitionbeanName并遍历

  2. Bean执行所有已注册的InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation方法(如果这个方法返回了一个Bean,Spring不会对这个Bean的属性进行注入,并且这个Bean的生命周期也会缺少几个步骤)
    PS:只要其中有一个方法的返回值不为null,则会立即返回这个Bean,这个Bean的生命周期和正常的Bean不同(Spring对这个类型的BeanPostProcessor的注释是让它有机会能返回一个代理对象)

  3. 实例化bean

  4. Bean执行所有MergedBeanDefinitionPostProcessor.prostProcessMergedBeanDefinition(用来修改BeanDefinition的信息)

  5. 对Bean属性进行填充(还有利用BeanPostProcessor对特殊Bean创建代理等暂时不讨论)

    1. 获取BeanFactory中所有InstantiationAwareBeanPostProcessor,对Bean执行postProcessAfterInstantiation方法(通常,这个方法应该返回true,如果返回false,后续的postProcessAfterInstantiation方法就不会执行了)
    2. 同上,获取所有InstantiationAwareBeanPostProcessor,对每一个InstantiationAwareBeanPostProcessor分两次调用
      1.postProcessProperties,如果返回null,则继续调用下一步
      1. postProcessPropertyValues(返回的PropertyValues是最终使用的PropertyValues。如果这一步返回null,则不会执行后面的InstantiationAwareBeanPostProcessor
      2. 如果上一步返回的PropertyValues有属性,则将属性应用到bean
  6. 对实现了BeanNameAwareBeanClassLoaderAwareBeanFactoryAware的接口进行接口调用

  7. Bean执行BeanPostProcessor.postProcessBeforeInitialization方法

  8. 对实现了InitializingBeanBean调用接口方法,然后调用init-method(可以是@PostConstruct标注的方法)

  9. Bean执行PostProcessor.postProcessAfterInitialization方法

  10. 如果Bean是一个SmartInitializingSingleton,则调用BeanafterSingletonsInstantiated方法

  • 对于EnvironmentAwareResourceLoaderAwareApplicationEventPublisherAwareMessageSourceAwareAppilcationContextAware等接口,是使用ApplicationContextAwareProcessor这个BeanPostProcessorpostProcessBeforeInitialization方法)实现调用的
  • 步骤2中,如果postProcessBeforeInstantiation返回了一个bean,则立马会对bean执行步骤8的PosttProcessor.postProcessAfterInitialization方法
12.afterRefresh(上下文刷新完毕)
  1. 初始LifecycleProcessor(生命周期处理器),向BeanFactory注册一个DefaultLifecycleProcessor
  2. 调用LifecycleProcessoronrefresh方法(找到所有已注册的SmartLifecycle,根据isRunningisAutoStartup的条件判断,执行SmartLifecyclestart方法)
  3. ServletWebServerApplicationContetxt中,启动了WebServer并发布了ServletWebServerInitializedEvent事件
总结:
Spring容器中单例对象的生命周期
  1. 使用BeanFactoryPostProcessor对BeanFactory进行预处理

  2. 对未实例化的Bean调用InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation方法

  3. 实例化Bean(此时会调用Bean构造函数)

  4. Bean调用MergedBeanDefinitionPostProcessor.prostProcessMergedBeanDefinition可用来修改BeanDefinition信息

  5. Bean调用InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation方法

  6. Bean调用InstantiationAwareBeanPostProcessor.postProcessProperties可用来修改Bean的属性配置

  7. Bean调用InstantiationAwareBeanPostProcessor.postProcessPropertyValues可用来修改Bean的属性配置

  8. 对实现了BeanNameAware、BeanClassLoaderAware、BeanFactoryAwareBean进行接口调用注入属性

  9. Bean调用BeanPostProcessor.postProcessBeforeInitialization可对实例化的Bean进行操作

  10. 对实现了InitializingBeanBean调用接口方法,然后有初始化方法(可以是@PostConstruct标注的方法)的话调用初始化方法

  11. Bean执行PostProcessor.postProcessAfterInitialization方法可对实例化的Bean进行操作

  12. 如果Bean实现了SmartInitializingSingleton接口,则调用Bean的afterSingletonsInstantiated方法

SpringApplicationrefresh方法调用结束后,在JVM上注册了一个shutdownHook,JVM正常关闭时会调用,其中做了一些资源清理和调用Beanclose的方法工作(单例Bean生命周期的一部分)

六、后续处理

  • 发布应用程序已启动(ApplicationStartedEvent)事件
  • BeanFactory中获取所有ApplicationRunnerCommandLineRunner并调用他们的run方法
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容