1. 程式人生 > >給你一份Spring Boot核心知識清單①-2

給你一份Spring Boot核心知識清單①-2

事件監聽 springboot springfactoriesloader javaconfig

由於博客字數限制,不允許發大於20w個字符的文章,所以需要分成兩篇,接上文

五、出神入化:揭秘自動配置原理

典型的Spring Boot應用的啟動類一般均位於 src/main/java根路徑下,比如 MoonApplication類:

@SpringBootApplication
public class MoonApplication {

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

其中 @SpringBootApplication

開啟組件掃描和自動配置,而 SpringApplication.run則負責啟動引導應用程序。 @SpringBootApplication是一個復合 Annotation,它將三個有用的註解組合在一起:

@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 {
    // ......
}

@SpringBootConfiguration就是 @Configuration,它是Spring框架的註解,標明該類是一個 JavaConfig配置類。而 @ComponentScan啟用組件掃描,前文已經詳細講解過,這裏著重關註 @EnableAutoConfiguration

@EnableAutoConfiguration註解表示開啟Spring Boot自動配置功能,Spring Boot會根據應用的依賴、自定義的bean、classpath下有沒有某個類 等等因素來猜測你需要的bean,然後註冊到IOC容器中。那 @EnableAutoConfiguration是如何推算出你的需求?首先看下它的定義:

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

你的關註點應該在 @Import(EnableAutoConfigurationImportSelector.class)上了,前文說過, @Import註解用於導入類,並將這個類作為一個bean的定義註冊到容器中,這裏它將把 EnableAutoConfigurationImportSelector作為bean註入到容器中,而這個類會將所有符合條件的@Configuration配置都加載到容器中,看看它的代碼:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 省略了大部分代碼,保留一句核心代碼
    // 註意:SpringBoot最近版本中,這句代碼被封裝在一個單獨的方法中
    // SpringFactoriesLoader相關知識請參考前文
    List<String> factories = new ArrayList<String>(new LinkedHashSet<String>(  
        SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader)));
}

這個類會掃描所有的jar包,將所有符合條件的@Configuration配置類註入的容器中,何為符合條件,看看 META-INF/spring.factories的文件內容:

// 來自 org.springframework.boot.autoconfigure下的META-INF/spring.factories
// 配置的key = EnableAutoConfiguration,與代碼中一致
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.....

DataSourceAutoConfiguration為例,看看Spring Boot是如何自動配置的:

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {
}

分別說一說:

  • @ConditionalOnClass({DataSource.class,EmbeddedDatabaseType.class}):當Classpath中存在DataSource或者EmbeddedDatabaseType類時才啟用這個配置,否則這個配置將被忽略。

  • @EnableConfigurationProperties(DataSourceProperties.class):將DataSource的默認配置類註入到IOC容器中,DataSourceproperties定義為:

// 提供對datasource配置信息的支持,所有的配置前綴為:spring.datasource
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties  {
    private ClassLoader classLoader;
    private Environment environment;
    private String name = "testdb";
    ......
}

  • @Import({Registrar.class,DataSourcePoolMetadataProvidersConfiguration.class}):導入其他額外的配置,就以 DataSourcePoolMetadataProvidersConfiguration為例吧。

@Configuration
public class DataSourcePoolMetadataProvidersConfiguration {

    @Configuration
    @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
    static class TomcatDataSourcePoolMetadataProviderConfiguration {
        @Bean
        public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() {
            .....
        }
    }
  ......
}

DataSourcePoolMetadataProvidersConfiguration是數據庫連接池提供者的一個配置類,即Classpath中存在 org.apache.tomcat.jdbc.pool.DataSource.class,則使用tomcat-jdbc連接池,如果Classpath中存在 HikariDataSource.class則使用Hikari連接池。

這裏僅描述了DataSourceAutoConfiguration的冰山一角,但足以說明Spring Boot如何利用條件話配置來實現自動配置的。回顧一下, @EnableAutoConfiguration中導入了EnableAutoConfigurationImportSelector類,而這個類的 selectImports()通過SpringFactoriesLoader得到了大量的配置類,而每一個配置類則根據條件化配置來做出決策,以實現自動配置。

整個流程很清晰,但漏了一個大問題: EnableAutoConfigurationImportSelector.selectImports()是何時執行的?其實這個方法會在容器啟動過程中執行: AbstractApplicationContext.refresh(),更多的細節在下一小節中說明。

六、啟動引導:Spring Boot應用啟動的秘密

6.1 SpringApplication初始化

SpringBoot整個啟動流程分為兩個步驟:初始化一個SpringApplication對象、執行該對象的run方法。看下SpringApplication的初始化流程,SpringApplication的構造方法中調用initialize(Object[] sources)方法,其代碼如下:

private void initialize(Object[] sources) {
     if (sources != null && sources.length > 0) {
         this.sources.addAll(Arrays.asList(sources));
     }
     // 判斷是否是Web項目
     this.webEnvironment = deduceWebEnvironment();
     setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
     setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
     // 找到入口類
     this.mainApplicationClass = deduceMainApplicationClass();
}

