Spring Boot 啟動(二) Environment 加載


Spring Boot 啟動(二) Environment 加載

Spring 系列目錄(https://www.cnblogs.com/binarylei/p/10198698.html)

上一節中講解了 SpringApplication 啟動的整個流程,本節關注第二步 prepareEnvironment,尤其是配置文件的加載。

  1. Spring Boot 配置使用
  2. Spring Boot 配置文件加載流程分析 - ConfigFileApplicationListener

spring-boot-config-load

一、prepareEnvironment 加載流程分析

public ConfigurableApplicationContext run(String... args) {
	// 1. listeners 用戶監聽容器的運行,默認實現為 EventPublishingRunListener
	SpringApplicationRunListeners listeners = getRunListeners(args);
	ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

	// 2. 初始化環境變量 environment
	ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
}

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments) {
	// 1. 根據 webApplicationType 創建相應的 Environment
	ConfigurableEnvironment environment = getOrCreateEnvironment();
	// 2. 配置 Environment,主要有三點:一是 ConversionService;二是數據源,包括命令行參數;三是 Profiles
	configureEnvironment(environment, applicationArguments.getSourceArgs());
	// 3. 激活 environmentPrepared 事件,主要是加載 application.yml 等配置文件
	//    ConfigFileApplicationListener#ApplicationEnvironmentPreparedEvent
	listeners.environmentPrepared(environment);
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverter(getClassLoader())
				.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
	}
	// ??? 以后再研究
	ConfigurationPropertySources.attach(environment);
	return environment;
}
  1. 根據 webApplicationType 類型創建相應的 Environment,分為 StandardEnvironment、StandardServletEnvironment、StandardReactiveWebEnvironment。

  2. configureEnvironment 主要有三點:一是 ConversionService;二是數據源,包括命令行參數;三是 Profiles

  3. 激活 environmentPrepared 事件,主要是加載 application.yml 等配置文件

2.1 getOrCreateEnvironment

對於 StandardServletEnvironment 的 servletContextInitParams 和 servletConfigInitParams 兩個 web 的數據源,會先用 StubPropertySource 占位,等初始化 web 容器時再替換。詳見:https://www.cnblogs.com/binarylei/p/10291323.html

2.2 configureEnvironment

protected void configureEnvironment(ConfigurableEnvironment environment,
		String[] args) {
	// 1. 設置 ConversionService
	if (this.addConversionService) {
		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
		environment.setConversionService((ConfigurableConversionService) conversionService);
	}
	// 2. 加載 defaultProperties 和 CommandLinePropertySource(main 參數) 信息
	configurePropertySources(environment, args);
	// 3. 設置 environment 的 Profiles(additionalProfiles + spring.profile.active/default)
	configureProfiles(environment, args);
}
  1. configurePropertySources 添加在 Spring Framework 基礎上添加了兩個新的據源,一是自定義的 defaultProperties;二是 CommandLinePropertySource(main 參數)

  2. configureProfiles 在原有的剖面上添加自定義的剖面 additionalProfiles,注意 additionalProfiles 在前,Spring Framework 默認的剖面在后。

2.3 environmentPrepared

listeners.environmentPrepared(environment) 主要是加載配置文件,其中 listeners 是通過 spring.factories 配置的 SpringApplicationRunListener,默認實現是 EventPublishingRunListener。

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
	this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
			this.application, this.args, environment));
}

environmentPrepared 觸發了 ApplicationEnvironmentPreparedEvent 事件,這個事件是在 spring.factories 配置的監聽器 ConfigFileApplicationListener 處理的。

二、ConfigFileApplicationListener

2.1 ConfigFileApplicationListener 處理流程

public class ConfigFileApplicationListener
		implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
				@Override
	public void onApplicationEvent(ApplicationEvent event) {
		// 1. Environment 加載完成觸發 ApplicationEnvironmentPreparedEvent
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		// 2. ApplicationContext 加載完成觸發 ApplicationPreparedEvent
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
}

本例中觸發了 ApplicationEnvironmentPreparedEvent 事件。

private void onApplicationEnvironmentPreparedEvent(
		ApplicationEnvironmentPreparedEvent event) {
	// 1. 委托給 EnvironmentPostProcessor 處理,也是通過 spring.factories 配置
	List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
	// 2. ConfigFileApplicationListener 本身也實現了 EnvironmentPostProcessor 接口
	postProcessors.add(this);
	// 3. spring 都都通過 AnnotationAwareOrderComparator 控制執行順序
	AnnotationAwareOrderComparator.sort(postProcessors);
	// 4. 執行 EnvironmentPostProcessor
	for (EnvironmentPostProcessor postProcessor : postProcessors) {
		postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
	}
}

在 spring.factories 配置文件中默認定義了三個 EnvironmentPostProcessor 的實現類:

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

