在Spring Boot源码分析-启动过程中我们进行了启动源码的分析,大致了解了整个Spring Boot
的启动过程,具体细节这里不再赘述,感兴趣的同学可以自行阅读。今天让我们继续阅读源码,了解配置文件加载原理。
基于
Spring Boot 2.1.0.RELEASE
在开始阅读源码之前,首先准备三个问题。
- 什么时候开始加载配置文件?
- 如何读取相关配置文件内容?
- 如何区分不同环境的配置?
下面用Spring
代替Spring Boot
接下来进入主题,首先关注第一个问题。
一、什么时候开始加载配置文件?
从Spring Boot源码分析-启动过程中我们可以得知,Spring
在启动的过程中发布了ApplicationEnvironmentPreparedEvent
事件,ConfigFileApplicationListener
监听到这个消息的时候,开始实例化并调用(META-INF/spring.factories
中定义)EnvironmentPostProcessor
的postProcessEnvironment
方法。而ConfigFileApplicationListener
本身也实现了EnvironmentPostProcessor
接口,且将自身加入到EnvironmentPostProcessor
集合中,故也会调用自身的方法。
跟踪ConfigFileApplicationListener
的postProcessEnvironment
方法源码
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
源码,Loader
是ConfigFileApplicationListener
的一个内部类,用来读取配置文件并配置相关环境。
首先跟踪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
源码得知,默认profile
是default
。
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
里面已经有了两个元素,null
和default
。
接下来回到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
实际上也是一个空集合。
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);
}
从上面的分析可知,当前profile
是null
,所以继续跟踪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
对象实际上是PropertiesPropertySourceLoader
和YamlPropertySourceLoader
,我们这里的配置文件是properties
文件,所以我们选择跟踪PropertiesPropertySourceLoader
的load
方法。
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
中写入中文,会发现在读取的时候中文都变成了乱码。这里只是通过注释得知的,如何寻找确凿的证据呢?继续跟踪OriginTrackedPropertiesLoader
的load
方法
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
存在两个值null
和default
,首先默认加载的是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
移除,添加读取到的profile
到profiles
,接着回到开始的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.properties
和application-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
中,并且都是从尾部添加。首先我们要明白一点,PropertySource
在MutablePropertySources
中的顺序决定了它的优先级,也就是说越靠前优先级越高。那么我们会想,loaded
中的元素顺序应该是application.properties -> application-dev.properties
,所以application.properties
优先级更高,这显然不合符实际情况。
再回到上面的代码中可以看到Collections.reverse(loaded)
,到这里我们就明白了,添加的顺序和读取的顺序正好是相反的,所以后读取到的application-dev.properties
反而先添加到destination
中,所以applicaiton-dev.properties
的优先级比application.properties
高。
到此我们已经完全明白了这三个问题,顺便还搞清楚了为什么properties
里面的中文会乱码的原因。
- 什么时候开始加载配置文件?
- 如何读取相关配置文件内容?
- 如何区分不同环境的配置?
中间涉及的源码非常多,而且方法名称相似,很容易让人迷惑,所以需要大家仔细多读,才能完全理解整个的流程。
本文由博客一文多发平台 OpenWrite 发布!