diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc
index 845a77b1a57..1e90297dc54 100644
--- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc
+++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc
@@ -396,6 +396,10 @@ On your application classpath (for example, inside your jar) you can have an `ap
When running in a new environment, an `application.properties` file can be provided outside of your jar that overrides the `name`.
For one-off testing, you can launch with a specific command line switch (for example, `java -jar app.jar --name="Spring"`).
+NOTE: Spring Boot also supports wildcard locations when loading configuration files.
+By default, a wildcard location of `config/*/` outside of your jar is supported.
+Wildcard locations are also supported when specifying `spring.config.additional-location` and `spring.config.location`.
+
[[boot-features-external-config-application-json]]
[TIP]
====
@@ -492,10 +496,11 @@ If `spring.config.location` contains directories (as opposed to files), they sho
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.
Config locations are searched in reverse order.
-By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/`.
+By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/`.
The resulting search order is the following:
. `file:./config/`
+. `file:./config/*/`
. `file:./`
. `classpath:/config/`
. `classpath:/`
@@ -513,6 +518,7 @@ For example, if additional locations of `classpath:/custom-config/,file:./custom
. `file:./custom-config/`
. `classpath:custom-config/`
. `file:./config/`
+. `file:./config/*/`
. `file:./`
. `classpath:/config/`
. `classpath:/`
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java
index b33937b87a7..897b3cc61e7 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -75,6 +76,7 @@ import org.springframework.util.StringUtils;
* 'application.properties' and/or 'application.yml' files in the following locations:
*
* - file:./config/
+ * - file:./config/{@literal *}/
* - file:./
* - classpath:config/
* - classpath:
@@ -107,7 +109,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private static final String DEFAULT_PROPERTIES = "defaultProperties";
// Note the order is from least to most specific (last one wins)
- private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
+ private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
private static final String DEFAULT_NAMES = "application";
@@ -158,6 +160,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final DeferredLog logger = new DeferredLog();
+ private static final Resource[] EMPTY_RESOURCES = {};
+
private String searchLocations;
private String names;
@@ -299,6 +303,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final ResourceLoader resourceLoader;
+ private final PathMatchingResourcePatternResolver patternResolver;
+
private final List propertySourceLoaders;
private Deque profiles;
@@ -317,6 +323,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
+ this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
}
void load() {
@@ -497,47 +504,51 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
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);
+ Resource[] resources = getResources(location);
+ for (Resource resource : resources) {
+ if (resource == null || !resource.exists()) {
+ if (this.logger.isTraceEnabled()) {
+ StringBuilder description = getDescription("Skipped missing config ", location, resource,
+ profile);
+ this.logger.trace(description);
+ }
+ return;
}
- 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);
+ 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;
}
- 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);
+ String name = (location.contains("*")) ? "applicationConfig: [" + resource.toString() + "]"
+ : "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;
}
- return;
- }
- List loaded = new ArrayList<>();
- for (Document document : documents) {
- if (filter.match(document)) {
- addActiveProfiles(document.getActiveProfiles());
- addIncludedProfiles(document.getIncludeProfiles());
- loaded.add(document);
+ 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);
+ 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);
+ }
}
}
}
@@ -546,6 +557,15 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
}
}
+ private Resource[] getResources(String location) {
+ try {
+ return this.patternResolver.getResources(location);
+ }
+ catch (IOException ex) {
+ return EMPTY_RESOURCES;
+ }
+ }
+
private void addIncludedProfiles(Set includeProfiles) {
LinkedList existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear();
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java
index 47fc08b96f3..3feb089fe85 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,6 +58,7 @@ import org.springframework.core.env.Profiles;
import org.springframework.core.env.SimpleCommandLinePropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.support.TestPropertySourceUtils;
@@ -73,6 +74,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Phillip Webb
* @author Dave Syer
* @author EddĂș MelĂ©ndez
+ * @author Madhura Bhave
*/
@ExtendWith(OutputCaptureExtension.class)
class ConfigFileApplicationListenerTests {
@@ -109,7 +111,7 @@ class ConfigFileApplicationListenerTests {
}
};
}
- return null;
+ return new ClassPathResource("doesnotexist");
}
@Override
@@ -1001,6 +1003,32 @@ class ConfigFileApplicationListenerTests {
this.initializer.postProcessEnvironment(this.environment, this.application);
}
+ @Test
+ void locationsWithWildcardFoldersShouldLoadAllFilesThatMatch() {
+ String location = "file:src/test/resources/config/*/";
+ TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
+ "spring.config.location=" + location);
+ this.initializer.setSearchNames("testproperties");
+ this.initializer.postProcessEnvironment(this.environment, this.application);
+ String a = this.environment.getProperty("a.property");
+ String b = this.environment.getProperty("b.property");
+ assertThat(a).isEqualTo("apple");
+ assertThat(b).isEqualTo("ball");
+ }
+
+ @Test
+ void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() {
+ String location = "file:src/test/resources/config/*/testproperties.properties";
+ TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
+ "spring.config.location=" + location);
+ this.initializer.setSearchNames("testproperties");
+ this.initializer.postProcessEnvironment(this.environment, this.application);
+ String a = this.environment.getProperty("a.property");
+ String b = this.environment.getProperty("b.property");
+ assertThat(a).isEqualTo("apple");
+ assertThat(b).isEqualTo("ball");
+ }
+
private Condition matchingPropertySource(final String sourceName) {
return new Condition("environment containing property source " + sourceName) {
diff --git a/spring-boot-project/spring-boot/src/test/resources/config/a/testproperties.properties b/spring-boot-project/spring-boot/src/test/resources/config/a/testproperties.properties
new file mode 100644
index 00000000000..12816704d31
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/resources/config/a/testproperties.properties
@@ -0,0 +1 @@
+a.property=apple
\ No newline at end of file
diff --git a/spring-boot-project/spring-boot/src/test/resources/config/b/testproperties.properties b/spring-boot-project/spring-boot/src/test/resources/config/b/testproperties.properties
new file mode 100644
index 00000000000..2db7c628437
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/resources/config/b/testproperties.properties
@@ -0,0 +1 @@
+b.property=ball
\ No newline at end of file