初始化流程中最重要的就是通過SpringFactoriesLoader找到 spring.factories文件中配置的 ApplicationContextInitializerApplicationListener兩個接口的實現類名稱,以便後期構造相應的實例。 ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,對ConfigurableApplicationContext實例做進一步的設置或處理。ConfigurableApplicationContext繼承自ApplicationContext,其主要提供了對ApplicationContext進行設置的能力。

實現一個ApplicationContextInitializer非常簡單,因為它只有一個方法,但大多數情況下我們沒有必要自定義一個ApplicationContextInitializer,即便是Spring Boot框架,它默認也只是註冊了兩個實現,畢竟Spring的容器已經非常成熟和穩定,你沒有必要來改變它。

ApplicationListener的目的就沒什麽好說的了,它是Spring框架對Java事件監聽機制的一種框架實現,具體內容在前文Spring事件監聽機制這個小節有詳細講解。這裏主要說說,如果你想為Spring Boot應用添加監聽器,該如何實現?

Spring Boot提供兩種方式來添加自定義監聽器:

  • 通過 SpringApplication.addListeners(ApplicationListener<?>...listeners)或者 SpringApplication.setListeners(Collection<?extendsApplicationListener<?>>listeners)兩個方法來添加一個或者多個自定義監聽器

  • 既然SpringApplication的初始化流程中已經從 spring.factories中獲取到 ApplicationListener的實現類,那麽我們直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:

org.springframework.context.ApplicationListener=cn.moondev.listeners.xxxxListener\

關於SpringApplication的初始化,我們就說這麽多。

6.2 Spring Boot啟動流程

Spring Boot應用的整個啟動流程都封裝在SpringApplication.run方法中,其整個流程真的是太長太長了,但本質上就是在Spring容器啟動的基礎上做了大量的擴展,按照這個思路來看看源碼:

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        // ①
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            // ②
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
            // ③
            Banner printedBanner = printBanner(environment);
            // ④
            context = createApplicationContext();
            // ⑤
            analyzers = new FailureAnalyzers(context);
            // ⑥
            prepareContext(context, environment, listeners, applicationArguments,printedBanner);
            // ⑦ 
            refreshContext(context);
            // ⑧
            afterRefresh(context, applicationArguments);
            // ⑨
            listeners.finished(context, null);
            stopWatch.stop();
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, analyzers, ex);
            throw new IllegalStateException(ex);
        }
    }

① 通過SpringFactoriesLoader查找並加載所有的 SpringApplicationRunListeners,通過調用starting()方法通知所有的SpringApplicationRunListeners:應用開始啟動了。SpringApplicationRunListeners其本質上就是一個事件發布者,它在SpringBoot應用啟動的不同時間點發布不同應用事件類型(ApplicationEvent),如果有哪些事件監聽者(ApplicationListener)對這些事件感興趣,則可以接收並且處理。還記得初始化流程中,SpringApplication加載了一系列ApplicationListener嗎?這個啟動流程中沒有發現有發布事件的代碼,其實都已經在SpringApplicationRunListeners這兒實現了。

簡單的分析一下其實現流程,首先看下SpringApplicationRunListener的源碼:

public interface SpringApplicationRunListener {

    // 運行run方法時立即調用此方法,可以用戶非常早期的初始化工作
    void starting();
    
    // Environment準備好後,並且ApplicationContext創建之前調用
    void environmentPrepared(ConfigurableEnvironment environment);

    // ApplicationContext創建好後立即調用
    void contextPrepared(ConfigurableApplicationContext context);

    // ApplicationContext加載完成,在refresh之前調用
    void contextLoaded(ConfigurableApplicationContext context);

    // 當run方法結束之前調用
    void finished(ConfigurableApplicationContext context, Throwable exception);

}

SpringApplicationRunListener只有一個實現類: EventPublishingRunListener。①處的代碼只會獲取到一個EventPublishingRunListener的實例,我們來看看starting()方法的內容:

public void starting() {
    // 發布一個ApplicationStartedEvent
    this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));
}

順著這個邏輯,你可以在②處的 prepareEnvironment()方法的源碼中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二個方法,那不出你所料, environmentPrepared()又發布了另外一個事件 ApplicationEnvironmentPreparedEvent。接下來會發生什麽,就不用我多說了吧。

② 創建並配置當前應用將要使用的 Environment,Environment用於描述應用程序當前的運行環境,其抽象了兩個方面的內容:配置文件(profile)和屬性(properties),開發經驗豐富的同學對這兩個東西一定不會陌生:不同的環境(eg:生產環境、預發布環境)可以使用不同的配置文件,而屬性則可以從配置文件、環境變量、命令行參數等來源獲取。因此,當Environment準備好後,在整個應用的任何時候,都可以從Environment中獲取資源。

