SpringBoot是如何啟動的?
- 2019 年 11 月 5 日
- 筆記
本文是通過查看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實現原理
- …
《架構文摘》每天一篇架構領域重磅好文,涉及一線互聯網公司應用架構(高可用、高性 能、高穩定)、大數據、機器學習等各個熱門領域。