Spring Boot 启动原理及自动配置原理

阿凡达2018-07-20 11:40

没有锚点可以用,如果觉得前面太啰嗦了可以自行跳到后面的配置处理原理


以 SpringApplication 类作为切入
SpringApplication 初始化
实例 SpringApplication 时需要传入一个 source,后续会根据这个 source 执行初始化
1. 将 source 存储到 set 中
2. 推断是否为 web 环境
只有 classpath 中存在指定类才算是 web 环境
3. 设置 initializers (一系列的 ApplicationContextInitializer)
4. 同理设置 listeners (一系列的 ApplicationListener)
5. 获取当前在执行的 main class 类(通过抛出一个 RuntimeException 然后遍历所有堆栈信息找到 main 方法的那条)
上述执行完就准备完如下的实例,SpingApplication 类也就构造完成了

容器启动
执行 SpringApplication.run 方法开始后续一整套的容器启动,返回 ConfigurableApplicationContext
1. 创建 StopWatch 实例,并调用它的 start 方法(该类主要用于记录运行时间,通过成对的 start / stop 方法)
2. 配置 headless 属性,默认是 true
3. 构造 SpringApplicationRunListeners,内部维护了一系列的 SpringApplicationRunListener(来源于 spring.factories 配置文件中的 SpringApplicationRunListener),以实现类 EventPublishingRunListener 为例
通过其构造函数构造,内部存储了 SpringApplication,以及 main 方法中的 args
构造 SimpleApplicationEventMulticaster 事件多路广播,遍历 SpringApplication 构造时候创建的 listeners,执行 ApplicationListener 的注册
上述执行的意思是由 SpringApplicationRunListeners 统一调度监听器,其中一个监听器 EventPublishingRunListener 又是负责统一广播
4. 调用创建出来的 SpringApplicationRunListeners 的 starting 方法触发所有 SpringApplicationRunListener 的 starting 方法,所以就会触发到 EventPublishRunListener 的 starting 方法,将 ApplicationStartedEvent 事件广播到所有的 listener 中(广播是委托给了 SimpleApplicationEventMulticaster)
调用 getApplicationListeners 获取所有满足当前事件类型的 ApplicationListener(内部执行则是将满足当前事件类型的 ApplicationListener 列表缓存起来,每个 ApplicationListener 都有各自支持的事件列表)
invokeListener 就是事件具体执行的地方,以 LoggingApplicationListener 为例就是查找所有日志实现类,然后调用它的 beforeInitialize 方法(所有日志类都继承了 LoggingSystem 抽象类,所以可以以多态形式调用)

5. 构造 DefaultApplicationArguments
内部会构造一个 SimpleCommandLineArgsParser 来解析输入的命令行参数,解析逻辑为
5.1 参数以 -- 为前缀的,获取 name 以及 value(name 必须要有,value 可以没有,常见有 -- server.port = 8080)
将结果存储到了 Map<String, List<String>> 类型的 optionArgs 中
5.2 参数中不以 -- 为前缀的则直接将值添加到 nonOptionArgs 中
6. 准备环境

6.1 创建 environment,web 情况下默认是 StandardServletEnvironment

6.2 根据命令行参数配置 environment,也就是往 environment 的 propertySourceList 中添加 propertySource(SimpleCommandLinePropertySource)(获取值会遍历所有的 propertySource,不同 PropertySource 的 getProperty 实现逻辑不一样,CommandLinePropertySource 就是直接从 main 方法的 args 中获取,而且 spring 的做法是先用参数构造各样的 PropertySource,调用的时候才会用到这些参数),以及配置 environment 的 activeProfiles(默认的 profile 为 default)、
6.3 广播 ApplicationEnvironmentPreparedEvent
以 ConfigFileApplicationListener 为例,它会加载所有的 EnvironmentPostProcessor(一样是 spring.factories 配置文件中映射的 EnvironmentPostProcessor),然后调用它们的 postProcessEnvironment 方法,包括自身,因为ConfigFileApplication 也实现了 EnvironmentPostProcessor 接口
以 ConfigFileApplicationListener 为例
6.3.1
添加一个 RandomValuePropertySource 到 environment 中用于获得随机值
添加 PropertySourceLoader(内部维护了 MutablePropertySources 和 PropertySourceLoaders,PropertySourceLoader 可以是读取 properties 的,也可以是读取 yml 的)
根据配置的 profile 去找配置文件,默认的配置文件名是 application 开头的,而默认的查找路径优先级则是
如果项目中没有配置 spring.profiles.active,而且项目中配置文件是 application.yml,那么最终得到的会是一个 Map 一样的数据结构,跟 yml 中配置的一一对应
最终 environment 中会有一系列的 PropertySource,最后一个即是 yml 等配置文件解析出来的,所以本意还是丰富 environment 中的 propertySourceList
6.3.2 设置 System Property 中的 spring.beaninfo.ignore 属性
6.3.4 bindToSpringApplication(支持覆盖 SpringApplication 属性)
7. 打印 banner,默认是 System.out.print。打印的内容中的占位符也会被 environment 中的变量替换
8. 创建 ApplicationContext

