SpringBoot啟動流程的源碼分析

前言

Spring Boot 版本 2.1.7.RELEASE
本文大致跟蹤了一遍Spring Boot的啟動流程。
請邊debug源代碼邊看本文,因為很多變量細節在debug時才能清晰的觀察到,而本文是無法全部涵蓋所有細節的。
文中如有錯誤遺漏,請留言指正。

相關文章

SpringBoot中Tomcat的源碼分析

正文

程序入口

public static void main(String[] args) {
    SpringApplication.run(XXXApplication.class, args);
}

SpringApplication.run()函數內部

return new SpringApplication(primarySources).run(args); //primarySources is XXXApplication.class

1. SpringApplication的構造函數內部

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();

1.1 WebApplicationType.deduceFromClasspath() 根據org.springframework.web.reactive.DispatcherHandler javax.servlet.Servlet org.springframework.web.context.ConfigurableWebApplicationContext等類是否存在來推斷服務器類型。類型包括WebApplicationType.REACTIVE WebApplicationType.SERVLET WebApplicationType.NONE

1.2 setInitializers()setListeners()函數為SpringApplication的成員變量initializerslisteners賦值。二者均使用getSpringFactoriesInstances(XXX.class)方法獲取參數。
1.2.1 getSpringFactoriesInstances(XXX.class)方法中,參數XXX.class是一個interface。此方法首先會根據接口類的類名,獲取其對應的多個實現類,然后實例化這些實現類,最后返回這些實例。這些實現類就是SpringFactories,是各個jar包中的factory。
getSpringFactoriesInstances()方法內部通過SpringFactoriesLoader.loadFactoryNames()方法加載。方法內部會在classpath下尋找路徑為META-INF/spring.factories的所有文件,此文件的內容格式為properties,文件中的每一行記錄一條interface及其對應的多個實現類。每行的內容形如
example.MyService=example.MyServiceImpl1,example.MyServiceImpl2
根據所有的spring.factories文件,可以構建出一個mapMultiValueMap<String, String>,其中key為interface,value為其對應的多個實現類組成的list。這個map會存到Map<ClassLoader, MultiValueMap<String, String>> cache中,方便未來多次查找。
spring.factories的加載類似于工具類,沒有確切的加載時機。隨時用到隨時加載,只是首次加載時,掃描到的spring.factories全部放入緩存,后續讀取緩存。

1.2 續
通過getSpringFactoriesInstances()方法的搜索, SpringApplication實例的成員變量得到了賦值。initializers類型為List,包含了ApplicationContextInitializer.class的多個實現類;listeners類型為List,包含了ApplicationListener.class的多個實現類。

2 SpringApplication結束實例化后,會調用成員方法run()run()中有一些關鍵步驟
2.1 SpringApplicationRunListeners listeners = getRunListeners(args);
2.1.1 此方法首先使用 1.2.1 中提到的getSpringFactoriesInstances(XXX.class)方法搜索SpringApplicationRunListener.class的實現類。
SpringApplicationRunListener interface 定義了SpringApplication.run()運行過程中會拋出的事件,(這個接口類的命名也可以看出SpringApplication - Run - Listener)。

public interface SpringApplicationRunListener {
    void starting();
    void environmentPrepared(ConfigurableEnvironment environment);
    void contextPrepared(ConfigurableApplicationContext context);
    void contextLoaded(ConfigurableApplicationContext context);
    void started(ConfigurableApplicationContext context);
    void running(ConfigurableApplicationContext context);
    void failed(ConfigurableApplicationContext context, Throwable exception);
}

這些事件都是SpringApplicationEvent類的子類,后文將會提到這些事件。
SpringApplicationRunListener接口的實現類SpringApplicationRunListener將會被搜索出來并實例化。
SpringApplicationRunListener類內部實例化了一個ApplicationEventMulticaster成員變量,即SimpleApplicationEventMulticaster類。將來這個multicaster會用來拋出各個事件。將這個multicaster實例化完成后,又將SpringApplication.listeners填入到這個multicaster內部,作為其初始值,代碼如下。

        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        for (ApplicationListener<?> listener : application.getListeners()) {
            this.initialMulticaster.addApplicationListener(listener);
        }

也就是說META-INF/spring.factories文件中定義的所有ApplicationListener.class的實現類都會監聽SpringApplicationEvent事件。
2.1.2 最終getRunListeners(args)方法會將多個SpringApplicationRunListener接口的實現類封裝成SpringApplicationRunListeners,然后返回。

2.2 listeners.starting();
SpringApplicationRunListener調用starting()方法,拋出ApplicationStartingEvent事件。
2.2.1 SimpleApplicationEventMulticaster類負責拋出事件。
2.2.1.1 SimpleApplicationEventMulticaster會首先調用getApplicationListeners()方法獲取監聽事件的所有listener。由于SimpleApplicationEventMulticaster類的初始值listener包括了SpringApplication.listeners(見2.1.1的末尾),所以這些listener會收到所有SpringApplicationEvent事件。
getApplicationListeners()中會使用AbstractApplicationEventMulticaster.supportsEvent()方法來篩選監聽某事件的listener。
篩選出的listener會放入multicaster的retrieverCache中,以備將來再次查詢。
2.2.1.2 SimpleApplicationEventMulticaster拿到所有的listener后,對每一個listener調用invokeListener(listener, event);。各個listener會通過ApplicationListener.onApplicationEvent(E event)方法實現自己的事件響應邏輯。

