Add support for wildcard locations for properties and YAML files

Closes gh-19909
pull/20032/head
Madhura Bhave 5 years ago
parent de1a26cf35
commit e64a145ef0

@ -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:/`

@ -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:
* <ul>
* <li>file:./config/</li>
* <li>file:./config/{@literal *}/</li>
* <li>file:./</li>
* <li>classpath:config/</li>
* <li>classpath:</li>
@ -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<PropertySourceLoader> propertySourceLoaders;
private Deque<Profile> 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<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);
String name = (location.contains("*")) ? "applicationConfig: [" + resource.toString() + "]"
: "applicationConfig: [" + location + "]";
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;
}
return;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
List<Document> 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<Profile> includeProfiles) {
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear();

@ -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<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {

Loading…
Cancel
Save