SpringBoot2 Binder的使用
Binder的使用其實比較簡單 有點類似註解ConfigurationProperties的作用,都是將屬性綁定到某個具體的對象上。 但是有一點區別 ConfigurationProperties是在容器啓動時綁定的,而Binder是我們手動編碼動態的綁定上去的。
我們回顧上一節 在向容器發送ApplicationEnvironmentPreparedEvent事件之後還執行了一行代碼 bindToSpringApplication(environment) 下面我們看一下這行代碼的具體作用
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
展開bindToSpringApplication方法
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
以上代碼是將spring.main下面的配置綁定到SpringApplication對象上。如:sources ,bannerMode等屬性賦值給當前的對象。也就是將spring.main.sources 綁定到SpringbootApplication的sources屬性上 將spring.main.banner-mode綁定到bannerMode屬性上。可以理解爲將屬性動態綁定到對象上。
我們再看一處Springboot中動態綁定的代碼
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
if (loaded == null) {
return Collections.emptyList();
}
return loaded.stream().map((propertySource) -> {
Binder binder = new Binder(ConfigurationPropertySources.from(propertySource),
this.placeholdersResolver);
return new Document(propertySource, binder.bind("spring.profiles", STRING_ARRAY).orElse(null),
getProfiles(binder, ACTIVE_PROFILES_PROPERTY), getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
}).collect(Collectors.toList());
}
這段代碼也是在上一篇分析的ConfigFileApplicationListener中。將YamlPropertySourceLoader解析的List<PropertySource<?>> 包裝成Document的過程。
代碼的作用是將spring.profiles下面的配置解析成字符串數組 賦值給Document的profiles屬性的過程。
分塊配置
springboot 官網文檔解釋:You can specify multiple profile-specific YAML documents in a single file by using a spring.profiles key to indicate when the document applies
大概的意思是 可以使用 spring.profiles 的key在單個文件中指定多個特定 profile 的 YAML 文檔,以指示文檔何時應用
我們用一個demo演示一下
#模塊一
server:
add: 192.168.1.100
spring:
profiles:
active:
- production
- eu-central
---
#模塊二
spring:
profiles: development
server:
add: 127.0.0.1
---
#模塊三
spring:
profiles: production & eu-central
server:
add: 192.168.1.120
在一個application.yml文件中 用 — 符號隔離每個模塊 可以爲每個模塊設置加載條件。例如模塊二的加載條件是當development被激活時 server.add纔有效。模塊三的激活條件是 production和eu-central同時被激活時纔會輸出192.168.1.120
我們運行程序輸出server.add 這個時候輸出的是192.168.1.120。假如我們註釋掉模塊一的spring.profiles.active 則輸出 192.168.1.100
再看SpringBoot解析配置塊的過程
上一篇文章我們只是分析了加載配置文件的流程,至於後面怎麼去加載主配置文件裏面配置的spring.profiles.active 和spring.profiles.include只是簡單的帶過。下面接着上一節的內容分析
在分析之前我們先看一下 配置文件的配置
#application.yml文件
server:
add: 192.168.1.100
spring:
profiles:
active: development
---
spring:
profiles: development
server:
add: 127.0.0.1
name:
test: 11111
---
spring:
profiles: production & eu-central
server:
add: 192.168.1.120
#application-development.yml 文件
name:
test: 2222
spring:
profiles:
active: test
#application-test.yml 文件
name:
test: 3333
以上有三個文件 application.yml 中採用了分塊配置 並且指定了spring.profiles.active=development
在application-development.yml文件中指定了spring.profiles.active=test
1,加載application.yml
直接進入解析配置文件的方法 同樣刪掉了參數判斷的代碼
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,DocumentConsumer consumer) {
Resource resource = this.resourceLoader.getResource(location);
String name = "applicationConfig: [" + location + "]";
//這裏第一次加載application.yml文件 圖1 處是解析成document之後的值。這裏解析出三個document。 因爲我們是分塊配置
List<Document> documents = loadDocuments(loader, name, resource);
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
//將spring.profiles.active配置的內容加入到profiles 用於以後解析
//這裏有一個條件 當加入完從成之後會將 activatedProfiles屬性改爲true 第二次加入的時候會先判斷activatedProfiles值 如果爲true 不會再加入。
//所以到這 我們知道application-development中配置的 spring.profiles.active=test就不會被加入 所以文件application-test.yml不會被解析
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
}
圖1:
上面會對解析的三個文檔做一個過濾 繼續跟進filter的match方法
//加載application.yml時 當前的profile是null 所以第一次加載只會進入第一次if分支中
private DocumentFilter getPositiveProfileFilter(Profile profile) {
return (Document document) -> {
if (profile == null) {
//這裏判斷如果document的profiles爲null 纔會執行將解析的內容加入loaded緩存中
return ObjectUtils.isEmpty(document.getProfiles());
}
return ObjectUtils.containsElement(document.getProfiles(), profile.getName())&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
};
}
所以根據上述的邏輯 application.yml文件中的三個模塊 第一次只會加載第一個模塊裏面的內容。並將development加入到profiles中。等待下次while循環解析。
2,加載active配置文件
public void load() {
....
while (!this.profiles.isEmpty()) {
//這一次取出的是 上一次解析application.yml的spring.profiles.active屬性配置的development
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
//將development加入Environment的activeProfiles屬性中
addProfileToEnvironment(profile.getName());
}
//繼續加載application-development.yml配置文件
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
....
}
我們看到上面和加載application.yml的流程是一樣的。下面直接進入到 具體的加載配置的方法。
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
//上一節我們提到 如果profile不爲空是直接進入到if分支裏面的
if (profile != null) {
//如果profile不爲空 拼接 文件名字
String profileSpecificFile = prefix + "-" + profile + fileExtension;
//加載內容 注意這裏傳的filter是filterFactory.getDocumentFilter(null)獲取的filter
//所以這裏還是會和解析application.yml的流程一樣。會把application-development.yml解析結果放在loaded中
//如果application-development.yml是分塊配置 會將默認(沒有配置spring.profiles值的塊)的塊加入loaded中
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
//這裏再次解析的目的是如果 application-development.yml是分塊配置 會把 spring.profiles=development的塊配置的內容解析加載到loaded中
load(loader, profileSpecificFile, profile, profileFilter, consumer);
//這裏的 this.processedProfiles是我們處理過的集合
for (Profile processedProfile : this.processedProfiles) {
//過濾掉profile = null的 也就是 application.yml
if (processedProfile != null) {
//和上面過程一樣 重新拼接 文件名字
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
//再一次加載之前加載過的 配置文件 請注意這裏傳的是一個profileFilter 而不是默認的filter
// 從剛在我們的分析知道默認的 實際上默認的filter本質上是加載默認塊的內容 而profileFilter是加載指定塊的內容
//哪這裏爲什麼又一次遍歷加載呢?其實原因也很簡單 假如我麼的application.yaml 中 spring.profiles.active指定了2個值 development,test
//而在development的配置文件中指定了 test模塊 所以這裏要把之前沒有加載的test模塊加載到loaded中
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
//查找主配置文件(application.yml)裏面的 當前激活的配置塊
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
上面的代碼主要是兼容 配置文件中的配置塊的配置。加載過程比較複雜。這裏我們整理一下
假設我們有三個配置文件:
application.yml 內容
server:
add: 192.168.1.100
spring:
profiles:
active: development,test
---
spring:
profiles: development
server:
add: 127.0.0.1
---
spring:
profiles: test
server:
add: 192.168.1.120
application.yml 有三個配置塊 其中在主配置塊中指定了 激活的配置塊爲development和test
application-development.yml配置文件內容
server:
add: 192.168.2.100
---
spring:
profiles: test
server:
add: 192.168.2.300
develpment配置文件有兩個模塊 一個是主模塊 一個是test模塊
application-test.yml的配置文件內容
server:
add: 192.168.3.100
---
spring:
profiles: development
server:
add: 192.168.3.200
test文件中有兩個模塊 一個是主模塊一個是 development模塊
當啓動程序加載以上三個配置文件的時候 會執行以下的順序:
- 加載application.yml文件的主模塊的內容
- 將application.yml中的spring.profiles.active 放入待解析的集合中
- 加載application-development.yml文件並解析主模塊內容
- 解析application-development.yml文件的development模塊內容
- 解析application.yml中development模塊的內容
- 加載application-test.yml文件 並解析主模塊內容
- 解析application-test.yml文件中的test模塊內容
- 解析application-development.yml文件中test模塊中的內容
- 解析application.yml文件中test模塊中的內容
從以上的加載規則可以看到 如配置的spring.profiles.active=development,test 那麼test文件中的development模塊是無法被加載的。
配置文件優先級
所謂配置文件優先級 其實是指配置文件的在MutablePropertySources中的順序。下標小的會被提前遍歷 如果條件匹配 提前返回 所以就沒有後面的配置什麼事了。我們首先看一下加載配置文件是怎麼被添加到Environment的容器MutablePropertySources中的。
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
//將解析的LinkedhashMap 的value轉成list 注意這裏是有序的LinkedHashMap 而不是hashmap
//所以根據我們上面的解析邏輯 application.yml最先被解析 放在第一個development再次被解析放在第二個 test放在最後
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
//這裏對list做了一個位置翻轉。也就是 現在的順序變成 test development application.yml
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
//這裏遍歷上面經過翻轉的集合 一個個添加到Environment 我們看一下下面的代碼是怎麼添加的
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
//添加邏輯
private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded,PropertySource<?> source) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
//第一次進入這裏 吧test放在最後面
destination.addLast(source);
}
}
else {
//後面會進入這裏 因爲lastAdded有值了 lastAdded保存的是上一次添加元素的值 這一個操作會把
//當前的元素放在上一個添加元素的後面。詳細操作可以看 下面的源碼
destination.addAfter(lastAdded, source);
}
}
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
removeIfPresent(propertySource);
//找到上一次添加元素的位置
int index = assertPresentAndGetIndex(relativePropertySourceName);
//直接將元素放到index+1的位置
addAtIndex(index + 1, propertySource);
}
從上面我們瞭解到application.yml的優先級對於我們配置來說是最低。然後就是我們配置的
spring.profiles.active 如果有多個值 越靠後優先級越高。
下面我們附一張圖 所有配置文件在Environment中的順序。
上圖可以看到 除了我們的自定義配置文件。其他配置項的優先級分別是:
commandLine > servletConfigInitParams > servletContextInitParams > systemProperties> systemEnvironment > random > 自定義配置文件