1. 程式人生 > >run方法(四)之prepareContext,絕對有值得你看的地方

run方法(四)之prepareContext,絕對有值得你看的地方

前言

  此係列是針對springboot的啟動,旨在於和大家一起來看看springboot啟動的過程中到底做了一些什麼事。如果大家對springboot的原始碼有所研究,可以挑些自己感興趣或者對自己有幫助的看;但是如果大家沒有研究過springboot的原始碼,不知道springboot在啟動過程中做了些什麼,那麼我建議大家從頭開始一篇一篇按順序讀該系列,不至於從中途插入,看的有些懵懂。當然,文中講的不對的地方也歡迎大家指出,有待改善的地方也希望大家不吝賜教。老規矩:一週至少一更,中途會不定期的更新一些其他的部落格,可能是springboot的原始碼,也可能是其他的原始碼解析,也有可能是其他的。

  路漫漫其修遠兮,吾將上下而求索!

前情回顧

  大家還記得上篇博文講了什麼嗎,或者說大家知道上篇博文講了什麼嗎。這裡幫大家做個簡單回顧:

    建立web應用上下文,對其部分屬性:reader、scanner、beanFactory進行了例項化;reader中例項化了屬性conditionEvaluator;scanner中添加了兩個AnnotationTypeFilter:一個針對@Component,一個針對@ManagedBean;beanFactory中註冊了8個註解配置處理器的Bean。應用上下文型別實際上是AnnotationConfigServletWebServerApplicationContext,beanFactory的型別是DefaultListableBeanFactory,這兩個型別的類圖大家重點看下,既是上篇博文的重點,也是接下來系列部落格的基點。建立上下文的過程其實還建立了environment,本文中會涉及到environment,大家請留意。

    通過createApplicationContext方法之後,context的包含的主要內容如下:

prepareContext

  先欣賞下我們的戰績,看看我們對run方法完成了多少的原始碼解讀

/**
 * Run the Spring application, creating and refreshing a new
 * {@link ApplicationContext}.
 * @param args the application arguments (usually passed from a Java main method)
 * @return a running {
@link ApplicationContext} */ public ConfigurableApplicationContext run(String... args) { // 秒錶,用於記錄啟動時間;記錄每個任務的時間,最後會輸出每個任務的總費時 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // spring應用上下文,也就是我們所說的spring根容器 ConfigurableApplicationContext context = null; // 自定義SpringApplication啟動錯誤的回撥介面 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 設定jdk系統屬性java.awt.headless,預設情況為true即開啟 configureHeadlessProperty(); // 獲取啟動時監聽器(EventPublishingRunListener例項) SpringApplicationRunListeners listeners = getRunListeners(args) // 觸發ApplicationStartingEvent事件,啟動監聽器會被呼叫,一共5個監聽器被呼叫,但只有兩個監聽器在此時做了事 listeners.starting(); try { // 引數封裝,也就是在命令列下啟動應用帶的引數,如--server.port=9000 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); // 準備環境:1、載入外部化配置的資源到environment;2、觸發ApplicationEnvironmentPreparedEvent事件 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 配置spring.beaninfo.ignore,並新增到名叫systemProperties的PropertySource中;預設為true即開啟 configureIgnoreBeanInfo(environment); // 列印banner圖 Banner printedBanner = printBanner(environment); // 建立應用上下文,並例項化了其三個屬性:reader、scanner和beanFactory context = createApplicationContext(); // 獲取異常報道器,即載入spring.factories中的SpringBootExceptionReporter實現類 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 準備上下文,本文重點 prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
View Code

  前菜

    exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);

      getSpringFactoriesInstances這個方法在之前已經講過,就是載入META-INF/spring.factories中指定型別的bean集合。如下圖

    SpringBootExceptionReporter是一個回撥介面,用於支援對SpringApplication啟動錯誤的自定義報告。

    先根據SpringBootExceptionReporter獲取FailureAnalyzers的全限定類名,例項化FailureAnalyzers的時候,再次呼叫SpringFactoriesLoader.loadFactoryNames方法獲取型別為FailureAnalyzer的名稱列表,然後再根據名稱列表例項化bean列表。

    bean列表建立好之後,設定bean列表中滿足條件的bean的beanFactory和environment,同時也將部分bean應用到context的environment和beanFactory中,程式碼如下

private void prepareFailureAnalyzers(List<FailureAnalyzer> analyzers,
        ConfigurableApplicationContext context) {
    for (FailureAnalyzer analyzer : analyzers) {
        prepareAnalyzer(context, analyzer);
    }
}

