上一篇從使用角度介紹瞭如何在 springboot 啓動時對 yml 文件進行配置,這一篇嘗試從源碼角度去探討其加載配置文件的機制。spring
咱們回到 springboot 初始啓動時的 run 方法,爲了突出主幹邏輯,對代碼作了一些精簡:sql
public ConfigurableApplicationContext run(String... args) {
//省略其餘代碼
...
Collection<SpringApplicationRunListener> runListeners = getRunListeners(args);
for (SpringApplicationRunListener runListener : runListeners) {
runListener.started();
}
try {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, args);
//廣播 environment 已準備事件到各個監聽器
for (SpringApplicationRunListener runListener : runListeners) {
runListener.environmentPrepared(environment);
}
// Create, load, refresh and run the ApplicationContext
context = createApplicationContext();
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
for (SpringApplicationRunListener runListener : runListeners) {
runListener.contextPrepared(context);
}
// Load the sources
Set<Object> sources = getSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[sources.size()]));
for (SpringApplicationRunListener runListener : runListeners) {
runListener.contextLoaded(context);
}
// Refresh the context
refresh(context);
afterRefresh(context, args);
for (SpringApplicationRunListener runListener : runListeners) {
runListener.finished(context, null);
}
return context;
}
catch (Throwable ex) {
//省略
....
}
複製代碼
對於 spring run listener 如何經過事件類型找到監聽器,前文已有敘述。此處從一個監聽器開始提及。springboot
正是這個監聽器監聽到 environmentPrepared event後,開始加載配置文件邏輯。bash
@Override
public void onApplicationEvent(ApplicationEvent event) {
//判斷類型,執行邏輯
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
}
複製代碼
跟着代碼走,進入下面的方法:服務器
private void onApplicationEnvironmentPreparedEvent(
ConfigurableEnvironment environment, SpringApplication application) {
//加載全部的配置屬性到 environment
addPropertySources(environment, application.getResourceLoader());
//將 environment 綁定到 springApplication
bindToSpringApplication(environment, application);
}
複製代碼
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
try {
//核心方法,使用 resourceLoader 從 environment 中加載屬性
new Loader(environment, resourceLoader).load();
} catch (IOException ex) {
throw new IllegalStateException("Unable to load configuration files", ex);
}
}
複製代碼
load 方法代碼以下:app
public void load() throws IOException {
//初始化屬性加載器,用於後面的屬性加載工做
this.propertiesLoader = new PropertySourcesLoader();
this.activatedProfiles = false;
//初始化 profiles 集合,包裝成爲一個 LIFO(後進先出)隊列,爲了實現後面覆蓋前面的特性
this.profiles = Collections.asLifoQueue(new LinkedList<String>());
//初始化 active profile 集合
Set<String> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
this.profiles.add(null);
//取出來全部的profile,依次load,若是後次加載的屬性會覆蓋上次加載的屬性值
while (!this.profiles.isEmpty()) {
String profile = this.profiles.poll();
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
// location is a filename already, so don't search for more // filenames load(location, null, profile); } else { for (String name : getSearchNames()) { load(location, name, profile); } } } } //綁定屬性到 environment 中 addConfigurationProperties(this.propertiesLoader.getPropertySources()); } 複製代碼
profile 用於區別不一樣環境下的配置,好比如今有兩套環境:生產環境和測試環境,若是有兩個配置文件對於同一個屬性,進行了不一樣的配置(好比服務器port),那我只須要針對兩套環境寫兩個配置文件,只須要更改 profile 的值便可讓應用自動選擇加載哪一個配置文件。dom
這裏經過使用一個 *Collections.asLifoQueue(new LinkedList());*方法構建了一個 LIFO(後進先出)的隊列,相似棧的性質。目的就是爲了後面的 profile 具備更高的優先級。好比指定了ide
spring.profiles.active=dev,hsqldb工具
則對於一樣一個屬性的配置,hsqldb 的屬性值會覆蓋 dev 的屬性值。post
對於 profile 的配置分三步進行:
Set<String> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
this.profiles.add(null)
複製代碼
第一步:初始化 active profile 的集合。
private Set<String> initializeActiveProfiles() {
//ACTIVE_PROFILES_PROPERTY = "spring.profiles.active"
//若是環境中沒有該屬性,則返回一個空集
if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
return Collections.emptySet();
}
//Property source(好比系統屬性)設置的 profile 優先級高於配置文件中的配置
Set<String> activeProfiles = getProfilesForValue(
this.environment.getProperty(ACTIVE_PROFILES_PROPERTY));
maybeActivateProfiles(activeProfiles);
return activeProfiles;
}
複製代碼
咱們進去 getProfilesForValue 往方法裏面一直走:
List<String> list = Arrays
.asList(StringUtils.commaDelimitedListToStringArray(value != null
? this.environment.resolvePlaceholders(value) : fallback));
Collections.reverse(list);
return new LinkedHashSet<String>(list);
複製代碼
此段代碼總共作了三件事:
獲取到 profile集合以後,按順序入隊。
private void addProfiles(Set<String> profiles) {
for (String profile : profiles) {
this.profiles.add(profile);
if (!this.environment.acceptsProfiles(profile)) {
// If it's already accepted we assume the order was set // intentionally prependProfile(this.environment, profile); } } } 複製代碼
第二步:拿到命令行配置的 profile
在初始化 profile 完畢以後,又經過下面的一行代碼對一些 profile 進行了合併。
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
複製代碼
咱們能夠繼續看下併入的profile都是什麼?
private List<String> getUnprocessedActiveProfiles(
Set<String> initialActiveProfiles) {
List<String> unprocessedActiveProfiles = new ArrayList<String>();
for (String profile : this.environment.getActiveProfiles()) {
if (!initialActiveProfiles.contains(profile)) {
unprocessedActiveProfiles.add(profile);
}
}
// 繼續反轉,邏輯同上
Collections.reverse(unprocessedActiveProfiles);
return unprocessedActiveProfiles;
}
複製代碼
這裏處理了一些經過其餘方式設置的 profile,因爲前面經過ACTIVE_PROFILES_PROPERTY屬性設置的profile擁有更高的優先級,因此此處繼續從後面入隊。
第三步:添加默認的profile
this.profiles.add(null);
複製代碼
默認的 profile 爲null,優先級最低,只要在 queue 前面有元素便可被覆蓋,因此此時加入 null,是爲了後續處理統一。
到此,通過三步操做,咱們已經按照優先級順序拿到了全部的 active 的 profile。
接下來就是出隊,按照優先級順序依次加載各個 profile 相應的屬性,若是屬性相同,則後面出隊的覆蓋前面的,與優先級特性相對應。
while (!this.profiles.isEmpty()) {
String profile = this.profiles.poll();
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
// location is a filename already, so don't search for more // filenames load(location, null, profile); } else { for (String name : getSearchNames()) { load(location, name, profile); } } } } 複製代碼
上面就是依次加載的主要邏輯。
從上面的代碼中能夠看到,依次出隊取到 profile 屬性後,經過一個 getSearchLocations 方法拿到全部應該加載 property 的位置,依次進行加載。
private Set<String> getSearchLocations() {
Set<String> locations = new LinkedHashSet<String>();
// CONFIG_LOCATION_PROPERTY = "spring.config.location"
// 用戶配置優先,因此先處理配置
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
for (String path : asResolvedSet(
this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
}
locations.add(path);
}
}
// 加載默認配置
// DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}
複製代碼
從上面代碼中能夠看到,若是用戶經過 spring.config.location 參數對加載文件的路徑進行了配置,則會首先將該路徑加入到 set 集合中(一樣也能夠保證順序)。以後纔會加載默認的路徑,而默認路徑即如前文所介紹的。
在拿到路徑後,先判斷是不是目錄,若是 location 不以 「/」結尾,則認爲已是一個文件名,直接加載;反之,則要在該路徑下搜索到全部知足條件的配置文件。
// Search for a file with the given name
for (String ext : this.propertiesLoader.getAllFileExtensions()) {
if (profile != null) {
// Try the profile specific file
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
null);
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
profile);
}
loadIntoGroup(group, location + name + "." + ext, profile);
}
複製代碼
上面代碼對符合目錄下全部的合法的配置文件進行了搜索。
合法文件名的樣式以下:
location + name + "-" + profile + "." + ext
好比 application-dev.yml
for循環裏面對於全部支持的文件擴展名進行了遍歷,咱們能夠看看支持的文件擴展名有哪些:
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
複製代碼
這是負責加載 yml 文件的 YamlPropertySourceLoader 返回的符合條件的文件擴展名。
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
複製代碼
這是負責加載 property 文件的 PropertiesPropertySourceLoader 返回的符合條件的文件擴展名。
這兩個加載器都是 springboot 自帶的加載器,固然也能夠自定義加載屬性類,便可加載指定擴展名的文件。
接下來重點看下實際進行加載的 loadIntoGroup 方法:
這裏 group 的名字爲「profile=dev 」樣式。
private PropertySource<?> loadIntoGroup(String identifier, String location,
String profile) throws IOException {
Resource resource = this.resourceLoader.getResource(location);
PropertySource<?> propertySource = null;
if (resource != null) {
String name = "applicationConfig: [" + location + "]";
String group = "applicationConfig: [" + identifier + "]";
propertySource = this.propertiesLoader.load(resource, group, name,
profile);
if (propertySource != null) {
handleProfileProperties(propertySource);
}
}
//省略日誌打印
....
return propertySource;
}
複製代碼
由 yml 加載器或者 property 加載器對文件加載後獲得 propertySource(裏面存放各類key-value變量),具體第三方加載器的加載細節此處暫時不討論。能夠看下 handleProfileProperties 方法的處理邏輯:
private void handleProfileProperties(PropertySource<?> propertySource) {
Set<String> activeProfiles = getProfilesForValue(
propertySource.getProperty(ACTIVE_PROFILES_PROPERTY));
maybeActivateProfiles(activeProfiles);
// INCLUDE_PROFILES_PROPERTY = "spring.profiles.include"
Set<String> includeProfiles = getProfilesForValue(
propertySource.getProperty(INCLUDE_PROFILES_PROPERTY));
addProfiles(includeProfiles);
}
複製代碼
這裏又對 spring.profiles.include 指定的 profile 參數進行了處理。
bindToSpringApplication(environment, application);
複製代碼
對該方法進一步跟進,能夠發現最終進入下面方法:
private void doBindPropertiesToTarget() throws BindException {
RelaxedDataBinder dataBinder = (this.targetName != null
? new RelaxedDataBinder(this.target, this.targetName)
: new RelaxedDataBinder(this.target));
//驗證
if (this.validator != null) {
dataBinder.setValidator(this.validator);
}
if (this.conversionService != null) {
dataBinder.setConversionService(this.conversionService);
}
//設置各類屬性
dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties);
dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields);
dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields);
customizeBinder(dataBinder);
Set<String> names = getNames();
PropertyValues propertyValues = getPropertyValues(names);
//執行真正的綁定
dataBinder.bind(propertyValues);
if (this.validator != null) {
validate(dataBinder);
}
}
複製代碼
dataBinder.bind(propertyValues) 方法執行真正的數據綁定工做。
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
複製代碼
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// 執行綁定
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
//異常處理
...
}
複製代碼
此文對 springboot 加載配置文件的流程進行了一個粗線條的分析,不少細節尚未分析到位,可是主幹邏輯已然分析清楚了,不足之處,留待後面完善了。