博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Spring Boot源码分析-配置文件加载
阅读量:6232 次
发布时间:2019-06-22

本文共 22402 字,大约阅读时间需要 74 分钟。

在中我们进行了启动源码的分析,大致了解了整个Spring Boot的启动过程,具体细节这里不再赘述,感兴趣的同学可以自行阅读。今天让我们继续阅读源码,了解配置文件加载原理。

基于Spring Boot 2.1.0.RELEASE

在开始阅读源码之前,首先准备三个问题。

  1. 什么时候开始加载配置文件?
  2. 如何读取相关配置文件内容?
  3. 如何区分不同环境的配置?

下面用Spring代替Spring Boot

接下来进入主题,首先关注第一个问题。

一、什么时候开始加载配置文件?

从中我们可以得知,Spring在启动的过程中发布了ApplicationEnvironmentPreparedEvent事件,ConfigFileApplicationListener监听到这个消息的时候,开始实例化并调用(META-INF/spring.factories中定义)EnvironmentPostProcessorpostProcessEnvironment方法。而ConfigFileApplicationListener本身也实现了EnvironmentPostProcessor接口,且将自身加入到EnvironmentPostProcessor集合中,故也会调用自身的方法。

跟踪ConfigFileApplicationListenerpostProcessEnvironment方法源码

public void postProcessEnvironmen(ConfigurableEnvironment environment,		SpringApplication application) {	addPropertySources(environment,application.getResourceLoader());}复制代码

继续跟踪addPropertySources方法

/** * Add config file property sources to the specified environment. * @param environment the environment to add source to * @param resourceLoader the resource loader * @see #addPostProcessors(ConfigurableApplicationContext) */protected void addPropertySources(ConfigurableEnvironmentenvironment,		ResourceLoader resourceLoader) {	RandomValuePropertySource.addToEnvironmen(environment);	new Loader(environment, resourceLoader).load();}复制代码

从注释中我们可以看出,这个方法是将配置文件内容添加到指定的Environment中。到此为止,我们已经明白了Spring是在发布ApplicationEnvironmentPreparedEvent事件之后,才开始加载配置文件的。接下来开始关注第二个问题。

二、如何读取相关配置文件内容?

继续跟踪Loader源码,LoaderConfigFileApplicationListener的一个内部类,用来读取配置文件并配置相关环境。

首先跟踪Loader构造方法(注意load存在多个方法重载)