private void prepareAnalyzer(ConfigurableApplicationContext context,
        FailureAnalyzer analyzer) {
    if (analyzer instanceof BeanFactoryAware) {
        ((BeanFactoryAware) analyzer).setBeanFactory(context.getBeanFactory());
    }
    if (analyzer instanceof EnvironmentAware) {
        ((EnvironmentAware) analyzer).setEnvironment(context.getEnvironment());
    }
}
View Code

    其中NoSuchBeanDefinitionFailureAnalyer bean的setBeanFactory方法

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory);
    this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    this.metadataReaderFactory = new CachingMetadataReaderFactory(
            this.beanFactory.getBeanClassLoader());
    // Get early as won't be accessible once context has failed to start
    this.report = ConditionEvaluationReport.get(this.beanFactory);        // 往beanFactory中註冊autoConfigurationReport
}
View Code

      往beanFactory中註冊一個名叫autoConfigurationReport的單例bean(型別是ConditionEvaluationReport),這個bean用於後面自動配置條件評估的詳情報告與日誌記錄。

    exceptionReporters 獲取成功後,我們來看看beanFactory的變化

  正餐

    prepareContext內容不多,原始碼如下

private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
    // 設定上下文的environment
    context.setEnvironment(environment);
    // 應用上下文後處理
    postProcessApplicationContext(context);
    // 在context refresh之前,對其應用ApplicationContextInitializer
    applyInitializers(context);
    // 上下文準備(目前是空實現,可用於拓展)
    listeners.contextPrepared(context);
    // 列印啟動日誌和啟動應用的Profile
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }

    // Add boot specific singleton beans
    context.getBeanFactory().registerSingleton("springApplicationArguments",
            applicationArguments);                                // 向beanFactory註冊單例bean:命令列引數bean
    if (printedBanner != null) {
        // 向beanFactory註冊單例bean:banner bean
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
    }

    // Load the sources
    Set<Object> sources = getAllSources();                        // 獲取全部資源,其實就一個:SpringApplication的primarySources屬性
    Assert.notEmpty(sources, "Sources must not be empty");        // 斷言資源是否為空
    // 將bean載入到應用上下文中
    load(context, sources.toArray(new Object[0]));
    // 向上下文中新增ApplicationListener,並廣播ApplicationPreparedEvent事件
    listeners.contextLoaded(context);
}
View Code

    我們逐個方法來看

    context.setEnvironment(environment)

/**
 * {@inheritDoc}
 * <p>
 * Delegates given environment to underlying {@link AnnotatedBeanDefinitionReader} and
 * {@link ClassPathBeanDefinitionScanner} members.
 */
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
    super.setEnvironment(environment);            // 設定context的environment
    this.reader.setEnvironment(environment);    // 例項化context的reader屬性的conditionEvaluator屬性
    this.scanner.setEnvironment(environment);    // 設定context的scanner屬性的environment屬性
}
View Code

      將context中相關的environment全部替換成SpringApplication中建立的environment。還記得這篇中的疑問嗎,引申下就是:之前我們的應用中有兩個environment,一個在context中,一個在SpringApplication中。經過此方法後,就只會存在SpringApplication中的environment了,而context中的原environment會被回收

    postProcessApplicationContext(context);

/**
 * Apply any relevant post processing the {@link ApplicationContext}. Subclasses can
 * apply additional processing as required.
 * @param context the application context
 */
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
    if (this.beanNameGenerator != null) {
        context.getBeanFactory().registerSingleton(
                AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
                this.beanNameGenerator);
    }
    if (this.resourceLoader != null) {
        if (context instanceof GenericApplicationContext) {
            ((GenericApplicationContext) context)
                    .setResourceLoader(this.resourceLoader);
        }
        if (context instanceof DefaultResourceLoader) {
            ((DefaultResourceLoader) context)
                    .setClassLoader(this.resourceLoader.getClassLoader());
        }
    }
}
View Code

      上下文後處理。SpringApplication子類可以根據需要應用其他處理。

      由於當前SpringApplication例項的屬性:beanNameGenerator和resourceLoader都為null,所以此方法目前相當於什麼也沒做。此方法可能是我們定製SpringApplication所用。

    applyInitializers(context);

/**
 * Apply any {@link ApplicationContextInitializer}s to the context before it is
 * refreshed.
 * @param context the configured ApplicationContext (not refreshed yet)
 * @see ConfigurableApplicationContext#refresh()
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {
    for (ApplicationContextInitializer initializer : getInitializers()) {
        // 解析當前initializer實現的ApplicationContextInitializer的泛型引數
        Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
                initializer.getClass(), ApplicationContextInitializer.class);
        // 斷言context是否是requiredType的例項
        Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
        // 向context應用初始化器
        initializer.initialize(context);
    }
}
View Code

      在context refresh之前應用ApplicationContextInitializer到context中。還記得SpringApplication的屬性initializers嗎,不記得的可以點這裡

      一共6個initializer,他們的initialize方法都被呼叫,原始碼就不跟了,上圖中已經進行了展示,我們總結下

      DelegatingApplicationContextInitializer

        environment沒有context.initializer.classes配置項,所以相當於沒有做任何事。

        如果配置了context.initializer.classes,獲取其值(逗號分隔的initializer列表字串),轉換成class列表,根據classes列表進行例項化獲取initializer例項列表,再對每個initializer例項呼叫initialize方法。

        DelegatingApplicationContextInitializer相當於context.initializer.classes的代理,最終還是會執行到被代理的initializer的initialize方法。      ContextIdApplicationContextInitializer

        設定application id:從environment中獲取spring.application.name配置項的值,並把設定成application id,若沒有配置spring.application.name,則取預設值application;

        將application id封裝成ContextId物件,註冊到beanFactory中。

      ConfigurationWarningsApplicationContextInitializer

        向上下文註冊了一個BeanFactoryPostProcessor:ConfigurationWarningsPostProcessor例項;

        例項化ConfigurationWarningsPostProcessor的時候,也例項化了它的屬性Check[] checks,check中只有一個型別是ComponentScanPackageCheck的例項。      ServerPortInfoApplicationContextInitializer

        向上下文註冊了一個ApplicationListener:ServerPortInfoApplicationContextInitializer物件自己;

        ServerPortInfoApplicationContextInitializer實現了ApplicationListener<WebServerInitializedEvent>,所以他本身就是一個ApplicationListener。      SharedMetadataReaderFactoryContextInitializer

        向context註冊了一個BeanFactoryPostProcessor:CachingMetadataReaderFactoryPostProcessor例項。      ConditionEvaluationReportLoggingListener

        將上下文賦值給自己的屬性applicationContext;

        向上下文註冊了一個ApplicationListener:ConditionEvaluationReportListener例項;

        從beanFactory中獲取名為autoConfigurationReport的bean賦值給自己的屬性report。

    listeners.contextPrepared(context);

      還記得SpringApplicationRunListeners中listeners屬性嗎,沒錯,裡面就一個EventPublishingRunListener物件。

      呼叫EventPublishingRunListener的contextPrepared,發現其是空實現。

      也就是相當於啥事也沒做。

    load(context, sources.toArray(new Object[0]));

      建立了一個BeanDefinitionLoader物件;BeanDefinitionLoader作為AnnotatedBeanDefinitionReader,XmlBeanDefinitionReader和ClassPathBeanDefinitionScanner的門面,從底層源載入bean定義,包括XML和JavaConfig;

      能被載入的source型別包括:Class、Resource、Package和CharSequence四種,每種型別的載入方式也不一樣,Class用AnnotatedBeanDefinitionReader處理、Resource用XmlBeanDefinitionReader處理、Package用ClassPathBeanDefinitionScanner,而CharSequence則比較特殊了,它按Class、Resource、Package的順序處理,哪種處理成功就按哪種處理(CharSequence方式貌似很少用,反正我還沒用過);

      而目前我們的source只有一個:class com.lee.shiro.ShiroApplication,是class型別;先判斷ShiroApplication是否有被component註解修飾,很顯然是(SpringBootApplication註解中包含component註解),那麼AnnotatedBeanDefinitionReader來處理:將com.lee.shiro.ShiroApplication封裝成一個名叫ShiroApplication的BeanDefinition物件,並將其註冊到了beanFactory的BeanDefinitionMap中。

    listeners.contextLoaded(context);

      還記得SpringApplication的屬性listeners嗎,不記得的可以點這裡。將這些ApplicationListener註冊到了上下文中,具體包括ConfigFileApplicationListener,AnsiOutputApplicationListener,LoggingApplicationListener,ClasspathLoggingApplicationListener,BackgroundPreinitializer,DelegatingApplicationListener,ParentContextCloserApplicationListener(實現了ApplicationContextAware介面;將上下文賦值給了屬性context,相當於有了上下文的引用),ClearCachesApplicationListener,FileEncodingApplicationListener,LiquibaseServiceLocatorApplicationListener,EnableEncryptablePropertiesBeanFactoryPostProcessor。

      廣播ApplicationPreparedEvent事件,並觸發對應的事件。過濾出匹配事件的監聽器可以檢視這裡,一共過濾出5個監聽器,他們的onApplicationEvent方法會被呼叫,具體做了如下事情:

        ConfigFileApplicationListener

          向context註冊了一個BeanFactoryPostProcessor:PropertySourceOrderingPostProcessor例項;該例項後面會對我們的property sources進行重排序,另外該例項擁有上下文的引用。

        LoggingApplicationListener

          向beanFactory中註冊了一個名叫springBootLoggingSystem的單例bean,也就是我們的日誌系統bean。

        BackgroundPreinitializer

          目前什麼也沒做

        DelegatingApplicationListener

          目前什麼也沒做

        EnableEncryptablePropertiesBeanFactoryPostProcessor

          僅僅列印了一句debug日誌,相當於什麼也沒做

  甜點

    一開始還以為本文內容不會多,但分析分析著,發現內容不少。不管我們是吃撐了還是沒吃飽,都來點甜點收尾。

    一般一個單例物件註冊到beanFactory中,beanFactory會有2個屬性都新增此單例物件資訊:singletonObjects、registeredSingletons

      Map<String, Object> singletonObjects = new ConcurrentHashMap<>(),key是bean name,value是單例物件

      Set<String> registeredSingletons = new LinkedHashSet<>(),存放的是bean name

    一般一個bean定義註冊到beanFactory中是,beanFactory也會有2個屬相會新增此bean定義資訊:beanDefinitionMap、beanDefinitionNames 

      List<String> beanDefinitionNames = new ArrayList<>(),beanDefinition的名稱列表      Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(),key是beanDefinition的名稱,value是beanDefinition物件

    另外beanFactory中Set<String> manualSingletonNames = new LinkedHashSet<>,按註冊順序存放手動註冊的單例的名稱。

    load方法,我會放到另一篇博文中重點分析;load負責載入bean定義資源,應該是挺重要的,而本文卻講的比較粗糙,我們一起期待吧。

    有時候,不是對手有多強大,只是我們不敢去嘗試;勇敢踏出第一步,你會發現自己比想象中更優秀!誠如海因斯第一次跑進人類10s大關時所說:上帝啊,原來那扇門是虛掩著的!

總結

  1、上文中的load

    就是載入bean定義資源,支援4種方式:Class、Resource、Package和CharSequence。

    Class:註解形式的Bean定義;AnnotatedBeanDefinitionReader負責處理。

    Resource:一般而言指的是xml bean配置檔案,也就是我們在spring中常用的xml配置。xml的載入大家可以去閱讀《Spring原始碼深度解析》。說的簡單點就是:將xml的bean定義封裝成BeanDefinition並註冊到beanFactory的BeanDefinitionMap中;XmlBeanDefinitionReader負責處理。

    Package:以掃包的方式掃描bean定義; ClassPathBeanDefinitionScanner負責處理。

    CharSequence:以先後順序進行匹配Class、Resource或Package進行載入,誰匹配上了就用誰的處理方式處理。

    當然還支援Groovy形式的Bean定義,有興趣的朋友可以自行去跟下原始碼。

    springboot鼓勵用java類實現java bean定義,所以springboot應用中,我們一般只需要關注Class方式、Package方式即可。

  2、prepareContext到底做了什麼    

    1、將context中的environment替換成SpringApplication中建立的environment    2、將SpringApplication中的initializers應用到context中       設定application id,並將application id封裝成ContextId物件,註冊到beanFactory中       向context的beanFactoryPostProcessors中註冊了一個ConfigurationWarningsPostProcessor例項       向context的applicationListeners中註冊了一個ServerPortInfoApplicationContextInitializer例項       向context的beanFactoryPostProcessors中註冊了一個CachingMetadataReaderFactoryPostProcessor例項       向context的applicationListeners中註冊了一個ConditionEvaluationReportListener例項

    3、載入兩個單例bean到beanFactory中

      向beanFactory中註冊了一個名叫springApplicationArguments的單例bean,該bean封裝了我們的命令列引數;      向beanFactory中註冊了一個名叫springBootBanner的單例bean。

    4、載入bean定義資源       資原始檔只有SpringApplication的primarySources集合,裡面就一個資源類:com.lee.shiro.ShiroApplication;       將該資源封裝成了名叫ShiroApplication的BeanDefinition物件,並將其註冊到了beanFactory的BeanDefinitionMap中。    5、將SpringApplication中的listeners註冊到context中,並廣播ApplicationPreparedEvent事件       總共11個ApplicationListener註冊到了context的applicationListeners中;       ApplicationPreparedEvent事件的監聽器一共做了兩件事         向context的beanFactoryPostProcessors中註冊了一個PropertySourceOrderingPostProcessor例項         向beanFactory中註冊了一個名叫springBootLoggingSystem的單例bean,也就是我們的日誌系統bean

    context中主要是三個屬性增加了內容:beanFactory、beanFactoryPostProcessors和applicationListeners,到目前為止,context的內容如下

參考

  《Spring原始碼深度解析》

  Spring boot 原始碼