Skip to content

Commit e64a145

Browse files
committed
Add support for wildcard locations for properties and YAML files
Closes gh-19909
1 parent de1a26c commit e64a145

File tree

5 files changed

+98
-42
lines changed

5 files changed

+98
-42
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ On your application classpath (for example, inside your jar) you can have an `ap
396396
When running in a new environment, an `application.properties` file can be provided outside of your jar that overrides the `name`.
397397
For one-off testing, you can launch with a specific command line switch (for example, `java -jar app.jar --name="Spring"`).
398398

399+
NOTE: Spring Boot also supports wildcard locations when loading configuration files.
400+
By default, a wildcard location of `config/*/` outside of your jar is supported.
401+
Wildcard locations are also supported when specifying `spring.config.additional-location` and `spring.config.location`.
402+
399403
[[boot-features-external-config-application-json]]
400404
[TIP]
401405
====
@@ -492,10 +496,11 @@ If `spring.config.location` contains directories (as opposed to files), they sho
492496
Files specified in `spring.config.location` are used as-is, with no support for profile-specific variants, and are overridden by any profile-specific properties.
493497

494498
Config locations are searched in reverse order.
495-
By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/`.
499+
By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/`.
496500
The resulting search order is the following:
497501

498502
. `file:./config/`
503+
. `file:./config/*/`
499504
. `file:./`
500505
. `classpath:/config/`
501506
. `classpath:/`
@@ -513,6 +518,7 @@ For example, if additional locations of `classpath:/custom-config/,file:./custom
513518
. `file:./custom-config/`
514519
. `classpath:custom-config/`
515520
. `file:./config/`
521+
. `file:./config/*/`
516522
. `file:./`
517523
. `classpath:/config/`
518524
. `classpath:/`

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@
6262
import org.springframework.core.io.DefaultResourceLoader;
6363
import org.springframework.core.io.Resource;
6464
import org.springframework.core.io.ResourceLoader;
65+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
6566
import org.springframework.core.io.support.SpringFactoriesLoader;
6667
import org.springframework.util.Assert;
6768
import org.springframework.util.CollectionUtils;
@@ -75,6 +76,7 @@
7576
* 'application.properties' and/or 'application.yml' files in the following locations:
7677
* <ul>
7778
* <li>file:./config/</li>
79+
* <li>file:./config/{@literal *}/</li>
7880
* <li>file:./</li>
7981
* <li>classpath:config/</li>
8082
* <li>classpath:</li>
@@ -107,7 +109,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
107109
private static final String DEFAULT_PROPERTIES = "defaultProperties";
108110

109111
// Note the order is from least to most specific (last one wins)
110-
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
112+
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
111113

112114
private static final String DEFAULT_NAMES = "application";
113115

@@ -158,6 +160,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
158160

159161
private final DeferredLog logger = new DeferredLog();
160162

163+
private static final Resource[] EMPTY_RESOURCES = {};
164+
161165
private String searchLocations;
162166

163167
private String names;
@@ -299,6 +303,8 @@ private class Loader {
299303

300304
private final ResourceLoader resourceLoader;
301305

306+
private final PathMatchingResourcePatternResolver patternResolver;
307+
302308
private final List<PropertySourceLoader> propertySourceLoaders;
303309

304310
private Deque<Profile> profiles;
@@ -317,6 +323,7 @@ private class Loader {
317323
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
318324
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
319325
getClass().getClassLoader());
326+
this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
320327
}
321328

