原始碼之下,沒有祕密.
接上篇, 探索如何解決保證自定義配置能被載入,並且相容 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 屬性, 問題解決.
確定方案
- 實現 SpringApplicationRunListener , 在 staring 方法中載入自定義配置到 environment 中.
- 複寫 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 等配置中心的配置支援日誌系統配置檔案位置的配置嗎?
不支援, 幾乎可以斷言, 如果配置中心沒有采用上文的實現方式, 這個問題是解決不了的.