2.3 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); 此方法執行了以下操作。
2.3.1 ConfigurableEnvironment environment = getOrCreateEnvironment();
此方法根據 1.1 中推斷出的服務器類型,實例化ConfigurableEnvironment類。

switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }

2.3.2 configureEnvironment(environment, applicationArguments.getSourceArgs());
本文以StandardServletEnvironment為例。configureEnvironment()包含以下步驟。
首先,

ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);

為environment設置了ApplicationConversionService實例,用于一些數據格式的轉換。
2.3.2.1 configurePropertySources(environment, args);
為environment配置Property源。
Property源按順序為servletConfigInitParams servletContextInitParams systemProperties systemEnvironment(這些值是String,來源于多個類的靜態變量)。其實Property源在environment的構造函數中就初始化了一部分。具體見各Environment類的構造函數代碼。
systemProperties內的數據來源于System.getProperties();
systemEnvironment內的數據來源于System.getenv();
2.3.2.2 configureProfiles(environment, args);
從上一步 2.3.2.1 配置的各個Property源中尋找當前激活的Profile
property key 為"spring.profiles.active"
2.3.2.1中的數據源中沒有"spring.profiles.active"的話,本步驟結束后,activeProfiles依舊為空。
2.3.3 listeners.environmentPrepared(environment);
發布ApplicationEnvironmentPreparedEvent事件
2.3.3.1 BootstrapApplicationListener會響應ApplicationEnvironmentPreparedEvent事件。
此listener會new StandardEnvironment(),與主流程的StandardServletEnvironment相互獨立。在這個獨立的環境下,通過硬編碼的方式加載bootstrap配置文件(如:bootstrap.yml)。
相關代碼通過這個字段可查到。org.springframework.cloud.bootstrap.BootstrapApplicationListener#BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap"
org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext方法中,通過硬編碼的方式增加spring.config.name = bootstrap配置。
然后在加載配置文件過程中的org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#getSearchNames方法通過條件判斷,選擇加載bootstrap配置。下一節2.3.3.2中會在這個方法的判斷中,選擇加載application配置。
2.3.3.2 ConfigFileApplicationListener會響應ApplicationEnvironmentPreparedEvent事件。
此listener會在一些路徑下尋找active profile,并加載profile內配置的properties,執行過程中的一些細節如下。

  • 搜索路徑包括"classpath:/,classpath:/config/,file:./,file:./config/"。
  • 通過org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loadDocuments()方法加載配置文件。關鍵代碼是
    List<PropertySource<?>> loaded = loader.load(name, resource);
    配置文件路徑名舉例"classpath:/application.yml" "classpath:/application-dev.yml"
  • 加載好的配置文件會被封裝成PropertySources,并存儲在org.springframework.boot.context.config.ConfigFileApplicationListener.Loader.loaded成員變量中。

2.4 Banner printedBanner = printBanner(environment);
打印Banner,就是在控制臺里打印那個SpringBoot圖形。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.7.RELEASE)

2.5 context = createApplicationContext();
根據webApplicationType實例化ConfigurableApplicationContext
如,SERVLET類型對應的class是org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext

2.6 prepareContext(context, environment, listeners, applicationArguments, printedBanner);
以下是方法內的一些關鍵步驟。
2.6.1 applyInitializers(context);
使用SpringApplication.initializers內的各個initializer(見1.2),對context進行初始化。
2.6.2 listeners.contextPrepared(context);
發布ApplicationContextInitializedEvent事件
2.6.3 load(context, sources.toArray(new Object[0]));
具體執行的操作如下。
2.6.3.1 BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
BeanDefinitionLoader的作用是可以從XML和java注解中加載bean definition。
這行代碼里的嵌套調用方法getBeanDefinitionRegistry(context)值得說一下。
2.6.3.1.1 getBeanDefinitionRegistry(context)
這里的context一般都是GenericApplicationContext的子類。
GenericApplicationContext類中有一個成員變量

private final DefaultListableBeanFactory beanFactory;

GenericApplicationContext類和DefaultListableBeanFactory類都實現了BeanDefinitionRegistry接口。GenericApplicationContext類其實是DefaultListableBeanFactory beanFactory的代理,它將所有對
BeanDefinitionRegistry接口的調用都交給了內部成員變量DefaultListableBeanFactory beanFactory去處理。
所以getBeanDefinitionRegistry(context)方法的調用一般得到的還是context自身。
2.6.3.2 loader.load();
將程序入口處的XXXpplication.class封裝成BeanDefinition,并將其注冊到context中(因為context實現了BeanDefinitionRegistry接口)。
2.6.4 listeners.contextLoaded(context);
SpringApplication.listenerslist中的listener全部加入到context中。
發布ApplicationPreparedEvent事件。
listener會在這個時候向context中加入一些BeanFactoryPostProcessor

