本文主要是结合源码,带大家看看springboot的run方法到底做了些什么事,能够让一个项目跑起来,并且通过简单的配置就能完成以前springmvc和spring要做大量配置才能完成的事。
因为本文也是作者第一篇结合源码分析的文章,可能会有些讲解不到位的地方,也欢迎大家留言提出宝贵的建议。小编也会尽可能多的采用一些流程图和源码图解加上自己的注释,帮大家梳理出一条完整的线。这里建议有一定源码阅读量的人阅读本文章,或者可以先看看以下两个视频,然后跟着断点和注释一起看看,不然可能看的不太明白。
这次主要是对run方法的两条主线做一个简单的分析,如下图。
-
线路一 :new SpringApplication(primarySources)
这个方法是 调用了 SpringApplication 的有参构造方法,在这个方法中 设置了spring应用的类型,初始化属性以及ApplicationListener监听的值 ,也是通过这个方法第一次调用 将 spring-boot 和 autoconfigure 两个 jar包 文件中的 META-INF/spring-factories文件的键值对加入缓存。
-
线路二:run(args)
这个方法是完成spring容器和环境配置的核心逻辑方法。其内部包括spring源码中最重要的 refreshContext(context),所有spring的ioc和aop以及容器创建逻辑都在这个refreshContext中。本次就不做重分析这个refreshContext方法了,只要知道spring容器的ioc和aop以及循环依赖等关键问题都是在这个方法中实现的即可,等下次有机会了再专门写一篇文章分析。
图 1
一 SpringApplication 有参构造分析
1.1 源码步骤分析
在run方法往里面点两层后就可以看到如下源码。
这里就是上文说的分的两条主线,我们先看看SpringApplication构造方法都做了些什么事。
再往里层走一层,可以看到如下代码
图 2
我们一步步来看
步骤 | 作用 |
---|---|
第一步 | 设置了 SpringApplication 的 ResourceLoader resourceLoader 资源加载器属性,这里默认设置为null。 |
第二步 | 设置了 SpringApplication的 Set<Class<?>> primarySources 属性, 赋值为我们传入的主启动类,我这里的是 StudySpringBootApplication.class |
第三步 | 设置了SpringApplication 的 webApplicationType的类型是一个servlet |
第四步 | 将jar包中包含spring-factories 文件 且key值为 ApplicationContextInitializer 的 value 值 & 通过构造注入赋值,然后将其 设置为 SpringApplication 的 List<ApplicationContextInitializer<?>> initializers 属性的值 |
第五步 | 将jar包中包含spring-factories 文件 且key值为 ApplicationListener 的 value 值 & 通过构造注入赋值,然后将其设置为 SpringApplication的 List<ApplicationListener<?>> listeners 属性的值 |
第六步 | 设置SpringApplication 的 Class<?> mainApplicationClass 属性 为主启动类的完全限定名,例如我这里的是 com.spring.StudySpringBootApplication |
我们这里要着重了解的是第四步,即SpringBoot中是如何把jar包中的 spring.factories 文件中的 key-value 加载到的?
1.2 SpringBoot如何获取spring-factories 文件中的bean?
1.2.1 认识 spring.factories文件
如果一个项目在没加入什么别的stater,只有一个 spring-boot-starter-web
。那么在 spring-boot-2.2.2.RELEASE. jar 、spring-boot-autoconfigure-2.2.2.RELEASE.jar 这两个jar包下的 META-INF
文件夹下就包含着 一个 spring.factories
文件。
那么这个文件到底是用来做什么的呢?我们先看看文件的内容
spring-boot-2.2.2.RELEASE. jar/META-INF/spring.factories 部分内容
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
可以看到文件内容是一个类似于key-value的格式,当我们选择一个 value 对象点进去看一下。
例如看看这个 SpringApplicationRunListener
这个key-value:
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
看看EventPublishingRunListener 这个类
图 3
可以看到正好这个 EventPublishingRunListener 就实现了 SpringApplicationRunListener 这个接口。
是的,spring.factories 中所有的key-value都符合这个规律 ,即
value值是key的实现类
1.2.2 springboot 如何加载 sping.factories
我们知道了 spring.factories 的基本内容后,那么springboot 又是 如何加载这个spring.factories中的这些bean的呢?加载过后又有什么作用呢?
下面,我们就来具体的看一下。
在 「图 2」 中我们有分析到第四步是去加载 spring.factories 文件,那么它具体是怎么实现的呢?
第四步中使用了 getSpringFactoriesInstances(ApplicationContextInitializer.class)
我们来看看这个方法是如何获取到 key值是 ApplicationContextInitializer的 value值的
图 4
getSpringFactoriesInstances(Class type) 方法在springboot源码中运用的比较多的方法,它主要是用来配置文件 META-INF/spring.factories 下指定key值的所有value 对象实例.
我们可以看看源码具体这个方法是怎么实现的,每一步的关键步骤已经加上了对应的注释,可以结合源码断点 和 我的注释 自己看看。
getSpringFactoriesInstances(Class type) 具体逻辑
图 5
这段源码中重点关注的应该是:
-
获取配文件 META-INF/spring.factories 的关键代码在 SpringFactoriesLoader.loadFactoryNames(type, classLoader) ,可以理解为这是spring的一个加载工具类,它调用用loadFactoryNames 静态方法,传入了要加载的类名和加载器。完成文件的加载
-
从配置文件中加载出来的names其实还是完全限定名,要通过 createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); 创建出对应的实例对象,并通过构造方法给实例对象赋值
1.3 加载文件和构造注入源码解析
1.3.1 加载spirng.factories
「图 5」 中 有说过在 getSpringFactoriesInstances(type, new Class<?>[] {}) 方法中 SpringFactoriesLoader.loadFactoryNames(type, classLoader) 是主要负责 加载 spring.factories的,我们来看看源码具体是怎么写的
SpringFactoriesLoader.loadFactoryNames(type, classLoader) 方法源码解析
图 6
这里可以看到如果是第一次进行调用 loadSpringFactories 这个方法,它会先判断一个缓存中是否为空,如果缓存不为空,那么直接把缓存返回就可以了;如果缓存为空,它会把 spring.factories 中的键值对加入到缓存中去。
这个缓存cache 其实就是SpringApplication 对象的一个Map集合。
图 7
当经过此步骤分析后,SpringApplication 是如何设置 initializers 和 Listeners ,如何加载spring.factories 文件的,你应该明白了吧
当SpringApplication的 cache 缓存有值后,以后每次再使用 loadFactoryNames(type, classLoader) 那么就会直接取缓存中取。
1.3.2 构造器注入
在经过拿到对应key值的 value 后,这时还只是个完全限定名,并不是可以使用的实例对象,于是接下来便会创建实例,并使用 构造器注入 给对应的实例对象赋值。这便是 createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); 做的事,下面来看看源码中这个方法是怎么实现的
createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names)
图 8
看 「 图 8 」 中的代码,首先是循环遍历** loadFactoryNames(type, classLoader) ** 得到的完全限定名。然后用类加载器加载得到了对应限定名的Class对象,接下来就是构造注入的核心代码。
先是通过Class对象得到对应对象的构造对象,然后再用spring自己写的一个BeanUtils 工具类,将构造对象和构造对象的实际要附的值一起传入 instantiateClass方法,返回的就是一个已经通过构造器附上值的实例对象 (感兴趣的同学可以看看 BeanUtils 的 instantiateClass(Constructor ctor, Object… args) 方法是怎么实现的,也不难 )
1.4 设置主应用程序
加载设置完Initializers 和 Listeners 后,我们再看看源码是如何设置主应用程序的,也就是我们的启动类
图 9
1.5 SpringApplication 构造方法流程图总结
图 10
二 SpringApplication的run方法分析
介绍完了SpringApplication的 有参构造,我们再来看看 run方法做了些什么事情吧
图 11
2.1 源码步骤分析
SpringApplication 的 run(String… args) 方法涉及的方法和内容比较多,所以本文会挑几个有代表性的方法深入跟进源码进行具体的讲解。其余的方法可能就仅放出注释跳过了,感兴趣的同学可以自己跟着断点点击进去看看,或者留言告诉小编想要了解具体什么方法,小编下次会再出相应的文章具体讲解。
接下来我也会把一些重点核心步骤和所包含的知识点挑出来具体分析。
源码步骤 1~3
图 12
源码步骤 4~10
图 13
源码步骤 11~14
图 13
2.2 getRunListeners(args);
「图 12」 第一步其实就是生成一个计时器,用于统计整个spring上下文创建完成的事件,这没什么好说的,知道就行。我们来看看第二步
//第二步: 获取所有的SpringApplicationRunListener 集合
//得到Spring运行监听对象-》
//SpringApplicationRunListeners 对象 中有 List<SpringApplicationRunListener> 属性
//这里也是调用了 之前说过的 getSpringFactoriesInstances 方法
//将 EvnetPublishingRunListener
// 放入 listeners 对象中的 listeners (List<SpringApplicationRunListener> ) 属性中
SpringApplicationRunListeners listeners = getRunListeners(args);
看我的注释,这里是得到了一个 EvnetPublishingRunListener 的对象 , 并且把 得到的对象重新封装到了一个 SpringApplicationRunListeners 的 listeners (List ) 属性中 。 那么这里就引发了两个问题:
- 是如何取到这个EventPublishingRunListener的?
- 这个EventPublishingRunListener为什么要封装到 SpringApplicationRunListeners 的属性中?它是做什么用的?
👇🏻我们就来具体结合源码围绕这两个问题讲解一下。
2.2.1 如何获取EventPublishingRunListener对象?
其实springboot大部分获取对象的方法都是使用的 「图 5」 讲解的 getSpringFactoriesInstances(Class type) 方法,我们来看看 getRunListeners怎么写的
getRunListeners(String[] args) 源码如下
图 14
可以看到,源码首先是通过new SpringApplicationRunListeners 来调用 SpringApplicationRunListeners的有参构造方法,然后再构造方法中使用 getSpringFactoriesInstances 方法,获取一个key值为 SpringApplicationRunListener的 value 集合,而 这个集合也就是 EventPublishingRunListener( 感兴趣的可以去 spring-boot-2.2.2.RELEASE. jar/META-INF/spring.factories 找找看看)。
而且他在调用getSpringFactoriesInstances 时它还传了一个 types数组,值是一个 SpringApplication.class 和 String[].class,然后对应的长对象 传递的是 this,即当前SpringApplication对象,和系统参数。
由 「 1.3.2 构造器注入 」,分析可知,它是要将 SpringApplication对象和 系统参数args 作为构造方法的入参 给 EventPublishingRunListener 赋值 。 验证的方法也很简单,我们找一下是否有这样一个有参构造。
EventPublishingRunListener 有参构造
图 15
2.2.2 EventPublishingRunListener的作用
根据上文的介绍,我们已经大概了解了EventPublishingRunListener是怎么获取到的,那么这个对象到底有什么作用呢?下面我们就来重点介绍一下它的作用
EventPublishingRunListener 的简单介绍
EventPublishingRunListener 是事件发布运行监听。主要作用是 用 SimpleApplicationEventMulticaster 完成 SpringApplication启动、ConfigurableEviroment 环境准备好后、ConfigurableApplicationContext 准备好后/加载时/启动后/运行时/失败后 的一些事件发布, 然后调用订阅了对应事件类型的监听的 onApplicationEvent 方法
简单来说,这个类主要是用来完成发布事件,并通知事件对应的监听的。
那么由此我们又可以仔细想想以下两个问题:
- EventPublishingRunListener是怎么发布事件的?
- EventPublishingRunListener又是怎么通知到对应事件指定的监听的呢?
2.2.3 listener.starting() 发布 「应用程序启动中」 事件
下面 以 EventPublishingRunListener 发布SpringApplication启动中事件 ( ApplicationStartingEvent ) 和 通知对应监听 做具体分析
步骤分析
图 16
源码分析
图 17
图 18
发布事件后,多播器就会自己去通知对应类型的监听,那么它是怎么找到对应类型的监听的呢?
2.2.4 如何找到事件对应监听
上文 「 图 17 」 介绍了是由 SimpleApplicationEventMulticaster 完成 ApplicationStartingEvent 事件的发布的,那么又是通过什么方法 找到对应事件类型的监听的呢?
往下跟 multicastEvent (多播事件)方法
图 19
调用判断的逻辑在
AbstractApplicationEventMulticaster 的 retrieveApplicationListeners(检索应用程序监听器) 方法
图 20
接着往下跟
图 21
也就是说当你传入的 listener是 GenericApplicationListener(通用ApplicationRunListener),那么它就会调用你本身方法的supportsEvent 和 supportsSourceType方法对事件类型和数据源类型做相关校验,查看所支持的类型。如果你不是这个类型,那么它就会 new 一个 GenericApplicationListenerAdapter适配器,然后把你的 listener封装进去,在这个适配器中有默认的supportsEventType方法。它会去通过Listener的泛型,查看到所支持的事件。
看看预置的 LoggingApplicationListener监听 supportsEventType 和 supportsSourceType 怎么写的
图 22
2.2.4 小结
通过对 小节 『 2.2.3 listener.starting() 发布 「应用程序启动中」 事件 』 和 『 2.2.4 如何找到事件对应监听 』 大致的了解,我们可以知道SpringApplication 对象在不同的时刻,通过发布不同的事件,通知对应的监听完成了相应的事情。这种模式就叫发布订阅模式,或者观察者模式。
下面是小编为大家总结的SpringApplication中,listener发布事件代码的位置,以及发布的事件类型。
位于run(String… args)的哪个方法 | 具体代码 | 发布事件 | 发布事件类型 |
---|---|---|---|
listeners.starting(); | listeners.starting(); | ApplicationStartingEvent | 应用程序 启动中事件 |
prepareEnvironment (listeners, applicationArguments); | 方法中的listeners. environmentPrepared(environment); | Application EnvironmentPreparedEvent | 应用程序环境准备事件 |
prepareContext (context, environment, listeners, applicationArguments, printedBanner); | 方法中的 listeners.contextPrepared(context); | Application ContextInitializedEvent | 应用程序上下文初始化事件 |
prepareContext(context, environment, listeners, applicationArguments, printedBanner); | 方法中的 listeners.contextLoaded(context); | ApplicationPreparedEvent | 应用程序准备完毕事件 |
listeners.started(context); | listeners.started(context); | ApplicationStartedEvent | 应用程序启动完毕事件 |
listeners.running(context); | listeners.running(context); | ApplicationReadyEvent | 应用程序就绪事件 |
2.3 prepareContext 准备上下文
上文重点介绍了getRunListeners方法获取到的 EventPublishingRunListener如何发布事件和找到对应事件的监听。springboot的run方法中大多是环节,都是用这种发布订阅的模式通知对应的监听驱处理相应的事情。如**「 图 24 」**
【如果对发布订阅/观察者模式这种事件驱动模型不是很了解的,可以先看一下以下文章补充一些基本的概念知识
下面我们来了解run方法中另一个比较重要的方法 prepareContext 方法
prepareContext方法
图 23
2.3.1 run 总流程步骤图
SpringApplication run(String… args) 总流程步骤图
图 24
2.3.2 prepareContext源码步骤分析
图 25
图 26
可以看到 「 图 24 」 和 「 图 25 」 ,我把 prepareContext方法囊括为做了五件事。在源码注释当中分别对应为:
源码步骤 | 所完成的事情 |
---|---|
第一步 | 设置环境对象进自身 |
第二第三步、第五步第八步 | 初始化属性 |
第四步 | 发布应用程序上下文初始化事件 |
第九步 | load方法,把主启动类加入spring容器 |
第十步 | 发布应用程序准备完成事件 |
这里我们重点看看load方法是如何把主启动类加入到Spring容器的。
2.3.3 load 方法源码解析
图 27
AnnotatedBeanDefinitionReader的register方法注册主启动类
图 28
「 图 27 」 和 「 图 28 」 ,为我们展示了load方法实际上就是,创建了一个 BeanDefinitionLoader 对象,然后通过这个对象的构造方法创建了一个 AnnotateBeanDefinitionReader 对象,通过这个对象的register方法,把主启动类注册到了spring容器当中。
那么,我们来看看他具体的注册逻辑是怎么写的。
2.3.4 注册主启动类到spring容器
因为 register 方法的调用链比较长,而且其实主要就是使用spring容器(DefaultListableBeanFactory) 的的registerBeanDefinition 方法完成注册 ,不是我们这期springboot要讲的,我们这里就跟一下重点的代码,放出对应的注释。
AnnotatedBeanDefinitionReader 类中
图 29
图 30
下面就是 调用spring源码 的地方了,我们大概看一下它是怎么注册的
图 31
由此,通过load方法就可以将主启动类加入到spring容器当中去了。
可能你还是有点迷糊,不要紧。看看👇🏻给你梳理的流程图
Load 方法主要流程图
图 32
以上便是load方法的主要流程。「 图 24 」 在prepareContext事件之后,还执行了refreshContext方法(该方法主要是spring的refresh方法,完成spring容器的ioc自动装配等工作,方法很多,下次有机会再写文章讲解),发布了 应用程序发布完毕事件 (listeners.started(context)) 和 应用程序就绪事件(listeners.running(context))。其实和之前讲解的 listeners.starting() 大同小异,这里就不做另外的分析了。
总结
以上便是SpringBoot 启动类run方法做的一些事情,希望能对大家阅读源码有一定的帮助吧。如果有什么不是很明白的,也可以留言,作者看到了会回复。