優先級 SystemEnvironmentPropertySourceEnvironmentPostProcessor > SpringApplicationJsonEnvironmentPostProcessor

  • SystemEnvironmentPropertySourceEnvironmentPostProcessor 對 systemEnvironment 屬性進行了包裝。
  • SpringApplicationJsonEnvironmentPostProcessor 解析 spring.application.json 或 SPRING_APPLICATION_JSON 配置的 json 字符串。

同時 ConfigFileApplicationListener 也實現了 EnvironmentPostProcessor 接口。我們重點關注的是 ConfigFileApplicationListener 是如何加載配置文件的,其它的 EnvironmentPostProcessor 暫時忽略。跟蹤 ConfigFileApplicationListener#postProcessEnvironment 方法,最終加載配置文件委托給了其內部類 Loader 完成。

protected void addPropertySources(ConfigurableEnvironment environment,
		ResourceLoader resourceLoader) {
	// 1. 加載隨機數據源 ${random.int} ${random.long} ${random.uuid}
	RandomValuePropertySource.addToEnvironment(environment);
	// 2. 加載配置文件
	new Loader(environment, resourceLoader).load();
}

三、Loader 加載配置文件

3.1 Spring Boot 默認目錄及配置文件名

Spring Boot 默認的配置文件的目錄及配置文件名稱如下:

// 1. 配置文件默認的目錄,解析時會倒置,所以 Spring Boot 默認 jar 包的配置文件會覆蓋 jar 中的配置文件
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

// 2. 配置文件默認的文件名
private static final String DEFAULT_NAMES = "application";
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

3.2 profiles 解析配置文件的順序

先了解一起 Spring FrameWork 和 Spring Boot 的 profiles 的概念。

配置 Spring 說明
spring.profiles.active Spring FrameWork AbstractEnvironment 激活的剖面
spring.profiles.default Spring FrameWork AbstractEnvironment 默認剖面
additionalProfiles Spring Boot SpringApplication 自定義激活的剖面
spring.profiles.include Spring Boot ConfigFileApplicationListener 自定義激活的剖面

在啟動 SpringApplication#prepareEnvironment 時已經激活了 additionalProfiles + Spring FrameWork 剖面,注意剖面的順序。 ConfigFileApplicationListener 引入 spring.profiles.include

private void initializeProfiles() {
	// 1. null
	this.profiles.add(null);
	// spring.profiles.include + spring.profiles.active 配置的剖面
	Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
	// 2. environment.getActiveProfiles() 過濾 activatedViaProperty 之后的剖面
	//    目前看只有 SpringApplication 配置的 additionalProfiles
	this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
	// 3. spring.profiles.include + spring.profiles.active
	//    addActiveProfiles 方法只能調用一次,前提是 activatedViaProperty 不為空
	addActiveProfiles(activatedViaProperty);
	// 4. spring.profiles.default
	if (this.profiles.size() == 1) {
		for (String defaultProfileName : this.environment.getDefaultProfiles()) {
			Profile defaultProfile = new Profile(defaultProfileName, true);
			this.profiles.add(defaultProfile);
		}
	}
}

Spring Boot 配置文件 Profiles(application-dev.properties) 的解析順序如下:

  1. 首先解析 null,也就是 application.properties 或 application.yml 文件
  2. spring.profiles.include/active 屬性配置之外的剖面先解析,一般是 activatedViaProperty 或其它編程式配置的 Profiles
  3. spring.profiles.include 定義的剖面,第三和第四的順序在 getProfilesActivatedViaProperty 中定義
  4. spring.profiles.active 定義的剖面
  5. spring.profiles.default 如果沒有激活的剖面,默認 default,即沒有 2、3、4 項

注意:實際讀取配置文件的順序和解析的相反,下面會詳細說明。

還有一種情況是在配置文件 application.properties 中定義了 spring.profiles.include/active 屬性的情況。加載到配置文件后需要判斷是否定義了以上兩個屬性,如果定義了,也需要加載該剖面對應的配置文件。

private void load(PropertySourceLoader loader, String location, Profile profile,
		DocumentFilter filter, DocumentConsumer consumer) {
	// 省略...
	List<Document> loaded = new ArrayList<>();
	for (Document document : documents) {
		if (filter.match(document)) {
			// 1. spring.profiles.active,如果已經定義了該方法就不會再執行了
			addActiveProfiles(document.getActiveProfiles());
			// 2. spring.profiles.include
			addIncludedProfiles(document.getIncludeProfiles());
			loaded.add(document);
		}
	}
}

// 毫無疑問,如果配置文件中定義了 spring.profiles.include 則需要先解析這些剖面,再解析其余的剖面
private void addIncludedProfiles(Set<Profile> includeProfiles) {
	LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
	this.profiles.clear();
	// 1. 先解析配置文件中定義的 spring.profiles.include,當然如果已經解析了則需要排除
	this.profiles.addAll(includeProfiles);
	this.profiles.removeAll(this.processedProfiles);
	// 2. 再解析剩余的剖面
	this.profiles.addAll(existingProfiles);
}