web 的默认是构造 AnnotationConfigEmbeddedWebApplicationContext

它继承了 EmbededWebApplicationContext,都是 boot 中增加的 ApplicationContext 类
AnnotationConfigEmbeddedWebApplicationContext 构造函数中实例了两个工具
AnnotationBeanDefinitionReader 是 Spring 中负责注册的(内部有个重要的 conditionEvaluator)。构造的时候会做很重要的一件事,执行 AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)

也就是这里往容器中注册了一系列的 BeanPostProcessor 和 BeanFactoryPostProcessor

ClassPathBeanDefinitionScanner 是 Spring 中扫描获取 BeanDefinition 的
9. 创建失败分析工具
加载 spring.factories 中配置的一系列 FailureAnalyzer,遍历所有的 analyzer 执行 setBeanFactory(DefaultListableBeanFactory)
10. 准备上下文
10.1 设置上下文的环境为前面创建的 environment
10.2 根据情况设置相关的 BeanNameGenerator 及 ResourLoader
10.3 执行 SpringApplication 中 initializers(initializers 也就是 spring.factories 中映射的 ApplicationContextInitializer)的 initialize 方法
10.4 将先前的 ApplicationArguments 以及 Banner 注册单例 bean 形式到上下文中
10.5 执行启动类的 BeanDefinition 注册
根据 SpringApplication 实例时传入的类作为 Source 创建 BeanDefinitionLoader,然后按照 Source 执行加载。加载会判断 Source 类是否含有 @Component 注解(大部分都注解都继承了 @Component),只有 @Component 注解了的才会执行注册
这里先介绍下 boot 提供的几个注解, @Configuration、@SpringBootConfiguration、@SpringBootApplication。其中 @SpringBootApplication 继承 @SpringBootConfiguration, @SpringBootConfiguration 继承 @Configuration,@Configuration 的元注解又是 @Component
委托 AnnotatedBeanDefinitionReader.register(source) 执行注册 bean
由 source 构造一个 AnnotatedGenericBeanDefinition
委托 conditionEvaluator 判断该 beanDefinition 是否应该跳过,方式则是检查 source 上是否带着 @Conditional 注解,此处 source 不会被跳过
后续就是注册到容器中
10.6 执行 listeners.contextLoaded
主要是将 SpringApplication 中的 listener 往上下文中注册,然后广播 ApplicationPreparedEvent 事件(应用准备好了事件)。事件处理主要做了几件事
1. 往上下文中添加 PropertySourceOrderingPostProcessor(为 PropertySource 排序)
2. 往容器中注册单例的 springBootLoggingSystem(日志用)
11. 执行 refresh
至此就是很熟悉的 applicationContext.refresh 了
12. 调用 ApplicationRunner 接口及 CommandLineRunner 接口

13. 构造 ApplicationReadyEvent 或者 ApplicationFailedEvent 事件然后 publish 到 context 中(由于 listener 已经都注册到 context 中了,所以可以直接使用 context.publishEvent)
14. 调用 stopWatch.stop
15. 打印日志
配置处理
至此你会好奇 boot 中的一些注解是怎么支持的,其实它跟以前一样,比如 BeanFactoryPostProcessor 来处理 @Configuration,具体类为 ConfigurationClassPostProcessor
它主要做几件事:
1. 遍历容器中所有的 BeanDefinition(包含 source)
由于之前 source 构造了一个 AnnotatedGenericBeanDefinition,所以它也会作为候选 BeanDefinition。
1.1 检查 BeanDefinition 代表类元数据上是否存在 @Configuration 注解,存在则设置 full 属性到 BeanDefinition 的 attribute 中
1.2 检查 BeanDefinition 代表类元数据上是否存在 @Component,@ComponentScan, @Import,@ImportResource 等注解或存在 @Bean 注解方法,存在则设置 lite 属性到 BeanDefinition 的 attribute 中
2. 构造 ConfigurationClassParser,遍历候选类
3. 对候选类执行 ConfigurationClassParser.parse

3.1 对候选类中的内部类先执行解析(递归 parse)