322329
void load() {
@@ -497,47 +504,51 @@ private void loadForFileExtension(PropertySourceLoader loader, String prefix, St
497504
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
498505
DocumentConsumer consumer) {
499506
try {
500-
Resource resource = this.resourceLoader.getResource(location);
501-
if (resource == null || !resource.exists()) {
502-
if (this.logger.isTraceEnabled()) {
503-
StringBuilder description = getDescription("Skipped missing config ", location, resource,
504-
profile);
505-
this.logger.trace(description);
507+
Resource[] resources = getResources(location);
508+
for (Resource resource : resources) {
509+
if (resource == null || !resource.exists()) {
510+
if (this.logger.isTraceEnabled()) {
511+
StringBuilder description = getDescription("Skipped missing config ", location, resource,
512+
profile);
513+
this.logger.trace(description);
514+
}
515+
return;
506516
}
507-
return;
508-
}
509-
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
510-
if (this.logger.isTraceEnabled()) {
511-
StringBuilder description = getDescription("Skipped empty config extension ", location,
512-
resource, profile);
513-
this.logger.trace(description);
517+
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
518+
if (this.logger.isTraceEnabled()) {
519+
StringBuilder description = getDescription("Skipped empty config extension ", location,
520+
resource, profile);
521+
this.logger.trace(description);
522+
}
523+
return;
514524
}
515-
return;
516-
}
517-
String name = "applicationConfig: [" + location + "]";
518-
List<Document> documents = loadDocuments(loader, name, resource);
519-
if (CollectionUtils.isEmpty(documents)) {
520-
if (this.logger.isTraceEnabled()) {
521-
StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
522-
profile);
523-
this.logger.trace(description);
525+
String name = (location.contains("*")) ? "applicationConfig: [" + resource.toString() + "]"
526+
: "applicationConfig: [" + location + "]";
527+
List<Document> documents = loadDocuments(loader, name, resource);
528+
if (CollectionUtils.isEmpty(documents)) {
529+
if (this.logger.isTraceEnabled()) {
530+
StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
531+
profile);
532+
this.logger.trace(description);
533+
}
534+
return;
524535
}
525-
return;
526-
}
527-
List<Document> loaded = new ArrayList<>();
528-
for (Document document : documents) {
529-
if (filter.match(document)) {
530-
addActiveProfiles(document.getActiveProfiles());
531-
addIncludedProfiles(document.getIncludeProfiles());
532-
loaded.add(document);
536+
List<Document> loaded = new ArrayList<>();
537+
for (Document document : documents) {
538+
if (filter.match(document)) {
539+
addActiveProfiles(document.getActiveProfiles());
540+
addIncludedProfiles(document.getIncludeProfiles());
541+
loaded.add(document);
542+
}
533543
}
534-
}
535-
Collections.reverse(loaded);
536-
if (!loaded.isEmpty()) {
537-
loaded.forEach((document) -> consumer.accept(profile, document));
538-
if (this.logger.isDebugEnabled()) {
539-
StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
540-
this.logger.debug(description);
544+
Collections.reverse(loaded);
545+
if (!loaded.isEmpty()) {
546+
loaded.forEach((document) -> consumer.accept(profile, document));
547+
if (this.logger.isDebugEnabled()) {
548+
StringBuilder description = getDescription("Loaded config file ", location, resource,
549+
profile);
550+
this.logger.debug(description);
551+
}
541552
}
542553
}
543554
}
@@ -546,6 +557,15 @@ private void load(PropertySourceLoader loader, String location, Profile profile,
546557
}
547558
}
548559

560+
private Resource[] getResources(String location) {
561+
try {
562+
return this.patternResolver.getResources(location);
563+
}
564+
catch (IOException ex) {
565+
return EMPTY_RESOURCES;
566+
}
567+
}
568+
549569
private void addIncludedProfiles(Set<Profile> includeProfiles) {
550570
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
551571
this.profiles.clear();

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,6 +58,7 @@
5858
import org.springframework.core.env.SimpleCommandLinePropertySource;
5959
import org.springframework.core.env.StandardEnvironment;
6060
import org.springframework.core.io.ByteArrayResource;
61+
import org.springframework.core.io.ClassPathResource;
6162
import org.springframework.core.io.Resource;
6263
import org.springframework.core.io.ResourceLoader;
6364
import org.springframework.test.context.support.TestPropertySourceUtils;
@@ -73,6 +74,7 @@
7374
* @author Phillip Webb
7475
* @author Dave Syer
7576
* @author Eddú Meléndez
77+
* @author Madhura Bhave
7678
*/
7779
@ExtendWith(OutputCaptureExtension.class)
7880
class ConfigFileApplicationListenerTests {
@@ -109,7 +111,7 @@ public String getFilename() {
109111
}
110112
};
111113
}
112-
return null;
114+
return new ClassPathResource("doesnotexist");
113115
}
114116

115117
@Override
@@ -1001,6 +1003,32 @@ void whenConfigLocationSpecifiesFolderConfigFileProcessingContinues() {
10011003
this.initializer.postProcessEnvironment(this.environment, this.application);
10021004
}
10031005

1006+
@Test
1007+
void locationsWithWildcardFoldersShouldLoadAllFilesThatMatch() {
1008+
String location = "file:src/test/resources/config/*/";
1009+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1010+
"spring.config.location=" + location);
1011+
this.initializer.setSearchNames("testproperties");
1012+
this.initializer.postProcessEnvironment(this.environment, this.application);
1013+
String a = this.environment.getProperty("a.property");
1014+
String b = this.environment.getProperty("b.property");
1015+
assertThat(a).isEqualTo("apple");
1016+
assertThat(b).isEqualTo("ball");
1017+
}
1018+
1019+
@Test
1020+
void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() {
1021+
String location = "file:src/test/resources/config/*/testproperties.properties";
1022+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1023+
"spring.config.location=" + location);
1024+
this.initializer.setSearchNames("testproperties");
1025+
this.initializer.postProcessEnvironment(this.environment, this.application);
1026+
String a = this.environment.getProperty("a.property");
1027+
String b = this.environment.getProperty("b.property");
1028+
assertThat(a).isEqualTo("apple");
1029+
assertThat(b).isEqualTo("ball");
1030+
}
1031+
10041032
private Condition<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
10051033
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {
10061034

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a.property=apple
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b.property=ball

0 commit comments

Comments
 (0)