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 13202548c3..0c75801cdf 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 @@ -499,7 +499,8 @@ For example, if you have some Redis configuration and some MySQL configuration, This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`. In such a case, having a wildcard location of `config/*/`, will result in both files being processed. -NOTE: Locations with wildcards are not processed in a deterministic order and files that match the wildcard cannot be used to override keys in the other. +NOTE: A wildcard location must contain only one `*` and end with `*/` for search locations that are directories or `*/` for search locations that are files. +Locations with wildcards are sorted alphabetically based on the absolute path of the file names. [[boot-features-external-config-application-json]] 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 78b093816d..cdea50f407 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 @@ -16,10 +16,12 @@ package org.springframework.boot.context.config; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -28,9 +30,12 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -60,9 +65,9 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.Profiles; import org.springframework.core.env.PropertySource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; 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.ResourcePatternResolver; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; @@ -163,6 +168,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, private static final Resource[] EMPTY_RESOURCES = {}; + private static final Comparator FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath); + private String searchLocations; private String names; @@ -304,8 +311,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, private final ResourceLoader resourceLoader; - private final PathMatchingResourcePatternResolver patternResolver; - private final List propertySourceLoaders; private Deque profiles; @@ -325,7 +330,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, : new DefaultResourceLoader(getClass().getClassLoader()); this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); - this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader); } void load() { @@ -555,9 +559,22 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, private Resource[] getResources(String location) { try { - return this.patternResolver.getResources(location); + if (location.contains("*")) { + String directoryPath = location.substring(0, location.indexOf("*/")); + String fileName = location.substring(location.lastIndexOf("/") + 1); + Resource resource = this.resourceLoader.getResource(directoryPath); + File[] files = resource.getFile().listFiles(File::isDirectory); + if (files != null) { + Arrays.sort(files, FILE_COMPARATOR); + return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName))) + .filter(Objects::nonNull).flatMap((Function>) Arrays::stream) + .map(FileSystemResource::new).toArray(Resource[]::new); + } + return EMPTY_RESOURCES; + } + return new Resource[] { this.resourceLoader.getResource(location) }; } - catch (IOException ex) { + catch (Exception ex) { return EMPTY_RESOURCES; } } @@ -658,7 +675,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, if (!path.contains("$")) { path = StringUtils.cleanPath(path); Assert.state(!path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX), - "Classpath wildard patterns cannot be used as a search location"); + "Classpath wildcard patterns cannot be used as a search location"); + validateWildcardLocation(path); if (!ResourceUtils.isUrl(path)) { path = ResourceUtils.FILE_URL_PREFIX + path; } @@ -669,6 +687,15 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, return locations; } + private void validateWildcardLocation(String path) { + if (path.contains("*")) { + Assert.state(StringUtils.countOccurrencesOf(path, "*") == 1, + "Wildard pattern with multiple '*'s cannot be used as search location"); + String directoryPath = path.substring(0, path.lastIndexOf("/") + 1); + Assert.state(directoryPath.endsWith("*/"), "Wildcard patterns must end with '*/'"); + } + } + private Set getSearchNames() { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); 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 06f0e574b0..65931c9ca7 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 @@ -716,7 +716,7 @@ class ConfigFileApplicationListenerTests { "spring.config.location=classpath*:override.properties"); assertThatIllegalStateException() .isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application)) - .withMessage("Classpath wildard patterns cannot be used as a search location"); + .withMessage("Classpath wildcard patterns cannot be used as a search location"); } @Test @@ -1032,6 +1032,36 @@ class ConfigFileApplicationListenerTests { this.initializer.postProcessEnvironment(this.environment, this.application); } + @Test + void directoryLocationsWithWildcardShouldHaveWildcardAsLastCharacterBeforeSlash() { + String location = "file:src/test/resources/*/config/"; + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, + "spring.config.location=" + location); + assertThatIllegalStateException() + .isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application)) + .withMessage("Wildcard patterns must end with '*/'"); + } + + @Test + void directoryLocationsWithMultipleWildcardsShouldThrowException() { + String location = "file:src/test/resources/config/**/"; + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, + "spring.config.location=" + location); + assertThatIllegalStateException() + .isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application)) + .withMessage("Wildard pattern with multiple '*'s cannot be used as search location"); + } + + @Test + void locationsWithWildcardDirectoriesShouldRestrictToOneLevelDeep() { + 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); + assertThat(this.environment.getProperty("third.property")).isNull(); + } + @Test void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() { String location = "file:src/test/resources/config/*/"; @@ -1045,6 +1075,22 @@ class ConfigFileApplicationListenerTests { assertThat(second).isEqualTo("ball"); } + @Test + void locationsWithWildcardDirectoriesShouldSortAlphabeticallyBasedOnAbsolutePath() { + 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); + List sources = this.environment.getPropertySources().stream() + .filter((source) -> source.getName().contains("applicationConfig")).map((source) -> { + String name = source.getName(); + return name.substring(name.indexOf("src/test/resources")); + }).collect(Collectors.toList()); + assertThat(sources).containsExactly("src/test/resources/config/1-first/testproperties.properties]]", + "src/test/resources/config/2-second/testproperties.properties]]"); + } + @Test void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() { String location = "file:src/test/resources/config/*/testproperties.properties"; diff --git a/spring-boot-project/spring-boot/src/test/resources/config/nested/3-third/testproperties.properties b/spring-boot-project/spring-boot/src/test/resources/config/nested/3-third/testproperties.properties new file mode 100644 index 0000000000..d00e7f0a62 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/config/nested/3-third/testproperties.properties @@ -0,0 +1 @@ +third.property=shouldnotbefound