Loader(ConfigurableEnvironment environmentResourceLoader resourceLoader) {	this.environment = environment;	this.placeholdersResolver = nePropertySourcesPlaceholdersResolver(			this.environment);	this.resourceLoader = (resourceLoader != null) resourceLoader			: new DefaultResourceLoader();    // 实例化配置文件读取工具    this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(			PropertySourceLoader.class, getClass.getClassLoader());}复制代码

SpringFactoriesLoader.loadFactories获取META-INF/spring.factories中预定义的类

org.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader复制代码

从类名中可以看出这两个类主要是用来读取.properties.yml文件

继续跟踪load方法

public void load() {	this.profiles = new LinkedList<>();	this.processedProfiles = new LinkedList<>();	this.activatedProfiles = false;	this.loaded = new LinkedHashMap<>();	initializeProfiles();	while (!this.profiles.isEmpty()) {		Profile profile = this.profiles.poll();		if (profile != null &!profile.isDefaultProfile()) {			addProfileToEnvironment(profile.getName(;		}		load(profile, this::getPositiveProfileFilter,				addToLoad(MutablePropertySources::addLastfalse));		this.processedProfiles.add(profile);	}	resetEnvironmentProfiles(this.processedProfiles);	load(null, this::getNegativeProfileFilter,			addToLoad(MutablePropertySources::addFirst, true));	addLoadedPropertySources();}复制代码

继续跟踪initializeProfiles方法

/** * Initialize profile information from both the {
@link Environment} active * profiles and any {
@code spring.profiles.active{
@code spring.profiles.include} * properties that are already set. */private void initializeProfiles() { // The default profile for these purposes irepresented as null. We add it // first so that it is processed first and halowest priority. this.profiles.add(null); Set
activatedViaProperty = getProfilesActivatedViaProperty(); this.profiles.addAll(getOtherActiveProfil(activatedViaProperty)); // Any pre-existing active profiles set viproperty sources (e.g. // System properties) take precedence over thosadded in config files. addActiveProfiles(activatedViaProperty); if (this.profiles.size() == 1) { // only has nulprofile for (String defaultProfileName this.environment.getDefaultProfiles()) { Profile defaultProfile = new Profi(defaultProfileName, true); this.profiles.add(defaultProfile); } }}复制代码

从注释中我们可以了解到这个方法用来初始化profile。继续往下看Spring如何初始化profile。接着跟踪getProfilesActivatedViaProperty方法。

private Set
getProfilesActivatedViaProperty { if (!this.environment.containsProper(ACTIVE_PROFILES_PROPERTY) && !this.environment.containsProper(INCLUDE_PROFILES_PROPERTY)) { return Collections.emptySet(); } Binder binder = Binder.get(this.environment); Set
activeProfiles = new LinkedHashSet(); activeProfiles.addAll(getProfiles(binderINCLUDE_PROFILES_PROPERTY)); activeProfiles.addAll(getProfiles(binderACTIVE_PROFILES_PROPERTY)); return activeProfiles;}复制代码

Environment目前没有读取配置文件,故这里返回一个空集合。继续回到上面的方法,跟踪addActiveProfiles方法

void addActiveProfiles(Set
profiles) { if (profiles.isEmpty()) { return; } if (this.activatedProfiles) { if (this.logger.isDebugEnabled()) { this.logger.debug("Profiles alreadactivated, '" + profiles + "' will not be applied"); } return; } this.profiles.addAll(profiles); if (this.logger.isDebugEnabled()) { this.logger.debug("Activated activeProfiles " StringUtils.collectionToCommaDelimitString(profiles)); } this.activatedProfiles = true; removeUnprocessedDefaultProfiles();}复制代码

上面分析得知profiles是一个空集合,所以这里不会继续往下执行。再回到上面方法。

private void initializeProfiles() {	this.profiles.add(null);	Set
activatedViaProperty getProfilesActivatedViaProperty(); this.profiles.addAll(getOtherActiveProfil(activatedViaProperty)); addActiveProfiles(activatedViaProperty); if (this.profiles.size() == 1) { for (String defaultProfileName : this.environment.getDefaultProfiles()) { Profile defaultProfile = new Profi(defaultProfileName, true); this.profiles.add(defaultProfile); } }}复制代码

因为profiles添加了一个null,所以if条件成立,遍历environment中默认的profile,默认的profile是什么呢?

通过查看AbstractEnvironment源码得知,默认profiledefault

protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";private final Set
defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());protected Set
getReservedDefaultProfiles() { return Collections.singleto(RESERVED_DEFAULT_PROFILE_NAME);}复制代码

继续回到上面方法,往profiles添加了一个default profile,这时候profiles里面已经有了两个元素,nulldefault

接下来回到load方法,关注while循环

public void load() {	this.profiles = new LinkedList<>();	this.processedProfiles = new LinkedList<>();	this.activatedProfiles = false;	this.loaded = new LinkedHashMap<>();	initializeProfiles();	while (!this.profiles.isEmpty()) {		Profile profile = this.profiles.poll();		if (profile != null &!profile.isDefaultProfile()) {			addProfileToEnvironment(profile.getName(;		}		load(profile, this::getPositiveProfileFilter,				addToLoad(MutablePropertySources::addLastfalse));		this.processedProfiles.add(profile);	}	resetEnvironmentProfiles(this.processedProfiles);	load(null, this::getNegativeProfileFilter,			addToLoad(MutablePropertySources::addFirst, true));	addLoadedPropertySources();}复制代码

从上面的分析已经可以知道profiles中的第一个元素实际上是null,所以直接进入load方法

private void load(Profile profileDocumentFilterFactory filterFactory,		DocumentConsumer consumer) {	getSearchLocations().forEach((location) -> {		boolean isFolder = location.endsWith("/");		Set
names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; names.forEach( (name) -> load(location, name, profile, filterFactory, consumer)); });}复制代码

先看看getSearchLocations返回的内容

public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";private Set
getSearchLocations() { if (this.environment.containsProper(CONFIG_LOCATION_PROPERTY)) { return getSearchLocatio(CONFIG_LOCATION_PROPERTY); } Set
locations = getSearchLocations( CONFIG_ADDITIONAL_LOCATION_PROPERTY); locations.addAll( asResolvedSet(ConfigFileApplicationListener.thisearchLocations, DEFAULT_SEARCH_LOCATIONS)); return locations;}复制代码

从上面可知,environment目前还没有读取到配置文件内容,所以不会进入if条件,同理可知Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY)实际上也是一个空集合。

asResolvedSet返回的是DEFAULT_SEARCH_LOCATIONS对应的四个配置文件位置。

回到load方法

private void load(Profile profileDocumentFilterFactory filterFactory,		DocumentConsumer consumer) {	getSearchLocations().forEach((location) -> {		boolean isFolder = location.endsWith("/");		Set
names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; names.forEach( (name) -> load(location, name, profile, filterFactory, consumer)); });}复制代码

这里的isFolder都是true,跟踪getSearchNames方法

private static final String DEFAULT_NAMES = "application";private Set
getSearchNames() { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); return asResolvedSet(property, null); } return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);}复制代码

从这里可以看出来getSearchNames返回的集合只包含一个application。继续跟踪load方法

private void load(String location, String nameProfile profile,		DocumentFilterFactory filterFactoryDocumentConsumer consumer) {	if (!StringUtils.hasText(name)) {		for (PropertySourceLoader loader this.propertySourceLoaders) {			if (canLoadFileExtension(loader, locatio) {				load(loader, location, profile,						filterFactorgetDocumentFilter(profile)consumer);				return;			}		}	}	Set
processed = new HashSet<>(); for (PropertySourceLoader loader this.propertySourceLoaders) { for (String fileExtension loader.getFileExtensions()) { if (processed.add(fileExtension)) { loadForFileExtension(loader, locatio+ name, "." + fileExtension, profile, filterFactoryconsumer); } } }}复制代码

从上面可以得知,nameProfile的值实际上是application,所以直接跟踪下面的for循环。

Loader的构造方法可知,propertySourceLoaders

org.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader复制代码

从类名可知PropertiesPropertySourceLoader解析properties文件,YamlPropertySourceLoader解析yml文件,但是PropertiesPropertySourceLoader还可以解析xml文件。

public String[] getFileExtensions() {	return new String[] { "properties", "xml" };}复制代码

继续跟踪loadForFileExtension

private void loadForFileExtensi(PropertySourceLoader loader, String prefix,		String fileExtension, Profile profile,		DocumentFilterFactory filterFactory, DocumentConsumer consumer) {	DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);	DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);	if (profile != null) {		// Try profile-specific file & profile section in profile file (gh-340)		String profileSpecificFile = prefix + "-" + profile + fileExtension;		load(loader, profileSpecificFile, profile, defaultFilter, consumer);		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);			}		}	}	// Also try the profile-specific section (if any) of the normal file	load(loader, prefix + fileExtension, profile, profileFilter, consumer);}复制代码

从上面的分析可知,当前profilenull,所以继续跟踪load方法

private void load(PropertySourceLoader loader, String location, Profile profile,		DocumentFilter filter, DocumentConsumer consumer) {	try {		Resource resource = this.resourceLoader.getResource(location);		if (resource == null || !resource.exists()) {			if (this.logger.isTraceEnabled()) {				StringBuilder description = getDescription(						"Skipped missing config ", location, resource, profile);				this.logger.trace(description);			}			return;		}		if (!StringUtils.hasText(				StringUtils.getFilenameExtension(resource.getFilename()))) {			if (this.logger.isTraceEnabled()) {				StringBuilder description = getDescription(						"Skipped empty config extension ", location, resource,						profile);				this.logger.trace(description);			}			return;		}		String name = "applicationConfig: [" + location + "]";        // 开始读取文件内容		List
documents = loadDocuments(loader, name, resource); if (CollectionUtils.isEmpty(documents)) { if (this.logger.isTraceEnabled()) { StringBuilder description = getDescription( "Skipped unloaded config ", location, resource, profile); this.logger.trace(description); } return; } List
loaded = new ArrayList<>(); for (Document document : documents) { if (filter.match(document)) { addActiveProfiles(document.getActiveProfiles()); addIncludedProfiles(document.getIncludeProfiles()); loaded.add(document); } } Collections.reverse(loaded); if (!loaded.isEmpty()) { loaded.forEach((document) -> consumer.accept(profile, document)); if (this.logger.isDebugEnabled()) { StringBuilder description = getDescription("Loaded config file ", location, resource, profile); this.logger.debug(description); } } } catch (Exception ex) { throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'", ex); }}复制代码

继续跟踪loadDocuments方法

private List
loadDocuments(PropertySourceLoader loader, String name, Resource resource) throws IOException { DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource); List
documents = this.loadDocumentsCache.get(cacheKey); if (documents == null) { // PropertySource 用来存储配置项 List
> loaded = loader.load(name, resource); documents = asDocuments(loaded); this.loadDocumentsCache.put(cacheKey, documents); } return documents;}复制代码

读取配置的时候首先看是否存在缓存,如果不存在,则调用loader.load方法。通过上面的分析可知loader对象实际上是PropertiesPropertySourceLoaderYamlPropertySourceLoader,我们这里的配置文件是properties文件,所以我们选择跟踪PropertiesPropertySourceLoaderload方法。

public List
> load(String name, Resource resource) throws IOException { // 调用loadProperties方法读取配置文件 Map
properties = loadProperties(resource); if (properties.isEmpty()) { return Collections.emptyList(); } return Collections .singletonList(new OriginTrackedMapPropertySource(name, properties));}private Map
loadProperties(Resource resource) throws IOException { String filename = resource.getFilename(); if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { // 读取配置文件 return (Map) PropertiesLoaderUtils.loadProperties(resource); } return new OriginTrackedPropertiesLoader(resource).load();}复制代码

可以看出PropertiesPropertySourceLoader是通过PropertiesLoaderUtils.loadProperties读取配置文件,继续跟踪loadProperties

/** * Load properties from the given resource (in ISO-8859-1 encoding). * @param resource the resource to load from * @return the populated Properties instance * @throws IOException if loading failed * @see #fillProperties(java.util.Properties, Resource) */public static Properties loadProperties(Resource resource) throws IOException {	Properties props = new Properties();	fillProperties(props, resource);	return props;}private Map
loadProperties(Resource resource) throws IOException { String filename = resource.getFilename(); if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { // 读取XML格式文件 return (Map) PropertiesLoaderUtils.loadProperties(resource); } return new OriginTrackedPropertiesLoader(resource).load();}复制代码

首先从注释中,得知Spring是以ISO-8859-1编码读取配置文件内容的,所以当我们在application.properties中写入中文,会发现在读取的时候中文都变成了乱码。这里只是通过注释得知的,如何寻找确凿的证据呢?继续跟踪OriginTrackedPropertiesLoaderload方法

public Map
load(boolean expandLists) throws IOException { // 创建字符读取Reader try (CharacterReader reader = new CharacterReader(this.resource)) { Map
result = new LinkedHashMap<>(); StringBuilder buffer = new StringBuilder(); while (reader.read()) { String key = loadKey(buffer, reader).trim(); if (expandLists && key.endsWith("[]")) { key = key.substring(0, key.length() - 2); int index = 0; do { OriginTrackedValue value = loadValue(buffer, reader, true); put(result, key + "[" + (index++) + "]", value); if (!reader.isEndOfLine()) { reader.read(); } } while (!reader.isEndOfLine()); } else { OriginTrackedValue value = loadValue(buffer, reader, false); put(result, key, value); } } return result; }}复制代码

为了寻找乱码的原因,我们继续跟踪CharacterReader构造方法

CharacterReader(Resource resource) throws IOException {    // InputStreamReader以ISO-8859-1读取内容	this.reader = new LineNumberReader(new InputStreamReader(			resource.getInputStream(), StandardCharsets.ISO_8859_1));}复制代码

看到这里我们终于明白了,原来是CharacterReader在读取文件内容的时候采用了ISO-8859-1编码,所以才导致中文乱码的原因。

明白了乱码原因之后,在回到上面的方法观察loadKey方法读取=前面的内容作为配置项名称,并且支持数组(配置项名称以[]结尾)。loadKey如何读取到key的呢?

private String loadKey(StringBuilder buffer, CharacterReader reader)		throws IOException {    // 有效char的数量,设置成0,相当于清空buffer,但实际字符还是存在StringBuilder中,只不过生成String的时候过滤了 >count 的字符	buffer.setLength(0);	boolean previousWhitespace = false;	while (!reader.isEndOfLine()) {        // 是否是分隔符		if (reader.isPropertyDelimiter()) {			reader.read();			return buffer.toString();		}        // 是否是空格		if (!reader.isWhiteSpace() && previousWhitespace) {			return buffer.toString();		}		previousWhitespace = reader.isWhiteSpace();        // 添加当前字符到buffer		buffer.append(reader.getCharacter());		reader.read();	}	return buffer.toString();}复制代码

reader.isPropertyDelimiter用来判断当前字符是否是key/value分隔符,如果是则说明已经读取到完整的key,继续读取下一个字符,直到读取到完整的key

接下来就要读取value的值了(数组配置项的值是什么格式呢?)

继续跟踪loadValue方法

private OriginTrackedValue loadValue(StringBuilder buffer, CharacterReader reader,		boolean splitLists) throws IOException {	buffer.setLength(0);	while (reader.isWhiteSpace() && !reader.isEndOfLine()) {		reader.read();	}	Location location = reader.getLocation();	while (!reader.isEndOfLine() && !(splitLists && reader.isListDelimiter())) {		buffer.append(reader.getCharacter());		reader.read();	}	Origin origin = new TextResourceOrigin(this.resource, location);	return OriginTrackedValue.of(buffer.toString(), origin);}public boolean isListDelimiter() {    // 数组配置分隔符	return !this.escaped && this.character == ',';}复制代码

这里的location是什么意思呢?继续跟踪Location类的定义

public static final class Location {	private final int line;	private final int column;    // 其余内容省略}复制代码

从这里可以看出Location实际记录了当前reader读取到的行和列的值。

继续回到上面的方法,可以发现读取value的方式实际和读取key相似,这里不再赘述,相信大家都能够看明白。

PropertiesPropertySourceLoader就基本完成了properties文件的读取。YamlPropertySourceLoader配置文件的加载逻辑类似,大家可以自行阅读相关源码。到此为止,我们也明白了第二个问题“如何读取相关配置文件内容?”。接下来关注第三个问题。

三、如何区分不同环境的配置?

假设我们在项目中存在两个多个配置文件

  • application.properties
spring.profiles.active=dev复制代码
  • application-dev.properties
a=dev复制代码
  • application-test.properties
a=test复制代码

通过之前的代码分析,我们可以知道初始状态下profiles存在两个值nulldefault,首先默认加载的是application.properties文件,从该文件中可以读取到spring.profiles.active配置项,然后将读取到的profile设置为当前激活的profile

for (Document document : documents) {	if (filter.match(document)) {		// 获取配置文件中设置的profile		addActiveProfiles(document.getActiveProfiles());		addIncludedProfiles(document.getIncludeProfiles());		loaded.add(document);	}}void addActiveProfiles(Set
profiles) { if (profiles.isEmpty()) { return; } if (this.activatedProfiles) { if (this.logger.isDebugEnabled()) { this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied"); } return; } this.profiles.addAll(profiles); if (this.logger.isDebugEnabled()) { this.logger.debug("Activated activeProfiles " + StringUtils.collectionToCommaDelimitedString(profiles)); } this.activatedProfiles = true; // 移除默认的default removeUnprocessedDefaultProfiles();}private void removeUnprocessedDefaultProfiles() { this.profiles.removeIf( (profile) -> (profile != null && profile.isDefaultProfile()));}复制代码

从上面的代码中可以看出来,读取完默认的配置文件之后,将原有的default移除,添加读取到的profileprofiles,接着回到开始的load方法

public void load() {	this.profiles = new LinkedList<>();	this.processedProfiles = new LinkedList<>();	this.activatedProfiles = false;	this.loaded = new LinkedHashMap<>();	initializeProfiles();	while (!this.profiles.isEmpty()) {		// 第一次循环的时候,profile的值为null		// 第二次循环的时候,profile的值为application.properties中配置的值		Profile profile = this.profiles.poll();		if (profile != null && !profile.isDefaultProfile()) {			addProfileToEnvironment(profile.getName());		}		load(profile, this::getPositiveProfileFilter,				addToLoaded(MutablePropertySources::addLast, false));		this.processedProfiles.add(profile);	}	resetEnvironmentProfiles(this.processedProfiles);	load(null, this::getNegativeProfileFilter,			addToLoaded(MutablePropertySources::addFirst, true));	addLoadedPropertySources();}复制代码

所以,当默认配置文件中设置了激活的profile,接下来就会去读取该文件内容。在本例中,第二次循环读取的就是application-dev.properties文件,而application-test.properties不会被读取。这样就实现了根据profile读取不同环境的配置文件。

这时候我们再考虑一个问题,如果在application.propertiesapplication-dev.properties同时添加相同的key,但value不同的配置,哪一个配置会生效呢?基于目前的分析来看,两个配置都已经被读取了,怎么决定优先级呢?

实际上application-dev.properties中的配置会生效,为了搞清楚这个问题,我们继续往下跟踪addLoadedPropertySources方法

/** * 已经读取到的配置 */private Map
loaded;private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List
loaded = new ArrayList<>(this.loaded.values()); // 反转集合 Collections.reverse(loaded); String lastAdded = null; Set
added = new HashSet<>(); 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 { // 从尾部添加 destination.addLast(source); } } else { // 从指定位置之后添加 destination.addAfter(lastAdded, source); }}复制代码

loaded对象保存了之前读取到的配置。从这里可以看出是将loaded中读取到的配置文件添加到environment中,并且都是从尾部添加。首先我们要明白一点,PropertySourceMutablePropertySources中的顺序决定了它的优先级,也就是说越靠前优先级越高。那么我们会想,loaded中的元素顺序应该是application.properties -> application-dev.properties,所以application.properties优先级更高,这显然不合符实际情况。

再回到上面的代码中可以看到Collections.reverse(loaded),到这里我们就明白了,添加的顺序和读取的顺序正好是相反的,所以后读取到的application-dev.properties反而先添加到destination中,所以applicaiton-dev.properties的优先级比application.properties高。

到此我们已经完全明白了这三个问题,顺便还搞清楚了为什么properties里面的中文会乱码的原因。

  1. 什么时候开始加载配置文件?
  2. 如何读取相关配置文件内容?
  3. 如何区分不同环境的配置?

中间涉及的源码非常多,而且方法名称相似,很容易让人迷惑,所以需要大家仔细多读,才能完全理解整个的流程。

转载地址:http://vfena.baihongyu.com/

你可能感兴趣的文章
vi/vim的三种基本工作模式
查看>>
程维:滴滴希望未来出行选择和调用飞机一样方便
查看>>
windows中使用Git创建分支(branch)?
查看>>
Zabbix3.0学习笔记
查看>>
【最佳实践】OSS开源工具ossutil-增量上传
查看>>
Python | Python学习之深浅拷贝
查看>>
初识Avro
查看>>
中国在人工智能领域成为全球最‘吸金’的国家
查看>>
Kubeflow实战系列:阿里云上使用JupyterHub
查看>>
研究人员成功从地面入侵飞行中的飞机
查看>>
关于代码的那些低级错误,都是血泪的教训
查看>>
0322理解db file parallel read等待事件2
查看>>
浅谈script标签中的async和defer
查看>>
不忘初“芯”,共创未来,2017安创成长营4期Demo Day在杭州圆满落幕
查看>>
JDBC示例(增删查改)
查看>>
MySQL集群方案收集
查看>>
IBM称可在1个原子内存储1 bit数据
查看>>
区块链应用场景
查看>>
Java数据库连接池研究
查看>>
【CVPR 2018】用狗的数据训练AI,华盛顿大学研发模拟狗行为的AI系统
查看>>