SpringBoot是如何启动的?
本文是通过查看SpringBoot源码整理出来的SpringBoot大致启动流程,整体大方向是以简单为出发点,不说太多复杂的东西,内部实现细节本文不深扣因为每个人的思路、理解都不一样,我个人看的理解跟大家看的肯定不一样,到时候表达的出来的云里雾里也没啥用。
首先我将SpringBoot的启动流程整理成以下阶段:
- SpringApplicaiton初始化
- 审查ApplicationContext类型
- 加载ApplicationContextInitializer
- 加载ApplicationListener
- Environment初始化
- 解析命令行参数
- 创建Environment
- 配置Environment
- 配置SpringApplication
- ApplicationContext初始化
- 创建ApplicationContext
- 设置ApplicationContext
- 刷新ApplicationContext
- 运行程序入口
省去了一些不影响主流程的细节,在查看SpringBoot源码之前,不得不提一下 spring.factories
这个文件的使用和功能。
关于spring.factories
spring.factories
是一个properties文件,它位于 classpath:/META-INF/
目录里面,每个jar包都可以有 spring.factories
的文件。Spring提供工具类 SpringFactoriesLoader
负责加载、解析文件,如 spring-boot-2.2.0.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 ...
关于 spring.factories
需要知道些什么?
-
spring.factories
是一个properties文件 -
spring.factories
里的键值对的value是以逗号分隔的完整类名列表
-
spring.factories
里的键值对的key是完整接口名称
-
spring.factories
键值对的value是key的实现类 -
spring.factories
是由SpringFactoriesLoader
工具类加载 -
spring.factories
位于classpath:/META-INF/
目录 -
SpringFactoriesLoader
会加载jar包里面的spring.factories
文件并进行合并
知道 spring.factories
的概念后,继续来分析SpringBoot的启动。
SpringApplicaiton初始化
Java程序的入口在 main
方法SpringBoot的同样可以通过 main
方法启动,只需要少量的代码加上 @SpringBootApplication
注解,很容易的就启动SpringBoot:
@SpringBootApplication @Slf4j public class SpringEnvApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringEnvApplication.class, args); } }
SpringApplicaiton初始化位于 SpringApplication
的构造函数中:
public SpringApplication(Class>... primarySources) { this(null, primarySources); } public SpringApplication(ResourceLoader resourceLoader, Class>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }
简单的说下 SpringApplication
的构造函数干了些啥:
- 基础变量赋值(resourceLoader、primarySources、…)
- 审查ApplicationContext类型如(Web、Reactive、Standard)
- 加载ApplicationContextInitializer
- 加载ApplicationListener
- 审查启动类(main方法的类)
然后再来逐个分析这些步骤。
审查ApplicationContext类型
SpringBoot会在初始化阶段审查 ApplicationContext
的类型,审查方式是通过枚举 WebApplicationType
的 deduceFromClasspath
静态方法:
static 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; }
WebApplicationType
枚举用于标记程序是否为Web程序,它有三个值:
- NONE:不是web程序
- SERVLET:基于Servlet的Web程序
- REACTIVE:基于Reactive的Web程序
简单的来说该方法会通过classpath来判断是否Web程序,方法中的常量是完整的class类名:
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext" }; private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet"; private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler"; private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer"; private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext"; private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
例如通过 pom.xml
文件引入 spring-boot-starter-web
那classpath就会有 org.springframework.web.context.ConfigurableWebApplicationContext
和 javax.servlet.Servlet
类,这样就决定了程序的 ApplicationContext
类型为 WebApplicationType.SERVLET
。
加载ApplicationContextInitializer
ApplicationContextInitializer
会在刷新context之前执行,一般用来做一些额外的初始化工程如:添加 PropertySource
、设置 ContextId
等工作它只有一个 initialize
方法:
public interface ApplicationContextInitializer{ void initialize(C applicationContext); }
SpringBoot通过 SpringFactoriesLoader
加载 spring.factories
中的配置读取key为 org.springframework.context.ApplicationContextInitializer
的value,前面提到过 spring.factoies
中的配置的value都为key的实现类:
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
上面列出的是 spring-boot-2.2.0.RELEASE.jar
中包含的配置,其他jar包也有可能配置 org.springframework.context.ApplicationContextInitializer
来实现额外的初始化工作。
加载ApplicationListener
ApplicationListener
用于监听 ApplicationEvent
事件,它的初始加载流程跟加载 ApplicationContextInitializer
类似,在 spring.factories
中也会配置一些优先级较高的 ApplicationListener
:
# Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ 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
ApplicationListener
的加载流程跟 ApplicationContextInitializer
类似都是通过 SpringFactoriesLoader
加载的。
小结
完成初始化阶段后,可以知道以下信息:
ApplicationContextInitializer ApplicationListener
Environment初始化
初始化工作完成后SpringBoot会干很多事情来为运行程序做好准备,SpringBoot启动核心代码大部分都位于SpringApplication实例的 run
方法中,在环境初始化大致的启动流程包括:
- 解析命令行参数
- 准备环境(Environment)
- 设置环境
当然还会有一些别的操作如:
- 实例化SpringApplicationRunListeners
- 打印Banner
- 设置异常报告
- …
这些不是重要的操作就不讲解了,可以看完文章再细细研究。
解析命令行参数
命令行参数是由 main
方法的 args
参数传递进来的,SpringBoot在准备阶段建立一个 DefaultApplicationArguments
类用来解析、保存命令行参数。如 --spring.profiles.active=dev
就会将SpringBoot的 spring.profiles.active
属性设置为dev。
public ConfigurableApplicationContext run(String... args) { ... ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ... }
SpringBoot还会将收到的命令行参数放入到 Environment
中,提供统一的属性抽象。
创建Environment
创建环境的代码比较简单,根据之前提到过的 WebApplicationType
来实例化不同的环境:
private ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; } switch (this.webApplicationType) { case SERVLET: return new StandardServletEnvironment(); case REACTIVE: return new StandardReactiveWebEnvironment(); default: return new StandardEnvironment(); } }
准备Environment
环境(Environment)大致由Profile和PropertyResolver组成:
- Profile是BeanDefinition的逻辑分组,定义Bean时可以指定Profile使SpringBoot在运行时会根据Bean的Profile决定是否注册Bean
- PropertyResolver是专门用来解析属性的,SpringBoot会在启动时加载配置文件、系统变量等属性
SpringBoot在准备环境时会调用 SpringApplication
的 prepareEnvironment
方法:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); bindToSpringApplication(environment); ... return environment; }
prepareEnvironment
方法大致完成以下工作:
- 创建一个环境
- 配置环境
- 设置SpringApplication的属性
配置Environment
创建完环境后会为环境做一些简单的配置:
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { if (this.addConversionService) { ConversionService conversionService = ApplicationConversionService.getSharedInstance(); environment.setConversionService((ConfigurableConversionService) conversionService); } configurePropertySources(environment, args); configureProfiles(environment, args); } protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { if (this.addCommandLineProperties && args.length > 0) { ... sources.addFirst(new SimpleCommandLinePropertySource(args)); ... } } protected void configureProfiles(ConfigurableEnvironment environment, String[] args) { Setprofiles = new LinkedHashSet<>(this.additionalProfiles); profiles.addAll(Arrays.asList(environment.getActiveProfiles())); environment.setActiveProfiles(StringUtils.toStringArray(profiles)); }
篇幅有限省去一些不重要的代码,配置环境主要用于:
- 设置ConversionService: 用于属性转换
- 将命令行参数添加到环境中
- 添加额外的ActiveProfiles
SpringApplicaton属性设置
配置 SpringApplicaton
主要是将已有的属性连接到 SpringApplicaton
实例,如 spring.main.banner-mode
属性就对应于 bannerMode
实例属性,这一步的属性来源有三种(没有自定义的情况):
- 环境变量
- 命令行参数
- JVM系统属性
SpringBoot会将前缀为 spring.main
的属性绑定到 SpringApplicaton
实例:
protected void bindToSpringApplication(ConfigurableEnvironment environment) { try { Binder.get(environment).bind("spring.main", Bindable.ofInstance(this)); } catch (Exception ex) { throw new IllegalStateException("Cannot bind to SpringApplication", ex); } }
Environment初始化小结
总结下环境准备阶段所做的大致工作:
WebApplicationType ConversionService args Prepared
ApplicationContext初始化
前面提到的一些步骤大部分都是为了准备 ApplicationContext
所做的工作, ApplicationContext
提供加载Bean、加载资源、发送事件等功能,SpringBoot在启动过程中创建、配置好 ApplicationContext
不需要开发都作额外的工作(太方便啦~~)。
本文不打算深入 ApplicationContext
中,因为与 ApplicationContext
相关的类很多,不是一两篇文章写的完的,建议 按模块来看,最后再整合起来看 ApplicationContext
源码 。
创建ApplicationContext
创建 ApplicationContext
的过程与创建环境基本模相似,根据 WebApplicationType
判断程序类型创建不同的 ApplicationContext
:
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 create a default ApplicationContext, please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }
前面提到过 WebApplicationType
有三个成员(SERVLET,REACTIVE,NONE),分别对应不同的context类型为:
- SERVLET: AnnotationConfigServletWebServerApplicationContext
- REACTIVE: AnnotationConfigReactiveWebServerApplicationContext
- NONE: AnnotationConfigApplicationContext
准备ApplicationContext
创建完 ApplicationContext
完后需要初始化下它,设置环境、应用ApplicationContextInitializer、注册Source类等,SpringBoot的准备Context的流程可以归纳如下:
- 为
ApplicationContext
设置环境(之前创建的环境) - 基础设置操作设置BeanNameGenerator、ResourceLoader、ConversionService等
- 执行
ApplicationContextInitializer
的initialize
方法(ApplicationContextInitializer是在初始化阶段获取的) - 注册命令行参数(springApplicationArguments)
- 注册Banner(springBootBanner)
- 注册sources(由@Configuration注解的类)
准备 ApplicationContext
的代码如下所示:
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } // Load the sources Set
注意 注册sources
这一步,sources是@Configuration注解的类SpringBoot根据提供的sources注册Bean,基本原理是通过解析注解元数据,然后创建BeanDefinition然后将它注册进 ApplicationContext
里面。
刷新ApplicationContext
如果说SpringBoot的是个汽车,那前面所做的操作都是开门、系安全带等基本操作了,刷新ApplicationContext就是点火了,没刷新ApplicationContext只是保存了一个Bean的定义、后处理器啥的没有真正跑起来。刷新ApplicationContext这个内容很重要,要理解ApplicationContext还是要看刷新操作的源码,
这里先简单列一下基本步骤:
- 准备刷新(验证属性、设置监听器)
- 初始化BeanFactory
- 执行BeanFactoryPostProcessor
- 注册BeanPostProcessor
- 初始化MessageSource
- 初始化事件广播
- 注册ApplicationListener
- …
刷新流程步骤比较多,关联的类库都相对比较复杂,建议先看完其他辅助类库再来看刷新源码,会事半功倍。
运行程序入口
context刷新完成后Spring容器可以完全使用了,接下来SpringBoot会执行 ApplicationRunner
和 CommandLineRunner
,这两接口功能相似都只有一个 run
方法只是接收的参数不同而以。通过实现它们可以自定义启动模块,如启动 dubbo
、 gRPC
等。
ApplicationRunner
和 CommandLineRunner
的调用代码如下:
private void callRunners(ApplicationContext context, ApplicationArguments args) { List
callRunners
执行完后,SpringBoot的启动流程就完成了。
总结
通过查看SpringApplication的源码,发现SpringBoot的启动源码还好理解,主要还是为ApplicationContext提供一个初始化的入口,免去开发人员配置ApplicationContext的工作。SpringBoot的核心功能还是自动配置,下次分析下SpringBoot Autoconfig的源码,要充分理解SpringBoot看源码是少了的。
看完SpringApplication的源码还有些问题值得思考:
- SpringBoot是启动Tomcat的流程
- SpringBoot自动配置原理
- SpringBoot Starter自定义
- BeanFactoryPostProcessor和BeanPostProcessor实现原理
- …
《架构文摘》每天一篇架构领域重磅好文,涉及一线互联网公司应用架构(高可用、高性 能、高稳定)、大数据、机器学习等各个热门领域。