Spring Boot
为我们提供了一种极简的项目搭建方式,看一下Spring Boot项目的启动类:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
1 注解启动分析
注解启动分析,就是说的@SpringBootApplication
首先看一下@SpringBootApplication
这个组合注解,除去元注解外,它还引入了其他三个重要的注解:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
1.1 @SpringBootConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
从源码可以看到,其实@SpringBootConfiguration
并没有额外功能,它只是Spring
中@Configuration
的派生注解,用于标注配置类,完成Bean
的配置与管理
@SpringBootConfiguration
只是Spring
标准@Configuration
批注的替代方法。 两者之间的唯一区别是@SpringBootConfiguration
允许自动找到配置
应用程序应该只包含一个@SpringBootConfiguration
并且大多数惯用的 SpringBoot
应用程序将从@SpringBootApplication
继承它
1.2 @ComponentScan
Spring
中的注解,用于包的扫描,并把声明了特定注解的类交给spring
的ioc
容器
1.3 自动装配@EnableAutoConfiguration
SpringBoot
根据应用所声明的依赖来对Spring
框架进行自动配置,其中包括两个重要注解:@AutoConfigurationPackage
和@Import({AutoConfigurationImportSelector.class})
1.3.1 @AutoConfigurationPackage
@AutoConfigurationPackage
:该注解上有一个@Import({Registrar.class})
注解,其中Registrar
类的作用是将启动类所在的包下的所有子包组件扫描注入到spring
容器中,因此这就是为什么将controller
、service
等包放在启动类的同级目录下的原因
1.3.2 @Import({AutoConfigurationImportSelector.class})
1.3.2.1 AutoConfigurationImportSelector.class
Spring Boot
有中一个非常重要的理念就是约定大于配置。而自动配置这一机制的核心实现就是靠@EnableAutoConfiguration
注解完成的
可以看出,在@EnableAutoConfiguration
注解中,使用@Import
导入了AutoConfigurationImportSelector
这个类,实现了ImportSelector
接口的selectImports()
方法。spring
中会把selectImports()
方法返回的String
数组中的类的全限定名实例化为bean
,并交给spring
容器管理
1.3.2.2 getAutoConfigurationEntry方法
查看其中的getAutoConfigurationEntry方法:
在执行完getCandidateConfigurations
后,把众多类的全限定名存储到了一个List
中
SpringFactoriesLoader
这个类非常重要,属于Spring
框架的一种扩展方案,提供一种了配置查找的功能支持。其主要功能就是读取配置文件META-INF/spring.factories
,决定要加载哪些类
当然,并不是所有spring.factories
中的类都会被加载到spring
容器中,很多情况下需要按照需求所需的情况引入,依赖条件注解@Conditional
进行判断。例如ServletWebServerFactoryAutoConfiguration
类
只有在classpath下存在ServletRequest
这一类时,才将ServletWebServerFactoryAutoConfiguration
作为配置类导入spring
容器中
1.3.2.3 loadFactoryNames方法
进入loadFactoryNames方法:
这个方法非常简短,因为他调用了真正实现的方法:loadSpringFactories
这一行return代码复制在下面:
loadSpringFactories(classLoader)
.getOrDefault(factoryTypeName, Collections.emptyList());
可以分析得出:loadSpringFactories
方法的返回值又调用了一个getOrDefault
方法,这明显是一个容器类的方法,目的是从容器中拿点东西出来
就此推测:loadSpringFactories
返回了一个包含我们需要的Config
全类名(字符串)的集合容器,然后从这个集合容器中拿出来的东西就是我们的configurations
1.3.2.4 loadSpringFactories方法
看这个loadSpringFactories方法:
它确实返回了一个容器:Map<String, List>
这个容器的类型是:MultiValueMap<String, String>
这个数据结构就非常牛逼了,多值集合映射
简单来说,一个key可以对应多个value,根据他的返回值,我们可以看到在这个方法中一个String
对应了一个List
接下来我们继续思考:我们来的目的是获取configurations,所以无论你做什么,必须得读取配置文件,拿到configurations
于是我们在try方法体中果然发现了这个操作:
获取了一个路径urls,那么这个路径是否就是我们前面验证的META-INF/spring.factories
呢?
查看静态常量FACTORIES_RESOURCE_LOCATION
的值:
继续往下看,果然他遍历了urls中的内容,从这个路径加载了配置文件:终于看到了我们熟悉的loadProperties方法!
那我们大概就知道了,他确实是通过找到路径,然后根据路径读取了配置文件,然后返回了读取的result
这就是loadFactoryNames方法的内部实现。
1.3.2.5 cache探秘
细心地朋友已经发现了玄机,隐藏在loadFactoryNames
方法的开头和结尾:
它是从cache缓存中取出来的
根据下面的if判断,如果从缓存中读取出来了result,并且result的结果不为空,就直接返回,不需要再进行下面的读写操作了,这样减少了磁盘频繁的读写I/O
同理,更新完所有的配置文件资源之后,退出时也要更新缓存。
1.3.2.6 getAutoConfigurationEntry再探
关键部分已经过去,重新审视一下遗漏的内容:
还记得getAutoConfigurationEntry
方法吗?
我们最后来研究一下这个类除了getCandidateConfigurations
还干了哪些事情:
- removeDuplicates
- configurations.removeAll(exclusions)
可以看到,这里对加载进来的配置进行了去重、排除的操作,这是为了使得用户自定义的排除包生效,同时避免包冲突异常,在SpringBoot的入口函数中我们可以通过注解指定需要排除哪些不用的包:
例如我不使用RabbitMQ的配置包,就把它的配置类的class传给exclude
@SpringBootApplication(exclude = {RabbitAutoConfiguration.class})
1.3.2.7 自动装配本质
SpringBoot
自动装配的本质就是通过Spring
去读取META-INF/spring.factories
中保存的配置类文件然后加载bean定义的过程。
如果是标了@Configuration
注解,就是批量加载了里面的bean定义
如何实现自动
:通过配置文件获取对应的批量配置类,然后通过配置类批量加载bean
定义,只要有写好的配置文件spring.factories
就实现了自动。
1.3.2.8 自动装配总结
Spring Boot
的自动装配特性可以说是Spring Boot
最重要、最核心的一环,正是因为这个特性,使得我们的生产复杂性大大降低,极大地简化了开发流程
Spring Boot
自动装配详细流程图:
1.3.3 自动装配用到类总结
最后,总结一下整个自动装配的过程:
- 引入
META-INF/spring.factories
配置文件,在EnableAutoConfiguration
对应的value
中配置需要引入的配置类。 - 启动类增加
@EnableAutoConfiguration
注解,@SpringBootApplication
已经自带。 -
@EnableAutoConfiguration
注解中通过@Import
标注了AutoConfigurationImportSelector
类。 -
AutoConfigurationImportSelector
继承了DeferredImportSelector
接口,在Spring
生命周期处理BeanFactoryPostProcessors
的时候会对配置信息进行后置处理,这是会调用到AutoConfigurationImportSelector.process
方法。 -
process
方法中会读取META-INF/spring.factories
配置文件中的内容为Key-Value
形式,读取完后值返回key = EnableAutoConfiguration
对应的配置类信息,保存到autoConfigurationEntries
中。 -
AutoConfigurationGroup#selectImports
方法返回排序、筛选后的配置类信息,然后依次遍历,递归调用processImports
, 根据这些配置类的全路径名读取并注册在 Spring 容器中。
2 SpringApplication引导启动
SpringApplication
类是用来执行Spring
框架启动的引导类。有两种方式可以进行启动引导:
- 通过静态方法
SpringApplication.run
启动。 - 先创建
SpringApplication
实例,在调用的实例方法run
进行启动。
无论是以上哪种方式,最终都是通过创建SpringApplication
实例,在调用run()
启动
2.1 SpringApplication—创建引导启动的实例
SpringApplication
提供了一个简单的方式以启动Spring boot
程序,查看SpringApplication.run
方法调用:
在此创建了一个SpringApplication的实例,并调用了它的run方法
在创建实例的过程中,会根据用户输入和工程环境做一些基础配置,供之后引导启动中使用:
- 设置资源加载器
ResourceLoader
和PrimarySources
,用于将资源加载到加载器中 - 从类中加载
initializer
和listener
放在集合 - 判断当前项目类型是什么? 提供了NONE,SERVLET,REACTIVE 三种类型备选
- 使用
SpringFactoriesLoader
查找并加载所有可用的ApplicationContextInitializer
- 使用
SpringFactoriesLoader
查找并加载所有可用的监听器ApplicationListener
- 推断并设置main方法的定义类
- 设置是否为
Web
环境(先确认用户是否指定,未指定则根据工程目录下是否有servlet
相关环境) - 从工程环境中决定主入口的类
2.2 run()—开始引导启动
SpringApplication
完成初始化后,调用run
方法,下面对run
方法中核心代码进行分析:
按照图中标注序号进行分析:
-
spring
监听器的使用,要获取这些监听器的对象,就要知道其全路径。通过SpringFactoriesLoader
查找spring.factories
获得,之后再调用它们的started()
方法。 - 创建并配置当前
Spring Boot
应用将要使用的Environment
,根据监听器和默认应用参数来准备所需要的环境。 - 打印
Banner
- 创建
spring
应用上下文。根据之前推断的项目类型,决定该为当前SpringBoot
应用创建什么类型的ApplicationContext
并创建完成。 - 准备应用上下文,首先将之前准备好的
Environment
设置给创建好的ApplicationContext
使用。
然后遍历调用所有ApplicationContextInitializer
的initialize
方法来对已经创建好的ApplicationContext
进行进一步的处理。
最后,遍历调用所有SpringApplicationRunListener
的contextPrepared()
方法。 - 这里最终调用了
Spring
中AbstractApplicationContext
的refresh
方法,可以说这个refresh
方法是Spring
中最重要的方法之一,完成了Bean
工厂创建,后置管理器注册,Bean
实例化等最重要的工作。这一步工作完成后,spring
的ioc
容器就完成了。 - 如果有
Bean
实现了CommandLineRunner
接口并重写了run
方法,则遍历执行CommandLineRunner
中的方法
2.2.1 new StopWatch()—创建计时器
StopWatch
是springframework.util
中提供的一个工具类,在启动过程中使用StopWatch
是为了记录启动花费的时间。
2.2.2 configureHeadlessProperty()—配置Headless模式
Headless
模式是在环境缺少显示器等设备情况下的一种配置,和我们启动流程并无太多关系
2.2.3 SpringApplicationRunListener.start()—获取监听器,启动监听
监听器可以用来监听SpringApplication
启动过程中的各个阶段。默认的监听器是EventPublishRunListener
,用户也可以通过实现SpringApplicationRunListener
接口,实现应用程序对SpringApplication
启动过程的监听。
在
resources/META-INF
下建立spring.factories
文件,文件中添加key=value
形式,其中key
为SpringApplicationRunListener
的全路径名,value
为应用程序对该接口的实现类(类需要一个参数类型为SpringApplication
和String
数组的构造函数,用于通过反射创建实例)。
2.2.4 prepareEnvironment()—准备环境,创建ConfigurableEnvironment对象
在这一步,SpringApplication
会创建Spring
启动所需的环境,这个环境主要由ConfigurableEnviroment
对象表示。首先,该对象确认了程序是否需要设置Web
环境,其次,该对象还确定了程序所需要的参数和读取的配置文件等信息。
此步骤会回调SpringApplicationRunListener
的environmentPrepared()
方法,通知监听器环境已经准备好。
2.2.5 printBanner()—打印横幅
这一步骤其实和启动并没有太大关系,只是会向控制台或是日志中输出Spring
的Logo
和版本信息。
2.2.6 createApplicationContext()—创建应用程序上下文并加载Bean
在准备好环境之后,接下来要做的就是创建应用程序上下文ApplicationContext
对象。
ApplicationContext
是Spring IoC
的核心组件,它不仅是程序所需Bean
的容器,还提供了国际化,事件发布等功能。
在创建应用程序上下文的时候,首先会根据之前配置决定上下文的具体类型(AnnotationConfigApplicationContext
或是AnnotationConfigServletWebServerApplicationContext
)。
再通过反射实例化到对象。
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable to create a default ApplicationContext, " +
"please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
在 createApplicationContext()
方法中,Spring Boot
首先会判断应用程序的类型,如果是 Web
应用程序,则会创建一个 WebApplicationContext;
否则,会创建一个普通的 ApplicationContext
。调用 BeanUtils.instantiateClass(contextClass)
方法创建应用程序的上下文。这个方法会根据上面的逻辑创建一个相应的 ApplicationContext
。调用 load()
方法加载应用程序的配置。
这个方法会扫描 classpath
中的各种配置文件,例如 application.properties、application.yml、META-INF/spring.factories
等,自动配置各种组件和 Bean
。调用 postProcessApplicationContext()
方法对应用程序的上下文进行后处理。这个方法会调用各种初始化器和监听器,执行各种初始化任务。
2.2.7 prepareContext()—准备ApplicationContext
虽然已经得到了ApplicationContext
对象,但此时的对象还只是一个空白对象,需要准备和处理后,ApplicationContext
才能被使用。
在准备过程中主要做了做了几件事:
- 为
ApplicationContext
设置之前准备好的Environment
对象。 - 通过对
ApplicationContext
后置处理或是BeanDefinitionLoader
等方式往容器中添加一些初始的Bean
- 应用默认的初始化器初始化应用程序上下文(责任链模式的应用,多个初始化器形成一个
List
,应用程序需要被每个初始化器应用一次,每个初始化器有自己的职责)。 - 准备过程中
ApplicationRunListener
发出两个消息,分别是contextPrepared
和contextLoaded
2.2.8 refreshContext()—刷新上下文
在应用程序上下文准备好后,可以通过刷新应用程序上下文发现Bean
并加载到容器中。
refreshContext()
会调用ApplicationContext.refresh()
方法。
AbstractApplicationContext
中定义了refresh()
方法的基本框架(模板模式的应用)。
在刷新上下文阶段中,Spring Boot
会执行各种启动任务,包括创建 Web
服务器、加载应用程序的配置、初始化各种组件等。这个阶段的核心源码是 Spring Boot
的刷新机制,它会调用各种初始化器和监听器,执行各种启动任务。
protected void refreshContext(ConfigurableApplicationContext applicationContext) {
refresh(applicationContext);
if (this.registerShutdownHook) {
try {
applicationContext.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
在 refreshContext()
方法中调用 refresh(applicationContext)
方法刷新上下文。这个方法是 ApplicationContext
接口的核心方法,会启动上下文,执行各种启动任务。调用 registerShutdownHook()
方法注册应用程序的关闭钩子。这个方法会在应用程序关闭时自动执行,清理资源、关闭线程等,所以我们利用此特性在服务关闭的时候清理一些资源。并向外部发送告警通知。
在 refresh(applicationContext)
方法中,Spring Boot
会执行上下文的各种启动任务,包括创建 Web 服务器、加载应用程序的配置、初始化各种组件等。具体的启动任务会调用各种初始化器和监听器,例如:
for (ApplicationContextInitializer<?> initializer : getInitializers()) {
initializer.initialize(applicationContext);
}
另外,Spring Boot 还会调用各种监听器,我们不做赘述,例如:
for (ApplicationListener<?> listener : getApplicationListeners()) {
if (listener instanceof SmartApplicationListener) {
SmartApplicationListener smartListener = (SmartApplicationListener) listener;
if (smartListener.supportsEventType(eventType)
&& smartListener.supportsSourceType(sourceType)) {
invokeListener(smartListener, event);
}
}
else if (supportsEvent(listener, eventType)) {
invokeListener(listener, event);
}
}
2.2.8.1 prepareRefresh()—准备刷新
准备刷新的阶段做了初始化和校验的工作。比如初始化启动时间,初始化PropertySources
(在AbstractApplicationContext
中只是一个空方法,留给子类根据需要实现),以及校验环境中是否已经有必要的参数。
2.2.8.2 prepareBeanFactory()—准备BeanFactory
BeanFactory
是 Spring
框架中容器的底层实现,所有的 Bean
都存放在BeanFactory
中,虽然ApplicationContext
也实现了BeanFactory
接口,但是在其内部还是将获取 Bean
的相关操作委托给内部的DefaultListableBeanFactory
变量,只是ApplicationContext
帮用户屏蔽了底层的操作,同时提供出一些更符合外部用户使用的接口。
对BeanFactory的准备主要是:
- 添加一些必要组件,比如类加载器,表达式解析器,属性编辑器注册表等。
以及一些后置处理器,比如ApplicationContextAwareProcessor
(xxxAware的接口就是通过后置处理器在Bean创建的时候,通过后置处理器设置的)。
此外还有一些特殊的Bean,environment,systemProperties和systemEnvirnoment。 - 点击查看Spring核心之bean生命周期和三级缓存
2.2.8.3 postProcessBeanFactory()—后置处理BeanFactory
对于非WebServlet
环境的ApplicationContext
而言这个方法是个空方法,但是Web
环境下的ApplicationContext
会通过这个方法定制一些后处理动作,比如添加WebApplicationContextServletAwareProcessor
后置处理器,添加在web
环境中可能使用的Scope
(session和request)。
2.2.8.4 invokeBeanFactoryPostProcessors()—实例化并调用BeanFactoryPostProcessor
BeanFactoryPostProcessor
是一种特殊的后置处理器,其操作的对象是针对BeanFactory
。
此时主要有三个后置处理器,分别是:SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
, ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
和ConfigFileApplicationListener$PropertySourceOrderingPostProcessor
。这三个类名字虽然很长,但是其实是因为内部类的关系,而且我们看名字也能发现类是怎么来的(外部类是xxxInitializer
的就说明是初始化器设置的)。
其中第一个类和启动流程有关,因为它会向容器注册ConfigurationClassPostProcessor
。
如果BeanFactoryPostProcessor
同时又是BeanDefinitionRegistryPostProcessor
,则先进行针对BeanDefinition
注册表的后置处理,目的是为了发现Bean
。
在最初的三个BeanFactoryProcessor
后置处理完成后,会从容器中获取BeanDefinitionRegistryPostProcessor
类型的后置处理器(这里主要会得到刚才加载的ConfigurationClassPostProcessor
实例)。再调用这些BeanDefinitionRegistry
的后置处理器,继续向发现并向容器中注册新的Bean。
这里主要是通过
@Configuration
注解作为入口发现Bean
,如果发现的Bean
中又存在新的@ConfigurationBean
,则以此Bean为入口再进行发现,直到所有的Bean
都被发现。
在针对BeanDefinition
注册表的后置处理完成(发现Bean的过程)中,如果找到了BeanFactoryPostProcessor
(包括最初的三个BeanFatoryProcessor
),会进行针对BeanFactory
的后置处理过程(之前只是进行针对注册表的后置处理,二者的目的还是有区别的)。
注意
Bean
的发现过程只是向BeanDefinition
注册表注册BeanDefinition
的过程,并没有针对发现的Bean
进行实例化(少部分需要用到的Bean
会进行实例化,比如这部分会对BeanDefinitionRegistryPostProcessor
类型的Bean
实例化)。
2.2.8.5 registerBeanPostProcessors()—注册Bean后置处理器
上一步是针对BeanFactory
和BeanDefinitionRegistry
的后置处理器,这一步从BeanFactory
中获取针对普通Bean
的后置处理器BeanFactoryPostProcessor
放到专门的容器beanPostProcessors
中。
2.2.8.6 initMessageSource()—初始化MessageSource
MessageSource
是拥有特殊功能的Bean
,用来处理国际化相关内容。
2.2.8.7 initApplicationEventMulticaster()—初始化ApplicationEventMulticaster
ApplicationEventMulticaster
是ApplicationEvent
广播器,可以通过这个对象向容器中添加移除Listener
,也可以通过这个对象发布事件(观察者模式的应用)。
2.2.8.8 onRefresh()—刷新应用程序
发现了所有的Bean
,并且需要实例化的Bean
也都被创建好了之后,Spring
接下去要做的是创建ThemeSource
(和主题相关的组件),以及创建Webserver
(如果是Web环境的话)。
2.2.8.9 registerListeners()—注册监听器
这一步会将初始化得到的ApplicationListener
方法和容器中获得ApplicationListener
一起注册到ApplicationEventMulticaster
中,并且如果存在需要早起发布的事件,则发布事件。
2.2.8.10 finishBeanFactoryInitialzation()—初始化容器中的Bean
经过之前的步骤,现在容器中必要的组件都已经准备好了,并且所有需要容器管理的Bean
也都已经被发现注册成BeanDefinition
注册表中。
对于Scope
是Singleton
的Bean
而言,此时已经具备了实例化Bean
的条件,因此在这一步中,Spring
会对所有Singleton
且非lazy-init
的Bean
进行实例化。
主要做法就是获取容器中所有为singletion
且非lazyInit
的BeanDefinition
,然后通过getBean
创建出Bean
的实例,保存在容器内部。
有一种特殊的情况是针对
FactoryBean
,FactoryBean
是一种用来创建Bean
的特殊Bean
,在得到FactoryBean
的Bean
之后,还需要判断是否要创建FactoryBean
负责创建的Bean
2.2.8.11 finishRefresh()—完成刷新
在这步主要是一些资源清理以及注册LifeCycleProcessor
。LifeCycleProcessor
可以用来在 Spring
生命周期的refresh
和close
时触发回调。
并且发布Refresh
的消息。
2.2.9 afterRefresh()—留给子类的钩子函数
在Application
完成刷新后,SpringApplication
给子类留了afterRefresh()
的方法作为回调。
2.3 启动完成
启动完成后,stopWatch
会记录下本次启动消费的时间。
然后向ApplicationRunListener
发布started
事件,说明已经启动就绪。
2.4 准备运行
启动完成后,正式运行前,SpringApplication
还会执行用户定义的ApplicationRunner
和CommandLineRunner
两个接口中定义的run()
方法。
在执行完成后,向ApplicationRunListener
发布runing
的消息。
至此,启动流程结束。
CommandLineRunner
和 ApplicationRunner
区别如下:
- 方法签名不同:
-
CommandLineRunner
接口有一个run(String... args)
方法,它接收命令行参数作为可变长度字符串数组。 -
ApplicationRunner
接口则提供了一个run(ApplicationArguments args)
方法,它接收一个ApplicationArguments
对象作为参数,这个对象提供了对传入的所有命令行参数(包括选项和非选项参数)的访问。
-
- 参数解析方式不同:
-
CommandLineRunner
接口更简单直接,适合处理简单的命令行参数。 -
ApplicationRunner
接口提供了一种更强大的参数解析能力,可以通过ApplicationArguments
获取详细的参数信息,比如获取选项参数及其值、非选项参数列表以及查询是否存在特定参数等。
-
- 使用场景不同:
当只需要处理一组简单的命令行参数时,可以使用CommandLineRunner
。
对于需要精细控制和解析命令行参数的复杂场景,推荐使用ApplicationRunner
3 总结
本文旨在对SpringBoot
启动流程各个步骤做一次梳理(本文的段落标题就是启动的各个步骤,不同等级的标题也含有方法前后调用的关系),并没有对每行代码做深入分析
另外,在贴一份整理的不错的流程图帮助大家加深印象。