本文是通過查看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<C extends ConfigurableApplicationContext> {
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
加載的。
小結
完成初始化階段后,可以知道以下信息:
- ApplicationContext是Web還是其他類型
- SpringApplication中有一些
ApplicationContextInitializer
實現類 - SpringApplication中有一些
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) {
Set<String> profiles = 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
添加到環境 - 將外部設置的Profiles添加到環境
- 綁定SprinngApplicaiton屬性
- 發送環境
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<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
注意注冊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<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
callRunners
執行完后,SpringBoot的啟動流程就完成了。
總結
通過查看SpringApplication的源碼,發現SpringBoot的啟動源碼還好理解,主要還是為ApplicationContext提供一個初始化的入口,免去開發人員配置ApplicationContext的工作。SpringBoot的核心功能還是自動配置,下次分析下SpringBoot Autoconfig的源碼,要充分理解SpringBoot看源碼是少了的。
看完SpringApplication的源碼還有些問題值得思考:
- SpringBoot是啟動Tomcat的流程
- SpringBoot自動配置原理
- SpringBoot Starter自定義
- BeanFactoryPostProcessor和BeanPostProcessor實現原理
- ...
《架構文摘》每天一篇架構領域重磅好文,涉及一線互聯網公司應用架構(高可用、高性 能、高穩定)、大數據、機器學習等各個熱門領域。