總結起來,②處的兩句代碼,主要完成以下幾件事:

  • 判斷Environment是否存在,不存在就創建(如果是web項目就創建 StandardServletEnvironment,否則創建 StandardEnvironment

  • 配置Environment:配置profile以及properties

  • 調用SpringApplicationRunListener的 environmentPrepared()方法,通知事件監聽者:應用的Environment已經準備好

③、SpringBoot應用在啟動時會輸出這樣的東西:

  .   ____          _            __ _ _
 /\\ / ___‘_ __ _ _(_)_ __  __ _ \ \ \ ( ( )\___ | ‘_ | ‘_| | ‘_ \/ _` | \ \ \  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  ‘  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.6.RELEASE)

如果想把這個東西改成自己的塗鴉,你可以研究以下Banner的實現,這個任務就留給你們吧。

④、根據是否是web項目,來創建不同的ApplicationContext容器。

⑤、創建一系列 FailureAnalyzer,創建流程依然是通過SpringFactoriesLoader獲取到所有實現FailureAnalyzer接口的class,然後在創建對應的實例。FailureAnalyzer用於分析故障並提供相關診斷信息。

⑥、初始化ApplicationContext,主要完成以下工作:

  • 將準備好的Environment設置給ApplicationContext

  • 遍歷調用所有的ApplicationContextInitializer的 initialize()方法來對已經創建好的ApplicationContext進行進一步的處理

  • 調用SpringApplicationRunListener的 contextPrepared()方法,通知所有的監聽者:ApplicationContext已經準備完畢

  • 將所有的bean加載到容器中

  • 調用SpringApplicationRunListener的 contextLoaded()方法,通知所有的監聽者:ApplicationContext已經裝載完畢

⑦、調用ApplicationContext的 refresh()方法,完成IoC容器可用的最後一道工序。從名字上理解為刷新容器,那何為刷新?就是插手容器的啟動,聯系一下第一小節的內容。那如何刷新呢?且看下面代碼:

// 摘自refresh()方法中一句代碼
invokeBeanFactoryPostProcessors(beanFactory);

看看這個方法的實現:

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
    ......
}

獲取到所有的 BeanFactoryPostProcessor來對容器做一些額外的操作。BeanFactoryPostProcessor允許我們在容器實例化相應對象之前,對註冊到容器的BeanDefinition所保存的信息做一些額外的操作。這裏的getBeanFactoryPostProcessors()方法可以獲取到3個Processor:

ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
ConfigFileApplicationListener$PropertySourceOrderingPostProcessor

不是有那麽多BeanFactoryPostProcessor的實現類,為什麽這兒只有這3個?因為在初始化流程獲取到的各種ApplicationContextInitializer和ApplicationListener中,只有上文3個做了類似於如下操作:

public void initialize(ConfigurableApplicationContext context) {
    context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
}

然後你就可以進入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,這個方法除了會遍歷上面的3個BeanFactoryPostProcessor處理外,還會獲取類型為 BeanDefinitionRegistryPostProcessor的bean: org.springframework.context.annotation.internalConfigurationAnnotationProcessor,對應的Class為 ConfigurationClassPostProcessorConfigurationClassPostProcessor用於解析處理各種註解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。當處理 @import註解的時候,就會調用<自動配置>這一小節中的 EnableAutoConfigurationImportSelector.selectImports()來完成自動配置功能。其他的這裏不再多講,如果你有興趣,可以查閱參考資料6。

⑧、查找當前context中是否註冊有CommandLineRunner和ApplicationRunner,如果有則遍歷執行它們。

⑨、執行所有SpringApplicationRunListener的finished()方法。

這就是Spring Boot的整個啟動流程,其核心就是在Spring容器初始化並啟動的基礎上加入各種擴展點,這些擴展點包括:ApplicationContextInitializer、ApplicationListener以及各種BeanFactoryPostProcessor等等。你對整個流程的細節不必太過關註,甚至沒弄明白也沒有關系,你只要理解這些擴展點是在何時如何工作的,能讓它們為你所用即可。

整個啟動流程確實非常復雜,可以查詢參考資料中的部分章節和內容,對照著源碼,多看看,我想最終你都能弄清楚的。言而總之,Spring才是核心,理解清楚Spring容器的啟動流程,那Spring Boot啟動流程就不在話下了。

參考資料

[1] 王福強 著;SpringBoot揭秘:快速構建微服務體系; 機械工業出版社, 2016

[2] 王福強 著;Spring揭秘; 人民郵件出版社, 2009

[3] Craig Walls 著;丁雪豐 譯;Spring Boot實戰;中國工信出版集團 人民郵電出版社,2016

[4] 深入探討 Java 類加載器

[5] spring boot實戰:自動配置原理分析

[6] spring boot實戰:Spring boot Bean加載源碼分析

本文出自 “迷失的月亮” 博客,請務必保留此出處http://luecsc.blog.51cto.com/2219432/1964057

給你一份Spring Boot核心知識清單①-2