總結,(1) 剖面最終的讀取順序如下:

  1. spring.profiles.active 配置的剖面
  2. spring.profiles.include 配置的剖面
  3. 編程式配置的剖面,如 SpringApplicaiton#etAdditionalProfiles 或 environment#addActiveProfile
  4. 如果未定義激活的剖面,則 spring.profiles.default
  5. 默認的配置文件,如 application.properties
  6. 如果 1-5 項定義了多個,則后面定義的剖面覆蓋前面的剖面,如 spring.profiles.active=dev,test 則 test 覆蓋 dev

(2) 文件名定義的讀取順序如下:

  1. spring.config.name 定義了配置文件名,默認為 applicaiton,可以定義多個,如果有多個則后面的覆蓋前面的

(3) 目錄定義的讀取順序如下:

  1. spring.config.location 定義配置文件所在目錄,默認為 classpath:/,classpath:/config/,file:./,file:./config/也就是后面的覆蓋前面的配置,也就是 jar 包外的配置覆蓋 jar 包內的配置。注意 spring.config.location 如果指定了文件名則 spring.config.name 不會生效。

  2. spring.config.additional-location 上面的配置會覆蓋 Spring Boot 的默認配置目錄,而本配置項則是在默認配置項上追加,先讀取 spring.config.additional-location 再讀取默認的目錄。當然如果顯示的定義了 spring.config.location 就只會讀取這一項。

3.3 配置文件解析

public void load() {
	// 所有的要解析的 profiles,注意讀取配置文件的時候可以會增加
	// 因為配置文件中可能又定義了 spring.profiles.include 屬性
	this.profiles = new LinkedList<>();
	// 已經解析過的 profiles,可以避免循環解析
	this.processedProfiles = new LinkedList<>();
	this.activatedProfiles = false;
	this.loaded = new LinkedHashMap<>();
	// 1. this.profiles 集合定義了 profile 解析順序
	initializeProfiles();
	while (!this.profiles.isEmpty()) {
		Profile profile = this.profiles.poll();
		if (profile != null && !profile.isDefaultProfile()) {
			addProfileToEnvironment(profile.getName());
		}
		// 2. 具體解析配置文件到 this.loaded 中
		load(profile, this::getPositiveProfileFilter,
				addToLoaded(MutablePropertySources::addLast, false));
		this.processedProfiles.add(profile);
	}
	// 3. 解析后 environment#getActiveProfles 可能和配置文件的順序 processedProfiles 不一致
	resetEnvironmentProfiles(this.processedProfiles);
	// 4. 默認的配置文件中定義了剖面,則要看這個配置文件定義的剖面是否激活
	//    即 application.properties 定義了 spring.profile=dev,dev 如果被激活則加載
	load(null, this::getNegativeProfileFilter,
			addToLoaded(MutablePropertySources::addFirst, true));
	// 5. 加載配置文件到 environment 中,注意讀取配置文件的順序和解析的相反
	addLoadedPropertySources();
}
  1. initializeProfiles 加載所有的剖面,解析時會按上面提到的順序進行解析
  2. load 具體解析配置文件到 this.loaded 中
  3. addLoadedPropertySources 加載配置文件到 environment 中,注意讀取配置文件的順序和解析的相反

配置文件屬性那一種剖面有三種定義方式:

  1. 文件名指定:application-dev.properties 屬於 dev 剖面
  2. 文件名為 application.properties 但文件配置了 spring.profile=dev 屬性也屬於 dev 剖面
  3. 以上兩種都指定了,即文件名為 application-dev.properties 的同時文件配置屬性 spring.profile=dev

Spring Boot 針對以上三種情況均有支持。load 方法加載配置文件,最終調用 loadForFileExtension 方法。

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);
	// 1. application-dev.properties
	if (profile != null) {
		String profileSpecificFile = prefix + "-" + profile + fileExtension;
		// application-dev.properties 這二個 load 最多只可能有一個生效 (gh-340)
		load(loader, profileSpecificFile, profile, defaultFilter, consumer);
		// application-dev.properties && spring.profile=dev
		load(loader, profileSpecificFile, profile, profileFilter, consumer);
		// Try profile specific sections in files we've already processed
		for (Profile processedProfile : this.processedProfiles) {
			if (processedProfile != null) {
				String previouslyLoaded = prefix + "-" + processedProfile
						+ fileExtension;
				load(loader, previouslyLoaded, profile, profileFilter, consumer);
			}
		}
	}
	// 2. application.properties && spring.profile=dev
	load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

DocumentFilter 判斷文件中是否定義了 spring.profile 的剖面

private DocumentFilter getPositiveProfileFilter(Profile profile) {
	return (Document document) -> {
		// profile==null 則文件中不能定義 spring.profile
		if (profile == null) {
			return ObjectUtils.isEmpty(document.getProfiles());
		}
		// profile!=null 則配置文件中定義的 spring.profile 包含該 profile
		// 且該配置文件定義的 spring.profile 被激活了
		return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
				&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
	};
}

另外這里的 PropertySourceLoader 也是通過 spring.factories 定義的,默認為 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩種。


每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM