關(guān)于springboot啟動(dòng)配置加載的那點(diǎn)事兒

前言

現(xiàn)在幾乎所有的java開(kāi)發(fā)都會(huì)用到springboot,除了很老很老的項(xiàng)目,應(yīng)該不會(huì)再有人直接用jsp,servlet等寫web項(xiàng)目了吧,直接用spring的都很少見(jiàn)了。
今天發(fā)生的這個(gè)問(wèn)題就得從springboot說(shuō)起。我們都知道springboot遵循約定大于配置的規(guī)則,盡量將spring中的配置減少,幾行代碼就可以跑一個(gè)web項(xiàng)目,但是默認(rèn)的東西越多,其實(shí)隱藏的東西也就越多,一旦碰到什么問(wèn)題,如果沒(méi)點(diǎn)準(zhǔn)備,真的是會(huì)手忙腳亂的。

問(wèn)題描述

說(shuō)起來(lái)也不復(fù)雜,新拆分了一個(gè)項(xiàng)目,公司用的apollo作為配置中心,所以要做一些基本配置,讓新項(xiàng)目從apollo拉取配置。本以為簡(jiǎn)單的Ctrl+C,Ctrl+V搞定,結(jié)果啟動(dòng)的時(shí)候報(bào)dubbo配置找不到,反復(fù)查看apollo,一個(gè)字母一個(gè)字母的比對(duì)過(guò)去,確定配置沒(méi)有配錯(cuò)的,但是怎么就找不到呢?

源碼分析

一般這種問(wèn)題想從網(wǎng)上找到答案的可能性微乎其微,只好自己動(dòng)手,深入源碼,看看到底是哪個(gè)妖怪在作祟。

SpringApplicationRunListener和ApplicationContextInitializer

Dubbo配置的加載是架構(gòu)組jar包提供的,定義了一個(gè)SpringApplicationRunListener的實(shí)現(xiàn)類,而apollo的配置加載是在ApplicationContextInitializer的一個(gè)實(shí)現(xiàn)類ApolloApplicationContextInitializer中處理的。所以一開(kāi)始的懷疑Listener和Initializer在應(yīng)用啟動(dòng)的時(shí)候的順序問(wèn)題。
因?yàn)镈ubboListener里面加載dubbo的配置的代碼在SpringApplicationRunListener的contextLoaded方法中,多以可以從下面SpringApplication的源碼看到,Initializer的initialize方法是在Listener的contextLoaded方法之前執(zhí)行的

private void prepareContext(ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    // Initializer的initialize方法在這里執(zhí)行
    applyInitializers(context);
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }

    // Add boot specific singleton beans
    context.getBeanFactory().registerSingleton("springApplicationArguments",
            applicationArguments);
    if (printedBanner != null) {
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
    }

    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
    // Listener的contextLoaded方法在這里執(zhí)行
    listeners.contextLoaded(context);
}

debug源碼

猜測(cè)不對(duì),只好跟著啟動(dòng)的源碼debug觀察了。這里省略一開(kāi)始的猜測(cè)和嘗試,直接進(jìn)入重點(diǎn):調(diào)試的時(shí)候有個(gè)發(fā)現(xiàn),apollo的是否啟用的配置值居然是false!可以從代碼看到,方法直接返回了,并沒(méi)有去加載apollo中的配置值,所以并不是順序有問(wèn)題,而是一開(kāi)始是否啟用的配置值有問(wèn)題。

public void initialize(ConfigurableApplicationContext context) {
    ConfigurableEnvironment environment = context.getEnvironment();

    initializeSystemProperty(environment);
    // 如果這里配置是false,那方法直接return了
    String enabled = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "false");
    if (!Boolean.valueOf(enabled)) {
      logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
      return;
    }
    // 省略

查看項(xiàng)目中的application.properties文件,沒(méi)啥問(wèn)題啊(本來(lái)就是其他項(xiàng)目拷過(guò)來(lái)的,能有啥問(wèn)題),那怎么就讀成false了呢?

apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=application,dev.common

只好繼續(xù)debug讓人頭大的源碼,還是略過(guò)那些不重要的代碼,直接來(lái)看重點(diǎn)和結(jié)論:

  1. 第一次進(jìn)入SimpleApplicationEventMulticaster的這個(gè)方法,這個(gè)時(shí)候其實(shí)是應(yīng)用啟動(dòng)事件,發(fā)現(xiàn)有4個(gè)監(jiān)聽(tīng)器,但看上去沒(méi)一個(gè)像跟配置有關(guān)的


  2. 第二次進(jìn)這個(gè)方法,這個(gè)時(shí)候事件類型是環(huán)境準(zhǔn)備事件,其中有個(gè)監(jiān)聽(tīng)器是ConfigFileApplicationListener


  3. 進(jìn)入ConfigFileApplicationListener的事件處理方法


  4. 通過(guò)ConfigFileApplicationListener的方法調(diào)用鏈onApplicationEnvironmentPreparedEvent --> postProcessEnvironment --> addPropertySources直接進(jìn)入到內(nèi)部類Loader的下面這個(gè)方法,其中常量NO_SEARCH_NAMES是包含單個(gè)null元素的集合,這里獲取到的names就是配置文件的名稱。
private void load(Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
        names.forEach(
                (name) -> load(location, name, profile, filterFactory, consumer));
    });
}
  1. 小心翼翼的一步一步往下走,本以為不會(huì)走進(jìn)if條件,這樣讀到的就是application配置文件了,結(jié)果居然走了進(jìn)去,而且"spring.config.name"對(duì)應(yīng)的value是boostrap,而項(xiàng)目里根本沒(méi)有配置boostrap.yaml文件,那這個(gè)值是哪來(lái)的呢?


  2. 找了個(gè)能正常啟動(dòng)的項(xiàng)目,同樣的地方打上了斷點(diǎn),發(fā)現(xiàn)沒(méi)進(jìn)if條件,"spring.config.name"這個(gè)配置到底什么時(shí)候設(shè)置進(jìn)去的,反復(fù)debug了好幾遍,發(fā)現(xiàn)出錯(cuò)的應(yīng)用多了一個(gè)監(jiān)聽(tīng)器BootstrapApplicationListener,而這個(gè)監(jiān)聽(tīng)器的優(yōu)先級(jí)如下,比ConfigFileApplicationListener小多了(越小優(yōu)先級(jí)越高)
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 5;

  1. 現(xiàn)在就剩一個(gè)問(wèn)題了,為啥會(huì)多這么一個(gè)監(jiān)聽(tīng)器呢?這個(gè)類所在的jar包是spring-cloud-context,分析了依賴發(fā)現(xiàn)正常啟動(dòng)的應(yīng)用果然是沒(méi)有這個(gè)依賴的。

結(jié)論

有時(shí)候默認(rèn)的約定會(huì)給開(kāi)發(fā)人員帶來(lái)一定的困擾,就像springcloud默認(rèn)會(huì)去讀bootstrap的配置一樣,而差別僅僅是依賴中是否含有對(duì)應(yīng)的jar包。本篇的問(wèn)題其實(shí)還有一種解決方案,就是直接將application.properties里的配置移動(dòng)到bootstrap.properties里,這樣apollo啟用的配置就能從bootstrap.properties里讀到了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容