3.2 处理 @PropertySource 注解(可以配置了多个,因为 @PropertySource 是 repeatable)
依次寻找 @PropertySource 中配置的配置文件读取形成 ResourcePropertySource(继承了 PropertiesPropertySource),并加入到环境的 propertySources 中,所以环境中 propertySources 如下
ResourcePropertySource 为 properties 配置文件,ConfigurationPropertySources 为 application.yml 配置文件
3.3 处理 @ComponentScan 注解(@SpringBootApplication 注解上带了 @ComponentScan 注解,所有值都是默认值)
和先前 spring 扫描一样,扫描项目路径下的类。注意 @Configuration 的元注解是 @Component,所以注解了 @Configuration 的类也会被扫描到,这时就会触发递归 parse。同时 boot 也是利用这点作为自动配置的扩展点,需要自动配置的提供配置类并标注 @Configuration 注解,而后增加 @Conditional 注解控制是否实例(例如只有当 XX 类存在时才实例)。如果 @Component 注解的类上存在 @Import 注解的话也会触发递归 parse(一样会先触发内部类的解析)
3.4 处理 @Import 注解
查找是否有 @Import 注解(会递归元注解信息查找 @Import),有则读取 value 值
如果 value 指定的类实现了 ImportSelector 接口,实例该类并执行 Aware 相应设置,然后执行 selectImports 决定 import 哪个,递归当前 import 的解析(实现 ImportSelector 接口使得注解通过代码控制不同场景 import 不同配置类)
如果 value 指定的类实现了 ImportBeanDefinitionRegistar 接口,实例该类并执行 Aware 相应的设置,然后将 实例类 -> metadata 映射存储在 ConfigurationClass (递归 parse 每次都会产生新的 ConfigurationClass)的 importBeanDefinitionRegistrars 中
如果都不是,将 value 指定的类作为一个 @Configuration 配置类处理。往 importStack 中 registerImport(暂时不知道干嘛的,有兴趣自己跟进代码),然后还是递归 parse(可以理解为 import 配置)
3.5 处理 @ImportResource 注解
3.6 处理配置类中的 @Bean 方法
获取类中 @Bean 注解了的方法,构造 BeanMethod 添加到 ConfigurationClass 的 beanMethods 集合中
3.7 处理配置类中的默认接口方法
该项主要针对 java8 中的默认接口方法,对默认方法上加了 @Bean 的也构造 BeanMethod 添加到 ConfigurationClass 的 beanMethods 集合中
3.8 递归处理配置类的父类(如果有的话)
parse 执行完后就得到了一系列的 ConfigurationClass
4. 校验 @Configuration 注解类
类不能为 final,@Bean 注解的方法必须是能重写的(即不能是 final,private)
5. 构造 ConfigurationClassBeanDefinitionReader 来加载 BeanDefinition
这里也是具体执行注册到容器的地方,遍历所有 ConfigurationClass
5.1 如果 ConfigurationClass 是内部类的配置文件或者 @Import 注解指定的配置类或者另外的 @Configuration 注解的配置类等情况,将其作为一个 Bean 注册到容器中
5.2 所有 @Bean 注解的方法注册 Bean 到容器中,bean 名字默认为方法名
如果 @Bean 注解的方法是 static 的话则使用 beanClass 和 factoryMethodName 形式产生实例
如果不是则使用 factoryBeanName 和 uniqueFactoryMethodName 形式产生实例
最后一样将其注册到容器中
5.3 从 @ImportSource 中加载 Bean
5.4 处理 @Import 注解产生的 importBeanDefinitionRegistrars,就是调用 ImportBeanDefinitionRegistrar.registerBeanDefinitions。
以 AspectJAutoProxyRegistrar 为例就是
而这段逻辑跟之前 spring xml 配置 aspectj-autoproxy 逻辑是一模一样的
6. 检查容器中是否还有可能的未解析的配置类,因为 @Bean 方法产生的 Bean 可能也带有 @Bean 注解的方法等情况。如果存在则执行解析
至此 boot 的启动就解析完了,这里还有几个点
1.
像这种 @Bean 方法中构造实例需要依赖到的 bean 又是通过 @Bean 方法创建出来的,直接使用了方法来获取,按我们本能的想法是他们是两个不同的对象,但为什么可以这样写呢?
原因是因为 Spring 加了个 ConfigurationClassEnhancer,所有的 ConfigurationClass 的 @Bean 方法都会被拦截,执行的时候会先从容器中 getBean,当获取不到时才会执行方法本身
2. @Bean 注解的方法可以结合 @ConditionalOnMissingBean,表示如果容器中不存在 @Bean 指定的 bean 的话执行 @Bean 方法创建 bean
3. @Import 注解注入的配置类上可以不带 @Configuration 注解,因为它的机制就是注入配置,可以本地自定义个配置类,添加个 @Bean 方法测试
个人建议配置类的使用 @Import 注入,这样比散落在项目中各种配置文件好的多,@Import 指定配置类
4. 自动配置
boot 中的自动配置是怎么实现的呢?jar 中的配置又是怎么应用的呢?怎么实现依赖 starter 无需配置直接使用呢?ComponentScan 扫描是不会扫描到 jar 包中的配置的
这归功于 @EnableAutoConfiguration 元注解,该注解 @Import 了 EnableAutoConfigurationImportSelector,它会读取所有 META-INF/spring.factories 文件中 EnableAutoConfiguration 映射的类列表返回,后续就是 import 这些配置类


本文来自网易实践者社区,经作者钱益再授权发布。