SpringBoot配置文件初见
在实际的开发中,使用配置文件的方式可以解决硬编码的问题,更加方便我们项目的部署和后续修改。
在SpringBoot中,使用全局配置文件能够对一些默认配置值进行修改及自定义配置。
Spring Boot使用一个application.properties或者application.yaml的文件作为全局配置文件
从官方文档可以看出, SpringBoot加载配置文件时会从以下四个位置进行加载 :
需要注意的是,在上列中越高的位置优先级越高。如果有相同的配置,优先级高的配置文件会覆盖优先级低的配置文件。
如果上图不方便理解 比较抽象的话,下图给出了实际的项目案例,来表示配置文件可以存放的位置:
上图中的标号即对应了官网给出的加载配置文件的四个位置。
配置文件位置的优先级
答案是按照优先级进行生效,也就是按照官网给出的顺序,顺序越靠上,优先级越高。
答案是能生效。SpringBoot会对上面四个位置的配置文件都进行加载,会形成一个互补设置。
properties与yaml的优先级对比
如果相同目录下,同时存在properties文件和yaml文件,那么以谁为准呢?
如果在同一个目录下,有application.yml也有application.properties,以谁为准需要参考版本:
在SpringBoot2.4.0以前,优先级properties > yaml
在SpringBoot2.4.0以后,优先级yaml > properties
如果同一个配置属性,在多个配置文件都配置了,默认使用第1哥读取到的,后面读取的不覆盖前面读取到的。
不过在创建SpringBoot项目时,一般的配置文件都是放置在"项目的resources目录下",SpringBoot会默认在resources目录下创建一个application.properties的文件。
另外,如果配置文件名字不叫application.properties或者application.yml,可以通过以下参数来指定 配置文件的名字,myproject是配置文件名
$ java -jar myproject.jar --spring.config.name=myproject
SpringBoot配置文件的加载原理源码分析
加载ApplicationListener实现类监听器
在了解了SpringBoot使用的全局配置文件后,我们来思考一个问题,就是该配置文件是如何生效的呢?也就是当我们在配置文件里配置了一些属性,比如配置了server.port = 8088,那么SpringBoot是如何加载该配置文件使得SpringBoot的启动监听端口为8088的呢?
这里初步做一个介绍,更细致的加载过程可以参考下文的源码解析来学习。
SpringBoot对于配置文件的加载是利用ConfigFileApplicationListener监听器来完成的。
对于每一个SpringBoot项目,都会有一个项目主程序启动类,在该类的main
方法中调用SpringApplication.run();
方法来启动SpringBoot程序,在启动过程中,便会有SpringBoot的一系列内部加载和初始化过程,因此该类对于我们的分析尤为重要。
点击进入到SpringApplication
的主类中,观察其构造函数:
可以看到在构造器中对监听器进行了设置。其中getSpringFactoriesInstances
方法用于从spring.factories文件中加载ApplicationListener实现类。该方法的加载原理如下所示,一直深入调用过程找到该方法,可以看到该方法主要就是去META-INF/spring.factories
路径下加载spring.factories文件,并对文件进行解析。对该文件下的每一个接口及其实现类,以接口名为Key,包含的实现类名为value进行保存,形成一个Map<String, List>的数据结构进行返回。
然后对调用过程回推,在对spring.factories文件加载完,并保存了文件下每个接口及其实现类的全限定类名后,getSpringFactoriesInstances
方法会根据传递的参数Class<T> type
来从上面得到的Map结构中拿出该接口名对应的所有实现类的集合。并对集合中的所有实现类,利用反射生成对应的实例进行保存: 到这里我们可以理清构造函数中setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
方法的过程和作用,即在SpringBoot启动初始化时,通过读取META-INF/spring.factories
文件,并对其进行解析,生成示例化对象,然后从中取出ApplicationListener
接口对应的所有实现类的实例对象,注入监听器中。
ConfigFileApplicationListener监听器监听ApplicationEnvironmentPreparedEvent事件
经过上面的分析我们可以看到,SpringBoot在启动时会初始化一系列监听器,而这些监听器都是在ApplicationListener接口下的,因此我们取到META-INF/spring.factories
看一下有哪些实现类: 这里最关键的实现类就是ConfigFileApplicationListener
,该监听器会监听ApplicationEnvironmentPreparedEvent事件,当监听到该事件后,会调用load
方法,去上面说的四个默认路径检索配置文件,如果检索到了,则进行加载封装供上层方法调用。
这是它的一个大致的整体流程,接下来我们深入源码中,按步骤对其进行分析
发送事件与监听器的触发
ApplicationStartingEvent事件
首先进入到run
方法中: 该方法中首先调用getRunListeners
方法,同样是从spring.factories文件中加载SpringApplicationRunListeners接口下的实现类org.springframework.boot.context.event.EventPublishingRunListener
,接下来调用该监听器的starting()
方法 该starting()
方法内,会创建一个ApplicationStartingEvent
的事件,并利用multicastEvent
方法进行广播该事件给应用中包含的所有监听器,这里的应用就是参数this.application
,也就是现在的SpringApplication
,它所包含的监听器也就是上文中最初加载的ApplicationListener
下的11个监听器。可以看到,每个event
对象下都包含一个source
源,这个源表示了事件最初在其上发生的对象,这里的source
源就是SpringApplication
。 接下来,会进入到SimpleApplicationEventMulticaster
类下的multicastEvent
方法,这里比较重要的一个方法就是getApplicationListeners
方法,该方法内部会根据该事件的类型,以及事件所包含的源里的监听器,筛选出对该事件感兴趣的监听器集合 节选出来getApplicationListeners
方法内的重要方法: retrieveApplicationListeners
方法,该方法就是实际检索给定事件和源类型的应用程序侦听器,返回的listeners
对象即包含了监听该事件的应用程序监听器集合。 这里的listeners
即包含了最初ApplicationListeners
接口下的11个监听器。 方法中的supportsEvent
方法即判断给定监听器是否支持给定事件(或者说是否监听该事件)。由于目前这里的eventType
表示的是ApplicationStartingEvent
,该事件触发的监听器包括11个中的:
BackgroundPreinitializer
org.springframework.boot.context.logging.LoggingApplicationListener
org.springframework.boot.context.config.DelegatingApplicationListener
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
这里该事件还不触发org.springframework.boot.context.config.ConfigFileApplicationListener
最终当getApplicationListeners
方法拿到监听器对象集合后,遍历得到每个监听器,然后调用invokeListener(listener, event);
方法,再利用listener.onApplicationEvent(event)
方法,通过调用相应监听器的onApplicationEvent(event)
方法来唤醒监听器对象,执行相应的触发操作。
ApplicationEnvironmentPreparedEvent事件
执行完相应监听器的操作后,会继续回到run
方法中执行prepareEnvironment
方法,该方法同样是利用监听器和事件的机制,来触发监听完成环境准备的工作。 这里的listeners
仍然是EventPublishingRunListener
,因此这里的prepareEnvironment
相当于是调用了该监听器的不同方法,来产生不同的事件类型,可以看到,这一次创建的事件类型为ApplicationEnvironmentPreparedEvent
,也就是我们最开始说的加载配置文件的监听器所监听的事件类型,因此到这里我们就离探究配置文件加载原理又近了一步。创建该事件类型后,同样是利用multicastEvent
将该事件广播给该应用程序下的所有监听器,其实它的流程就跟上面是一样的了,只是产生的事件不同。 因此,这里不在赘述该事件的触发流程,同样的是在retrieveApplicationListeners
方法里的supportEvent
方法中,筛选出支持ApplicationEnvironmentPreparedEvent
事件的监听器集合并返回,而这次触发的监听器就包括了org.springframework.boot.context.config.ConfigFileApplicationListener
监听器。由于监听器的真正执行是通过调用listener.onApplicationEvent(event)
方法来执行的,因此我们从该方法开始分析: 这里loadPostProcessors
方法就是从spring.factories
中加载EnvironmentPostProcessor
接口对应的实现类,并把当前对象也添加进去(因为ConfigFileApplicationListener
也实现了EnvironmentPostProcessor
接口,所以可以添加)。因此在下方遍历时,会访问该类下的postProcessEnvironment
方法。
接下来进入到postProcessEnvironment
方法 接下来就是要分析最重要的Loader
方法 该方法中,首先SpringFactoriesLoader.loadFactories
从spring.factories
中加载PropertySourceLoader
接口对应的实现类,也就是 这两个实现类分别用于加载文件名后缀为properties和yaml的文件。
接下来最核心的方法就是红框中的load
方法,这里会最终加载我们的配置文件,因此我们进行深入探究:
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
首先调用了getSearchLocations
方法
//获得加载配置文件的路径
//可以通过spring.config.location配置设置路径,如果没有配置,则使用默认
//默认路径由DEFAULT_SEARCH_LOCATIONS指定:
// CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location"
// CONFIG_LOCATION_PROPERTY = "spring.config.location";
// DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
private Set<String> getSearchLocations() {
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
else {
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
该方法用于获取配置文件的路径,如果利用spring.config.location
指定了配置文件路径,则根据该路径进行加载。否则则根据默认路径加载,而默认路径就是我们最初提到的那四个路径。接下来,再深入asResolvedSet
方法内部分析一下
private Set<String> asResolvedSet(String value, String fallback) {
List<String> list = Arrays.asList(StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(
(value != null) ? this.environment.resolvePlaceholders(value) : fallback)));
Collections.reverse(list);
return new LinkedHashSet<>(list);
这里的value
表示ConfigFileApplicationListener
初始化时设置的搜索路径,而fallback
就是DEFAULT_SEARCH_LOCATIONS
默认搜索路径。StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray
())方法就是以逗号作为分隔符对"classpath:/,classpath:/config/,file:./,file:./config/"
进行切割,并返回一个字符数组。而这里的Collections.reverse(list);
之后,就是体现优先级的时候了,先被扫描到的配置文件会优先生效。
这里我们拿到搜索路径之后,load
方法里对每个搜索路径进行遍历,首先调用了getSearchNames()
方法
// 返回所有要检索的配置文件前缀
// CONFIG_NAME_PROPERTY = "spring.config.name"
// DEFAULT_NAMES = "application"
private Set<String> 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);
该方法中如果我们通过spring.config.name
设置了要检索的配置文件前缀,会按设置进行加载,否则加载默认的配置文件前缀即application
。
拿到所有需要加载的配置文件前缀后,则遍历每个需要加载的配置文件,进行搜索加载,加载过程如下:
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//下面的if分支默认是不走的,除非我们设置spring.config.name为空或者null
//或者是spring.config.location指定了配置文件的完整路径,也就是入参location的值
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
//检查配置文件名的后缀是否符合要求,
//文件名后缀要求是properties、xml、yml或者yaml
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
throw new IllegalStateException("File extension of config file location '" + location
+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
+ "a directory, it must end in '/'");
Set<String> processed = new HashSet<>();
//propertySourceLoaders属性是在Load类的构造方法中设置的,可以加载文件后缀为properties、xml、yml或者yaml的文件
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
关注下面的两个for
循环,this.propertySourceLoaders
既包含了上面提到的两个PropertiesPropertySourceLoader
和YamlPropertySourceLoader
,PropertiesPropertySourceLoader
可以加载文件扩展名为properties
和xml
的文件,YamlPropertySourceLoader
可以加载文件扩展名为yml
和yaml
的文件。获取到搜索路径、文件名和扩展名后,就可以到对应的路径下去检索配置文件并加载了。
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);
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
//在文件名上加上profile值,之后调用load方法加载配置文件,入参带有过滤器,可以防止重复加载
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
//加载不带profile的配置文件
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
// 加载配置文件
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
//调用Resource类到指定路径加载配置文件
// location比如file:./config/application.properties
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 + "]";
//读取配置文件内容,将其封装到Document类中,解析文件内容主要是找到
//配置spring.profiles.active和spring.profiles.include的值
List<Document> 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<Document> loaded = new ArrayList<>();
//遍历配置文件,处理里面配置的profile
for (Document document : documents) {
if (filter.match(document)) {
//将配置文件中配置的spring.profiles.active和
//spring.profiles.include的值写入集合profiles中,
//上层调用方法会读取profiles集合中的值,并读取对应的配置文件
//addActiveProfiles方法只在第一次调用时会起作用,里面有判断
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);
该方法首先调用this.resourceLoader.getResource(location);
用来判断location路径下的文件是否存在,如果存在,会调用loadDocuments
方法对配置文件进行加载:
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
throws IOException {
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
List<PropertySource<?>> loaded = loader.load(name, resource);
documents = asDocuments(loaded);
this.loadDocumentsCache.put(cacheKey, documents);
return documents;
再内部根据不同的PropertySourceLoader
调用相应的load
方法和loadProperties(resource)
方法
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
Map<String, ?> properties = loadProperties(resource);
if (properties.isEmpty()) {
return Collections.emptyList();
return Collections
.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
@SuppressWarnings({ "unchecked", "rawtypes" })
private Map<String, ?> 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();
由于我们目前的配置文件只有application.properties
,也就是文件结尾不是以xml
作为扩展名。因此loadProperties
方法会进入到new OriginTrackedPropertiesLoader
。因此再进入到new OriginTrackedPropertiesLoader(resource).load();
。(不要急 就快到了)
Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
try (CharacterReader reader = new CharacterReader(this.resource)) {
Map<String, OriginTrackedValue> 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;
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(Resource resource) throws IOException {
this.reader = new LineNumberReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.ISO_8859_1));
private String loadKey(StringBuilder buffer, CharacterReader reader) throws IOException {
buffer.setLength(0);
boolean previousWhitespace = false;
while (!reader.isEndOfLine()) {
// 判断读取到的字节是否为'=' 或者为 ':',如果是则直接返回读取都的buffer内容
if (reader.isPropertyDelimiter()) {
reader.read();
return buffer.toString();
if (!reader.isWhiteSpace() && previousWhitespace) {
return buffer.toString();
previousWhitespace = reader.isWhiteSpace();
buffer.append(reader.getCharacter());
reader.read();
return buffer.toString();
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);
终于,我们看见了曙光。在这个方法里,首先CharacterReader
方法将我们的resource也就是配置文件转为了输入流,然后利用reader.read()
进行读取,在loadKey
方法中我们看到,这里判断读取到的是否为'=' 或者为 ':',也就是我们在配置文件中以'='或者':'分割的key-value。因此看到这里,我们可以直观的感受到这里应该是读取配置文件,并切分key和value的地方。
最终,对配置文件读取完成后,会将其以key-value的形式封装到一个Map集合中进行返回,然后封装到OriginTrackedMapPropertySource
中作为一个MapPropertySource
对象。再层层往上回退发现会最终封装成一个asDocuments(loaded);
Document对象。最后回到最上层的load
方法中,loadDocuments(loader, name, resource);
方法即返回我们加载好的配置文件Document对象集合。并对集合中的每一个配置文件document对象进行遍历,调用loaded.forEach((document) -> consumer.accept(profile, document));
整理和总结
经过我们上面比较长篇大论的分析,我们已经知道配置文件是如何被检索以及如何被加载的了,接下来,我们对上面的流程进行一下总结和分析:
SpringBoot在启动加载时,会利用事件-监听器模式,就像发布-订阅模式,在不同的阶段利用不同的事件唤醒相应的监听器执行对应的操作。对于配置文件加载关键的监听器是ConfigFileApplicationListener
,该监听器会监听ApplicationEnvironmentPreparedEvent
事件。
每个事件event都会包含一个source
源来表示该事件最先发生在其上的对象,ApplicationEnvironmentPreparedEvent
事件包含的source
源是SpringApplication
,包含了一组listeners
监听器。SpringBoot会根据事件对监听器进行筛选,只筛选出那些支持该事件的监听器,并调用方法唤醒这些监听器执行相应逻辑。
当ApplicationEnvironmentPreparedEvent
事件发生时,会唤醒ConfigFileApplicationListener
监听器执行相应逻辑。最主要的加载方法load
中,首先会获取到配置文件的搜索路径。如果设置了spring.config.location
则会去指定目录下搜索,否则就去默认的搜索目录下classpath:/,classpath:/config/,file:./,file:./config/
。
拿到所有待搜索目录后,遍历每个目录获取需要加载的配置文件。如果指定了spring.config.name
,则加载指定名称的配置文件。否则使用默认的application
作为配置文件的前缀名。然后,会利用PropertiesPropertySourceLoader
和YamlPropertySourceLoader
加载后缀名为properties、xml、yml或者yaml
的文件。
拿到文件目录和文件名后,就可以去对应的路径下加载配置文件了。核心的过程是利用输入流读取配置文件,并根据读到的分隔符进行判断来切分配置文件的key和value。并将内容以key-value键值对的形式封装成一个OriginTrackedMapPropertySource
,最后再将一个个配置文件封装成Document
。最后遍历这些Documents
,调用consumer.accept(profile, document));
供上层调用访问。
下面用流程图梳理一下整个加载过程中的关键步骤:
上面是自己关于这个问题阅读源码过程中的一些观点和想法,还是会有不够细致的地方,也可能有理解不够深刻或者错误的地方,还希望各位指正,一起通过阅读源码提升自己的代码水平!
\