學習Spring Boot:(二)啟動原理


前言

主要了解前面的程序入口 @@SpringBootApplication 這個注解的結構。

正文

參考《SpringBoot揭秘 快速構建微服務體系》第三章的學習,總結下。

SpringBootApplication背后的秘密

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}

雖然定義使用了多個Annotation進行了原信息標注,但實際上重要的只有三個Annotation:

  • @Configuration(@SpringBootConfiguration點開查看發現里面還是應用了@Configuration)
  • @EnableAutoConfiguration
  • @ComponentScan

所以,如果我們使用如下的SpringBoot啟動類,整個SpringBoot應用依然可以與之前的啟動類功能對等:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

但每次都寫三個Annotation顯然過於繁瑣,所以寫一個@SpringBoot-Application這樣的一站式復合Annotation顯然更方便些。

@Configuration創世紀

這里的@Configuration對我們來說並不陌生,它就是JavaConfig形式的Spring IoC容器的配置類使用的那個@Configuration,既然SpringBoot應用骨子里就是一個Spring應用,那么,自然也需要加載某個IoC容器的配置,而SpringBoot社區推薦使用基於JavaConfig的配置形式,所以,很明顯,這里的啟動類標注了@Configuration之后,本身其實也是一個IoC容器的配置類!
很多SpringBoot的代碼示例都喜歡在啟動類上直接標注@Configuration或者@SpringBootApplication,對於初接觸SpringBoot的開發者來說,其實這種做法不便於理解,如果我們將上面的SpringBoot啟動類拆分為兩個獨立的Java類,整個形勢就明朗了:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class DemoConfiguration {
    @Bean
    public Controller controller() {
        return new Controller();
    }
}
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoConfiguration.class, args);
    }
}

所以,啟動類DemoApplication其實就是一個標准的Standalone類型Java程序的main函數啟動類,沒有什么特殊的。
而@Configuration標注的DemoConfiguration定義其實也是一個普通的JavaConfig形式的IoC容器配置類,沒啥新東西,全是Spring框架里的概念!

不要被這個長篇大論弄模糊了,這個其實在以前學習Spring中也有這些注解,Spring容器中為了簡化XMl配置,允許使用JavaConfig注冊一個Bean。就是使用的是@Configuration,每個擁有注解@Bean的函數的返回值,都將會在SPring啟動時候注冊到容器中,可以使用自動裝配,如下一個JavaConfig的注冊Bean:

@Configuration
public class Configs {
    @Value("classpath:data.json")
    protected File configFile;
    @Bean
    public PersonCfg readServerConfig() throws IOException {
        return new ObjectMapper().readValue(configFile, PersonCfg.class);
    }

@EnableAutoConfiguration的功效

@EnableAutoConfiguration其實也沒啥“創意”,各位是否還記得Spring框架提供的各種名字為@Enable開頭的Annotation定義?比如@EnableScheduling、@EnableCaching、@EnableMBeanExport等,@EnableAutoConfiguration的理念和“做事方式”其實一脈相承,簡單概括一下就是,借助@Import的支持,收集和注冊特定場景相關的bean定義:
* @Enable Scheduling是通過@Import將Spring調度框架相關的bean定義都加載到IoC容器。
* @Enable M Bean Export是通過@Import將JMX相關的bean定義加載到IoC容器。

而@EnableAutoConfiguration也是借助@Import的幫助,將所有符合自動配置條件的bean定義加載到IoC容器,僅此而已!
@EnableAutoConfiguration作為一個復合Annotation,其自身定義關鍵信息如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    ...
}

其中,最關鍵的要屬@Import(EnableAutoConfigurationImportSelector.class),借 助EnableAutoConfigurationImportSelector, @EnableAutoConfiguration可以幫助SpringBoot應用將所有符合條件的@Configuration配置都加載到當前SpringBoot創建並使用的IoC容器,就跟一只“八爪魚”一樣。
借助於Spring框架原有的一個工具類:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以“智能”地自動配置功效才得以大功告成!

自動配置幕后英雄:SpringFactoriesLoader詳解

SpringFactoriesLoader屬於Spring框架私有的一種擴展方案,其主要功能就是從指定的配置文件META-INF/spring.factories加載配置。

public abstract class SpringFactoriesLoader {
    //...
    public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        ...
    }


    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        ....
    }
}

配合@EnableAutoConfiguration使用的話,它更多是提供一種配置查找的功能支持,即根據@EnableAutoConfiguration的完整類名org.springframework.boot.autoconfigure.EnableAutoConfiguration作為查找的Key,獲取對應的一組@Configuration類:

# 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

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer

# 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.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

