配置SpringBoot-應用配置如何被載入

孤星可發表於2018-12-20

原始碼之下,沒有祕密.


接上篇, 探索如何解決保證自定義配置能被載入,並且相容 springboot 預設所有配置.
回答上篇留下的幾個問題

  • 有沒有更優雅的解決此問題的方法?
  • 為何 Spring Boot 的配置檔案載入順序是這般定義?
  • Spring Boot 載入配置是如何實現的?
  • Spring Cloud Config 等配置中心的配置是如何結合到具體應用中的?
  • Spring Cloud Config 等配置中心的配置支援日誌系統配置檔案位置的配置嗎?

先看原始碼

核心程式碼就一行

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
複製程式碼

Spring Boot 2.0.4 版, org.springframework.boot.SpringApplication line: 320

為什麼核心就此一行 在此行程式碼執行之前, environment 不出意外是沒有初始化的(後文解釋). 在此行程式碼執行之後, environment 正式建立完畢. 在此之後繼續載入配置檔案, 有些配置屬性是不生效的, 比如 spring.profiles.active.

深入看看

private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();  // 1
    configureEnvironment(environment, applicationArguments.getSourceArgs()); // 2 
    listeners.environmentPrepared(environment); // 3
    bindToSpringApplication(environment);
    if (this.webApplicationType == WebApplicationType.NONE) {
        environment = new EnvironmentConverter(getClassLoader())
                .convertToStandardEnvironmentIfNecessary(environment);
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}
複製程式碼

核心關注點就方法前兩行.

getOrCreateEnvironment

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    if (this.webApplicationType == WebApplicationType.SERVLET) {
        return new StandardServletEnvironment();
    }
    return new StandardEnvironment();
}
複製程式碼

這個方法有點意思, 如果 SpringApplication 例項的 屬性 environment 是有值的話, 返回的就是當前例項的 environment. 這是個切入點, 如果能保證這個 environment 是已經載入了自定義配置檔案的 environment, 自然可以保證 Spring Boot 的所有配置均支援.

configureEnvironment

protected void configureEnvironment(ConfigurableEnvironment environment,
        String[] args) {
    configurePropertySources(environment, args);
    configureProfiles(environment, args);
}
複製程式碼

這是另一個切入點, 關注一下這個方法是被 protectted 修飾的, 可以重寫之. 方法體裡的兩個方法也是 protected 修飾, configurePropertySources(environment, args) 這個方法實現了上篇提到的 官方文件載入順序的 第 4-15 步驟, 細節不談. 可以複寫以支援自定義的配置檔案載入.

再看看第三行

listeners.environmentPrepared(environment)
複製程式碼

眾所周知, Spring 擴充套件點尤其多, Spring Boot 也不例外.
這個 listeners 是什麼東西?

class SpringApplicationRunListeners {
	private final List<SpringApplicationRunListener> listeners;
}
複製程式碼

SpringApplicationRunListener 又是什麼?

public interface SpringApplicationRunListener {

	/**
	 * Called immediately when the run method has first started. Can be used for very
	 * early initialization.
	 */
	void starting();

	/**
	 * Called once the environment has been prepared, but before the
	 * {@link ApplicationContext} has been created.
	 * @param environment the environment
	 */
	void environmentPrepared(ConfigurableEnvironment environment);

	/**
	 * Called once the {@link ApplicationContext} has been created and prepared, but
	 * before sources have been loaded.
	 * @param context the application context
	 */
	void contextPrepared(ConfigurableApplicationContext context);

	/**
	 * Called once the application context has been loaded but before it has been
	 * refreshed.
	 * @param context the application context
	 */
	void contextLoaded(ConfigurableApplicationContext context);

	/**
	 * The context has been refreshed and the application has started but
	 * {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner
	 * ApplicationRunners} have not been called.
	 * @param context the application context.
	 * @since 2.0.0
	 */
	void started(ConfigurableApplicationContext context);

	/**
	 * Called immediately before the run method finishes, when the application context has
	 * been refreshed and all {@link CommandLineRunner CommandLineRunners} and
	 * {@link ApplicationRunner ApplicationRunners} have been called.
	 * @param context the application context.
	 * @since 2.0.0
	 */
	void running(ConfigurableApplicationContext context);

	/**
	 * Called when a failure occurs when running the application.
	 * @param context the application context or {@code null} if a failure occurred before
	 * the context was created
	 * @param exception the failure
	 * @since 2.0.0
	 */
	void failed(ConfigurableApplicationContext context, Throwable exception);
}
複製程式碼

比較明顯了, Spring Boot 啟動生命週期的監聽器. 如果能在 void starting();中初始化 environment 並設定到 SpringApplication environment 屬性, 問題解決.

確定方案

  1. 實現 SpringApplicationRunListener , 在 staring 方法中載入自定義配置到 environment 中.
  2. 複寫 configureEnvironment 方法.

相對來講, 方案 2 成本略高. 採用方案1

落地

方案確定後, 實現很簡單. 核心程式碼

  @Override
  public void starting() {

    ConfigurableEnvironment environment = this.getOrCreateEnvironment();

    Class<?> applicationClass = application.getMainApplicationClass();
    DtConfig annotation = applicationClass.getAnnotation(DtConfig.class);
    if (annotation == null) {
      return;
    }

    String path = annotation.value();

    Resource resource = null;
    if (path.startsWith("classpath")) {
      resource = new ClassPathResource(path.substring(10));
    } else {
      try {
        resource = new FileUrlResource(path);
      } catch (MalformedURLException e) {
        resource = new FileSystemResource(path);
      }
    }

    PropertySource<?> propertySource = loadProperties("Dtconfig", resource, loader);

    environment.getPropertySources().addFirst(propertySource);

    application.setEnvironment(environment);
  }
複製程式碼

詳細程式碼, 傳送門 sb-config

問題解答

  • 有沒有更優雅的解決此問題的方法?
    有, 如上.
  • 為何 Spring Boot 的配置檔案載入順序是這般定義?
    暫且跳過, 沒什麼好說的, 約定優於配置, 配置分層級, 越靠近使用者端, 配置的優先順序越高.
  • Spring Boot 載入配置是如何實現的?
    參閱 configurePropertySources 方法, 過於細節.
  • Spring Cloud Config 等配置中心的配置是如何結合到具體應用中的?
    • Spring Cloud Config, Spring Boot 的自動配置
    • apollo, Spring Boot 的自動配置
  • Spring Cloud Config 等配置中心的配置支援日誌系統配置檔案位置的配置嗎?
    不支援, 幾乎可以斷言, 如果配置中心沒有采用上文的實現方式, 這個問題是解決不了的.

REF

相關文章