Add support for wildcard locations for properties and YAML files
Closes gh-19909
This commit is contained in:
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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
a.property=apple
|
||||
|
|
@ -0,0 +1 @@
|
|||
b.property=ball
|
||||
Loading…
Reference in New Issue