SpringBoot源碼系列:Environment機制深入分析(二)

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模塊

當啓動程序加載以上三個配置文件的時候 會執行以下的順序:

  1. 加載application.yml文件的主模塊的內容
  2. 將application.yml中的spring.profiles.active 放入待解析的集合中
  3. 加載application-development.yml文件並解析主模塊內容
  4. 解析application-development.yml文件的development模塊內容
  5. 解析application.yml中development模塊的內容
  6. 加載application-test.yml文件 並解析主模塊內容
  7. 解析application-test.yml文件中的test模塊內容
  8. 解析application-development.yml文件中test模塊中的內容
  9. 解析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 > 自定義配置文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章