2.7 refreshContext(context);
這里將會執行很多任務。
首先是調用AbstractApplicationContext.refresh()方法,此方法中一些關鍵步驟如下。
2.7.1 prepareRefresh();
設置AbstractApplicationContext為active

this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);

2.7.2 prepareBeanFactory(beanFactory);
為beanFactory設置一些值,具體見源碼。
2.7.3 invokeBeanFactoryPostProcessors(beanFactory);
其中包括ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry
正如其注釋所言,ConfigurationClassPostProcessor會在此時掃描并注冊BeanDefinition。

Derive further bean definitions from the configuration classes in the registry.

其中的核心方法為ConfigurationClassParser.parse()

2.7.3.1
由于入口類使用@SpringBootApplication注解,該注解包含了@ComponentScan
ConfigurationClassParser.parse()方法中首先會調用一次
org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass()方法。由于入口類作為一個Bean,且被@ComponentScan注解,所以會自動掃描一下所有的java類來尋找Bean,但此次掃描的basePackages會使用main函數所在類的package。
例如com.demo.SomeTestApplication,則只會掃描com.demo下的bean。
2.7.3.2
ConfigurationClassParser.parse()方法的末尾處this.deferredImportSelectorHandler.process()會進行@Import注解的處理。
由于@SpringBootApplication注解包含了@EnableAutoConfiguration。而@EnableAutoConfiguration又包含了@Import(AutoConfigurationImportSelector.class)
所以會使用AutoConfigurationImportSelector.getCandidateConfigurations()方法對被@EnableAutoConfiguration注解的類進行加載。而獲取這些被@EnableAutoConfiguration注解的類的方式是使用1.2.1中提到的SpringFactoriesLoader.loadFactoryNames()方法。即在META-INF/spring.factories文件中,將這些類配置為org.springframework.boot.autoconfigure.EnableAutoConfiguration的對應值,才能被識別。
這些被識別出的class會作為ConfigurationClass放入ConfigurationClassParser.configurationClasses這一map中。
被引入的ConfigurationClass也可以通過被@Import(XXXXImportSelector.class)注解的方式繼續import其他ConfigurationClass

2.7.3 續
ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry方法的后續,會通過
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
取出 2.7.3.2 中收集到的ConfigurationClass,使用
ConfigurationClassBeanDefinitionReader.loadBeanDefinitions()方法加載所有ConfigurationClass中配置的Bean。

2.7.4 onRefresh();, 此函數只做一件重要的事,就是createWebServer();首先在beanFactory尋找并實例化ServletWebServerFactory對應的bean,默認找到bean是TomcatServletWebServerFactory`。

注:
spring-boot-dependencies.pom文件中默認定義了依賴tomcat-embed-core.jar包,沒有依賴其他的web容器jar包。在 2.7.3 中掃描BeanDefinition時,發現了ServletWebServerFactoryConfiguration配置類。在該配置類中定義了多個WebServer的bean方法,包括Tomcat,Jetty和Undertow。但是因為只有Tomcat.class等tomcat相關類存在,所以只有tomcatServletWebServerFactory()bean method生效,默認實例化的便是TomcatServletWebServerFactory
Tomcat相關流程的源碼分析在本文開頭的相關文章中介紹,本文不再贅述。

this.webServer = factory.getWebServer(getSelfInitializer()); 生成tomcat實例并啟動tomcat。
2.7.5 初始化剩余的(non-lazy-init)單例的bean,一般controller啥的就會在這里被實例化

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

2.7.6 finishRefresh();
2.7.6.1 publishEvent(new ContextRefreshedEvent(this));
發布ContextRefreshedEvent事件
2.7.6.2 WebServer webServer = startWebServer();
啟動TomcatWebServer,與2.7.4中的啟動tomcat時不同的。
2.7.6.3 publishEvent(new ServletWebServerInitializedEvent(webServer, this));
發布ServletWebServerInitializedEvent事件

2.7 續 最后回到SpringApplication.refreshContext(context);方法內部
registerShutdownHook

2.8 listeners.started(context);
發布ApplicationStartedEvent事件

2.9 listeners.running(context);
發布ApplicationReadyEvent事件

3 至此,SpringApplication.run()函數全部執行完畢,也就意味著main()函數執行完畢。main線程結束,java虛擬機啟動一個DestroyJavaVM線程,并等待其他的非daemon線程執行結束。

附:SpringApplicationEvent各個事件的拋出位置

事件類型(SpringApplicationEvent的子類) SpringApplicationRunListener接口的調用方法 本文的章節號
ApplicationStartingEvent starting() 2.2
ApplicationEnvironmentPreparedEvent environmentPrepared() 2.3.3
ApplicationContextInitializedEvent contextPrepared() 2.6.2
ApplicationPreparedEvent contextLoaded() 2.6.4
ApplicationStartedEvent started() 2.8
ApplicationReadyEvent running() 2.9 ·
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容