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`. 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"`). 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]] [[boot-features-external-config-application-json]]
[TIP] [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. 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. 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: The resulting search order is the following:
. `file:./config/` . `file:./config/`
. `file:./config/*/`
. `file:./` . `file:./`
. `classpath:/config/` . `classpath:/config/`
. `classpath:/` . `classpath:/`
@ -513,6 +518,7 @@ For example, if additional locations of `classpath:/custom-config/,file:./custom
. `file:./custom-config/` . `file:./custom-config/`
. `classpath:custom-config/` . `classpath:custom-config/`
. `file:./config/` . `file:./config/`
. `file:./config/*/`
. `file:./` . `file:./`
. `classpath:/config/` . `classpath:/config/`
. `classpath:/` . `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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; 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: * 'application.properties' and/or 'application.yml' files in the following locations:
* <ul> * <ul>
* <li>file:./config/</li> * <li>file:./config/</li>
* <li>file:./config/{@literal *}/</li>
* <li>file:./</li> * <li>file:./</li>
* <li>classpath:config/</li> * <li>classpath:config/</li>
* <li>classpath:</li> * <li>classpath:</li>
@ -107,7 +109,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private static final String DEFAULT_PROPERTIES = "defaultProperties"; private static final String DEFAULT_PROPERTIES = "defaultProperties";
// Note the order is from least to most specific (last one wins) // 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"; private static final String DEFAULT_NAMES = "application";
@ -158,6 +160,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final DeferredLog logger = new DeferredLog(); private final DeferredLog logger = new DeferredLog();
private static final Resource[] EMPTY_RESOURCES = {};
private String searchLocations; private String searchLocations;
private String names; private String names;
@ -299,6 +303,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final ResourceLoader resourceLoader; private final ResourceLoader resourceLoader;
private final PathMatchingResourcePatternResolver patternResolver;
private final List<PropertySourceLoader> propertySourceLoaders; private final List<PropertySourceLoader> propertySourceLoaders;
private Deque<Profile> profiles; private Deque<Profile> profiles;
@ -317,6 +323,7 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(); this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader()); getClass().getClassLoader());
this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
} }
void load() { void load() {
@ -497,47 +504,51 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) { DocumentConsumer consumer) {
try { try {
Resource resource = this.resourceLoader.getResource(location); Resource[] resources = getResources(location);
if (resource == null || !resource.exists()) { for (Resource resource : resources) {
if (this.logger.isTraceEnabled()) { if (resource == null || !resource.exists()) {
StringBuilder description = getDescription("Skipped missing config ", location, resource, if (this.logger.isTraceEnabled()) {
profile); StringBuilder description = getDescription("Skipped missing config ", location, resource,
this.logger.trace(description); profile);
this.logger.trace(description);
}
return;
} }
return; if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
} if (this.logger.isTraceEnabled()) {
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) { StringBuilder description = getDescription("Skipped empty config extension ", location,
if (this.logger.isTraceEnabled()) { resource, profile);
StringBuilder description = getDescription("Skipped empty config extension ", location, this.logger.trace(description);
resource, profile); }
this.logger.trace(description); return;
} }
return; String name = (location.contains("*")) ? "applicationConfig: [" + resource.toString() + "]"
} : "applicationConfig: [" + location + "]";
String name = "applicationConfig: [" + location + "]"; List<Document> documents = loadDocuments(loader, name, resource);
List<Document> documents = loadDocuments(loader, name, resource); if (CollectionUtils.isEmpty(documents)) {
if (CollectionUtils.isEmpty(documents)) { if (this.logger.isTraceEnabled()) {
if (this.logger.isTraceEnabled()) { StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
StringBuilder description = getDescription("Skipped unloaded config ", location, resource, profile);
profile); this.logger.trace(description);
this.logger.trace(description); }
return;
} }
return; List<Document> loaded = new ArrayList<>();
} for (Document document : documents) {
List<Document> loaded = new ArrayList<>(); if (filter.match(document)) {
for (Document document : documents) { addActiveProfiles(document.getActiveProfiles());
if (filter.match(document)) { addIncludedProfiles(document.getIncludeProfiles());
addActiveProfiles(document.getActiveProfiles()); loaded.add(document);
addIncludedProfiles(document.getIncludeProfiles()); }
loaded.add(document);
} }
} Collections.reverse(loaded);
Collections.reverse(loaded); if (!loaded.isEmpty()) {
if (!loaded.isEmpty()) { loaded.forEach((document) -> consumer.accept(profile, document));
loaded.forEach((document) -> consumer.accept(profile, document)); if (this.logger.isDebugEnabled()) {
if (this.logger.isDebugEnabled()) { StringBuilder description = getDescription("Loaded config file ", location, resource,
StringBuilder description = getDescription("Loaded config file ", location, resource, profile); profile);
this.logger.debug(description); 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) { private void addIncludedProfiles(Set<Profile> includeProfiles) {
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles); LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear(); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.SimpleCommandLinePropertySource;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.context.support.TestPropertySourceUtils;
@ -73,6 +74,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Phillip Webb * @author Phillip Webb
* @author Dave Syer * @author Dave Syer
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Madhura Bhave
*/ */
@ExtendWith(OutputCaptureExtension.class) @ExtendWith(OutputCaptureExtension.class)
class ConfigFileApplicationListenerTests { class ConfigFileApplicationListenerTests {
@ -109,7 +111,7 @@ class ConfigFileApplicationListenerTests {
} }
}; };
} }
return null; return new ClassPathResource("doesnotexist");
} }
@Override @Override
@ -1001,6 +1003,32 @@ class ConfigFileApplicationListenerTests {
this.initializer.postProcessEnvironment(this.environment, this.application); 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) { private Condition<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) { return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {

Loading…
Cancel
Save