以上是從SpringBoot的autoconfigure依賴包中的META-INF/spring.factories配置文件中摘錄的一段內容,可以很好地說明問題。
以,@EnableAutoConfiguration自動配置的魔法其實就變成了:從classpath中搜尋所有META-INF/spring.factories配置文件,並將其中org.spring-framework.boot.autoconfigure.EnableAutoConfiguration對應的配置項通過反射(Java Reflection)實例化為對應的標注了@Configuration的JavaConfig形式的IoC容器配置類,然后匯總為一個並加載到IoC容器。

可有可無的@Configuration

@Component Scan的功能其實就是自動掃描並加載符合條件的組件或bean定義,最終將這些bean定義加載到容器中。加載bean定義到Spring的IoC容器,我們可以手工單個注冊,不一定非要通過批量的自動掃描完成,所以說@Component Scan是可有可無的。

深入探索SpringApplication執行流程

SpringApplication的run方法的實現是我們本次旅程的主要線路, 該方法的主要流程大體可以歸納如下:

  1. 如果我們使用的是SpringApplication的靜態run方法,那么,這個方法里面首先需要創建一個SpringApplication對象實例,然后調用這個創建好的SpringApplication的實例run方法。在SpringApplication實例初始化的時候,它會提前做幾件事情:
    • 根據classpath里面是否存在某個特征類(org.springframework.web.context.ConfigurableWebApplicationContext)來決定是否應該創建一個為Web應用使用的ApplicationContext類型,還是應該創建一個標准Standalone應用使用的ApplicationContext類型。
    • 使用SpringFactoriesLoader在應用的classpath中查找並加載所有可用的ApplicationContextInitializer。
    • 使用SpringFactoriesLoader在應用的classpath中查找並加載所有可用的ApplicationListener。
    • 推斷並設置main方法的定義類。
  2. SpringApplication實例初始化完成並且完成設置后,就開始執行run方法的邏輯了,方法執行伊始,首先遍歷執行所有通過SpringFactoriesLoader可以查找到並加載的SpringApplicationRunListener,調用它們的started()方法,告訴這些SpringApplicationRunListener,“嘿,SpringBoot應用要開始執行咯!”。
  3. 創建並配置當前SpringBoot應用將要使用的Environment(包括配置要使用的PropertySource以及Profile)。
  4. 遍歷調用所有SpringApplicationRunListener的environmentPrepared()的方法,告訴它們:“當前SpringBoot應用使用的Environment准備好咯!”。
  5. 如果SpringApplication的showBanner屬性被設置為true,則打印banner(SpringBoot 1.3.x版本,這里應該是基於Banner.Mode決定banner的打印行為)。這一步的邏輯其實可以不關心,我認為唯一的用途就是“好玩”(Just For Fun)。
  6. 根據用戶是否明確設置了applicationContextClass類型以及初始化階段的推斷結果,決定該為當前SpringBoot應用創建什么類型的ApplicationContext並創建完成,然后根據條件決定是否添加ShutdownHook,決定是否使用自定義的BeanNameGenerator,決定是否使用自定義的ResourceLoader,當然,最重要的,將之前准備好的Environment設置給創建好的ApplicationContext使用。
  7. ApplicationContext創建好之后,SpringApplication會再次借助Spring-FactoriesLoader,查找並加載classpath中所有可用的ApplicationContext-Initializer,然后遍歷調用這些ApplicationContextInitializer的initialize (applicationContext)方法來對已經創建好的ApplicationContext進行進一步的處理。
  8. 遍歷調用所有SpringApplicationRunListener的contextPrepared()方法, 通知它們:“SpringBoot應用使用的ApplicationContext准備好啦!”
  9. 最核心的一步,將之前通過@EnableAutoConfiguration獲取的所有配置以及其他形式的IoC容器配置加載到已經准備完畢的ApplicationContext。
  10. 遍歷調用所有SpringApplicationRunListener的contextLoaded()方法,告知所有SpringApplicationRunListener,ApplicationContext”裝填完畢”!
  11. 調用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。
  12. 查找當前ApplicationContext中是否注冊有CommandLineRunner,如果有,則遍歷執行它們。
  13. 正常情況下,遍歷執行SpringApplicationRunListener的finished()方法,告知它們:“搞定!”。(如果整個過程出現異常,則依然調用所有SpringApplicationRunListener的finished()方法,只不過這種情況下會將異常信息一並傳入處理)。

至此,一個完整的SpringBoot應用啟動完畢!

整個過程看起來冗長無比,但其實很多都是一些事件通知的擴展點,如果我們將這些邏輯暫時忽略,那么,其實整個SpringBoot應用啟動的邏輯就可以壓縮到極其精簡的幾步。
springboot.jpg

參考文章

  • 《SpringBoot揭秘 快速構建微服務體系》 第三章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM