diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc index ea63a9bfccec..125fad876740 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -103,25 +103,29 @@ This means that the JSON cannot override properties from lower order property so === External Application Properties [[features.external-config.files]] Spring Boot will automatically find and load `application.properties` and `application.yaml` files from the following locations when your application starts: -. The classpath root -. The classpath `/config` package -. The current directory -. The `/config` subdirectory in the current directory -. Immediate child directories of the `/config` subdirectory +. From the classpath +.. The classpath root +.. The classpath `/config` package +. From the current directory +.. The current directory +.. The `/config` subdirectory in the current directory +.. Immediate child directories of the `/config` subdirectory The list is ordered by precedence (with values from lower items overriding earlier ones). Documents from the loaded files are added as `PropertySources` to the Spring `Environment`. If you do not like `application` as the configuration file name, you can switch to another file name by specifying a configprop:spring.config.name[] environment property. -You can also refer to an explicit location by using the `spring.config.location` environment property (which is a comma-separated list of directory locations or file paths). -The following example shows how to specify a different file name: +For example, to look for `myproject.properties` and `myproject.yaml` files you can run your application as follows: [source,shell,indent=0,subs="verbatim"] ---- $ java -jar myproject.jar --spring.config.name=myproject ---- -The following example shows how to specify two locations: +You can also refer to an explicit location by using the configprop:spring.config.location[] environment property. +This properties accepts a comma-separated list of one or more locations to check. + +The following example shows how to specify two distinct files: [source,shell,indent=0,subs="verbatim"] ---- @@ -135,14 +139,19 @@ TIP: Use the prefix `optional:` if the <>`" section for more details. Locations configured by using `spring.config.location` replace the default locations. For example, if `spring.config.location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete set of locations considered is: @@ -154,11 +163,8 @@ If you prefer to add additional locations, rather than replacing them, you can u Properties loaded from additional locations can override those in the default locations. For example, if `spring.config.additional-location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete set of locations considered is: -. `optional:classpath:/` -. `optional:classpath:/config/` -. `optional:file:./` -. `optional:file:./config/` -. `optional:file:./config/*/` +. `optional:classpath:/;optional:classpath:/config/` +. `optional:file:./;optional:file:./config/;optional:file:./config/*/` . `optional:classpath:custom-config/` . `optional:file:./custom-config/` @@ -219,6 +225,35 @@ Profile-specific properties are loaded from the same locations as standard `appl If several profiles are specified, a last-wins strategy applies. For example, if profiles `prod,live` are specified by the configprop:spring.profiles.active[] property, values in `application-prod.properties` can be overridden by those in `application-live.properties`. +[NOTE] +==== +The last-wins strategy applies at the <> level. +A configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` will not have the same override rules as `classpath:/cfg/;classpath:/ext/`. + +For example, continuing our `prod,live` example above, we might have the following files: + +---- +/cfg + application-live.properties +/ext + application-live.properties + application-prod.properties +---- + +When we have a configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` we process all `/cfg` files before all `/ext` files: + +. `/cfg/application-live.properties` +. `/ext/application-prod.properties` +. `/ext/application-live.properties` + + +When we have `classpath:/cfg/;classpath:/ext/` instead (with a `;` delimiter) we process `/cfg` and `/ext` at the same level: + +. `/ext/application-prod.properties` +. `/cfg/application-live.properties` +. `/ext/application-live.properties` +==== + The `Environment` has a set of default profiles (by default, `[default]`) that are used if no active profiles are set. In other words, if no profiles are explicitly activated, then properties from `application-default` are considered. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java index dd0a9fa9a009..dcdf85d8f005 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java @@ -87,11 +87,8 @@ class ConfigDataEnvironment { static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS; static { List locations = new ArrayList<>(); - locations.add(ConfigDataLocation.of("optional:classpath:/")); - locations.add(ConfigDataLocation.of("optional:classpath:/config/")); - locations.add(ConfigDataLocation.of("optional:file:./")); - locations.add(ConfigDataLocation.of("optional:file:./config/")); - locations.add(ConfigDataLocation.of("optional:file:./config/*/")); + locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/")); + locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/")); DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java index 98c941ba69cf..f1df92af3e6b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java @@ -97,6 +97,32 @@ public Origin getOrigin() { return this.origin; } + /** + * Return an array of {@link ConfigDataLocation} elements built by splitting this + * {@link ConfigDataLocation} around a delimiter of {@code ";"}. + * @return the split locations + * @since 2.4.7 + */ + public ConfigDataLocation[] split() { + return split(";"); + } + + /** + * Return an array of {@link ConfigDataLocation} elements built by splitting this + * {@link ConfigDataLocation} around the specified delimiter. + * @param delimiter the delimiter to split on + * @return the split locations + * @since 2.4.7 + */ + public ConfigDataLocation[] split(String delimiter) { + String[] values = StringUtils.delimitedListToStringArray(toString(), delimiter); + ConfigDataLocation[] result = new ConfigDataLocation[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = of(values[i]).withOrigin(getOrigin()); + } + return result; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index 058c8100a680..a14293ce58f1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -116,7 +116,16 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat @Override public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException { - return resolve(getReferences(context, location)); + return resolve(getReferences(context, location.split())); + } + + private Set getReferences(ConfigDataLocationResolverContext context, + ConfigDataLocation[] configDataLocations) { + Set references = new LinkedHashSet<>(); + for (ConfigDataLocation configDataLocation : configDataLocations) { + references.addAll(getReferences(context, configDataLocation)); + } + return references; } private Set getReferences(ConfigDataLocationResolverContext context, @@ -139,15 +148,17 @@ public List resolveProfileSpecific(ConfigDataLocatio if (context.getParent() != null) { return null; } - return resolve(getProfileSpecificReferences(context, location, profiles)); + return resolve(getProfileSpecificReferences(context, location.split(), profiles)); } private Set getProfileSpecificReferences(ConfigDataLocationResolverContext context, - ConfigDataLocation configDataLocation, Profiles profiles) { + ConfigDataLocation[] configDataLocations, Profiles profiles) { Set references = new LinkedHashSet<>(); - String resourceLocation = getResourceLocation(context, configDataLocation); for (String profile : profiles) { - references.addAll(getReferences(configDataLocation, resourceLocation, profile)); + for (ConfigDataLocation configDataLocation : configDataLocations) { + String resourceLocation = getResourceLocation(context, configDataLocation); + references.addAll(getReferences(configDataLocation, resourceLocation, profile)); + } } return references; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java index 3fce4eb875c9..3a5a969f12f4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java @@ -772,6 +772,21 @@ void runWhenHasProfileSpecificImportWithCustomImportDoesNotResolveProfileSpecifi assertThat(environment.containsProperty("test:boot:ps")).isFalse(); } + @Test // gh-26593 + void runWhenHasFilesInRootAndConfigWithProfiles() { + ConfigurableApplicationContext context = this.application + .run("--spring.config.name=file-in-root-and-config-with-profile", "--spring.profiles.active=p1,p2"); + ConfigurableEnvironment environment = context.getEnvironment(); + assertThat(environment.containsProperty("file-in-root-and-config-with-profile")).isTrue(); + assertThat(environment.containsProperty("file-in-root-and-config-with-profile-p1")).isTrue(); + assertThat(environment.containsProperty("file-in-root-and-config-with-profile-p2")).isTrue(); + assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile")).isTrue(); + assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile-p1")).isTrue(); + assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile-p2")).isTrue(); + assertThat(environment.getProperty("v1")).isEqualTo("config-file-in-root-and-config-with-profile-p2"); + assertThat(environment.getProperty("v2")).isEqualTo("file-in-root-and-config-with-profile-p2"); + } + private Condition matchingPropertySource(final String sourceName) { return new Condition("environment containing property source " + sourceName) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationTests.java index 491ee39d2db3..38d9755b336b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -134,4 +134,36 @@ void ofReturnsLocation() { assertThat(ConfigDataLocation.of("test")).hasToString("test"); } + @Test + void splitWhenNoSemiColonReturnsSingleElement() { + ConfigDataLocation location = ConfigDataLocation.of("test"); + ConfigDataLocation[] split = location.split(); + assertThat(split).containsExactly(ConfigDataLocation.of("test")); + } + + @Test + void splitWhenSemiColonReturnsElements() { + ConfigDataLocation location = ConfigDataLocation.of("one;two;three"); + ConfigDataLocation[] split = location.split(); + assertThat(split).containsExactly(ConfigDataLocation.of("one"), ConfigDataLocation.of("two"), + ConfigDataLocation.of("three")); + } + + @Test + void splitOnCharReturnsElements() { + ConfigDataLocation location = ConfigDataLocation.of("one::two::three"); + ConfigDataLocation[] split = location.split("::"); + assertThat(split).containsExactly(ConfigDataLocation.of("one"), ConfigDataLocation.of("two"), + ConfigDataLocation.of("three")); + } + + @Test + void splitWhenHasOriginReturnsElementsWithOriginSet() { + Origin origin = mock(Origin.class); + ConfigDataLocation location = ConfigDataLocation.of("a;b").withOrigin(origin); + ConfigDataLocation[] split = location.split(); + assertThat(split[0].getOrigin()).isEqualTo(origin); + assertThat(split[1].getOrigin()).isEqualTo(origin); + } + } diff --git a/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p1.properties b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p1.properties new file mode 100644 index 000000000000..ffbd719314e1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p1.properties @@ -0,0 +1,3 @@ +config-file-in-root-and-config-with-profile-p1=true +v1=config-file-in-root-and-config-with-profile-p1 +#v2 intentionally missing \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p2.properties b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p2.properties new file mode 100644 index 000000000000..5ead8d0cd908 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile-p2.properties @@ -0,0 +1,3 @@ +config-file-in-root-and-config-with-profile-p2=true +v1=config-file-in-root-and-config-with-profile-p2 +#v2 intentionally missing \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile.properties b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile.properties new file mode 100644 index 000000000000..556c851a5f22 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/config/file-in-root-and-config-with-profile.properties @@ -0,0 +1,3 @@ +config-file-in-root-and-config-with-profile=true +v1=config-file-in-root-and-config-with-profile +v2=config-file-in-root-and-config-with-profile \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p1.properties b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p1.properties new file mode 100644 index 000000000000..c4de4d2a978d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p1.properties @@ -0,0 +1,3 @@ +file-in-root-and-config-with-profile-p1=true +v1=file-in-root-and-config-with-profile-p1 +v2=file-in-root-and-config-with-profile-p1 diff --git a/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p2.properties b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p2.properties new file mode 100644 index 000000000000..c60d2dafc39b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile-p2.properties @@ -0,0 +1,3 @@ +file-in-root-and-config-with-profile-p2=true +v1=file-in-root-and-config-with-profile-p2 +v2=file-in-root-and-config-with-profile-p2 diff --git a/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile.properties b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile.properties new file mode 100644 index 000000000000..b34ddf5169ba --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/file-in-root-and-config-with-profile.properties @@ -0,0 +1,3 @@ +file-in-root-and-config-with-profile=true +v1=file-in-root-and-config-with-profile +v2=file-in-root-and-config-with-profile