Skip to content

Commit 7396e1e

Browse files
committed
Fix property ordering within '.' and '/config'
Allow groups to be used with standard locations so that order of profile-specific files is consistent. Prior to this commit, the default search locations considered for application properties/yaml files was the following: optional:classpath:/ optional:classpath:/config/ optional:file:./ optional:file:./config/ optional:file:./config/*/ Each of these locations was independent which could cause confusion if certain combinations were used. For example, if profile-specific files were added to `classpath:/` and `classpath:/config/` then the latter would always override the former regardless of the profile ordering. This commit updates `StandardConfigDataLocationResolver` so that a group of locations can be specified for each item. This allows us to define the following set of search locations which provide more logical ordering for profile-specific files optional:classpath:/;optional:classpath:/config/ optional:file:./;optional:file:./config/;optional:file:./config/*/ Closes gh-26593
1 parent c0cbef9 commit 7396e1e

12 files changed

+164
-28
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -606,25 +606,29 @@ This means that the JSON cannot override properties from lower order property so
606606
=== External Application Properties [[boot-features-external-config-application-property-files]]
607607
Spring Boot will automatically find and load `application.properties` and `application.yaml` files from the following locations when your application starts:
608608

609-
. The classpath root
610-
. The classpath `/config` package
611-
. The current directory
612-
. The `/config` subdirectory in the current directory
613-
. Immediate child directories of the `/config` subdirectory
609+
. From the classpath
610+
.. The classpath root
611+
.. The classpath `/config` package
612+
. From the current directory
613+
.. The current directory
614+
.. The `/config` subdirectory in the current directory
615+
.. Immediate child directories of the `/config` subdirectory
614616

615617
The list is ordered by precedence (with values from lower items overriding earlier ones).
616618
Documents from the loaded files are added as `PropertySources` to the Spring `Environment`.
617619

618620
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.
619-
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).
620-
The following example shows how to specify a different file name:
621+
For example, to look for `myproject.properties` and `myproject.yaml` files you can run your application as follows:
621622

622623
[indent=0]
623624
----
624625
$ java -jar myproject.jar --spring.config.name=myproject
625626
----
626627

627-
The following example shows how to specify two locations:
628+
You can also refer to an explicit location by using the configprop:spring.config.location[] environment property.
629+
This properties accepts a comma-separated list of one or more locations to check.
630+
631+
The following example shows how to specify two distinct files:
628632

629633
[indent=0]
630634
----
@@ -636,12 +640,19 @@ TIP: Use the prefix `optional:` if the <<boot-features-external-config-optional-
636640
WARNING: `spring.config.name`, `spring.config.location`, and `spring.config.additional-location` are used very early to determine which files have to be loaded.
637641
They must be defined as an environment property (typically an OS environment variable, a system property, or a command-line argument).
638642

639-
If `spring.config.location` contains directories (as opposed to files), they should end in `/` (at runtime they will be appended with the names generated from `spring.config.name` before being loaded).
643+
If `spring.config.location` contains directories (as opposed to files), they should end in `/`.
644+
At runtime they will be appended with the names generated from `spring.config.name` before being loaded.
640645
Files specified in `spring.config.location` are used as-is.
641-
Whether specified directly or contained in a directory, configuration files must include a file extension in their name.
642-
Typical extensions that are supported out-of-the-box are `.properties`, `.yaml`, and `.yml`.
643646

644-
When multiple locations are specified, the later ones can override the values of earlier ones.
647+
In most situations, each configprop:spring.config.location[] item you add will reference a single file or directory.
648+
Locations are processed in the order that they are defined and later ones can override the values of earlier ones.
649+
650+
[[boot-features-external-config-files-location-groups]]
651+
If you have a complex location setup, and you use profile-specific configuration files, you may need to provide further hints so that Spring Boot knows how they should be grouped.
652+
A location group is a collection of locations that are all considered at the same level.
653+
For example, you might want to group all classpath locations, then all external locations.
654+
Items within a location group should be separated with `;`.
655+
See the example in the "`<<boot-features-external-config-files-profile-specific>>`" section for more details.
645656

646657
Locations configured by using `spring.config.location` replace the default locations.
647658
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:
@@ -653,11 +664,8 @@ If you prefer to add additional locations, rather than replacing them, you can u
653664
Properties loaded from additional locations can override those in the default locations.
654665
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:
655666

656-
. `optional:classpath:/`
657-
. `optional:classpath:/config/`
658-
. `optional:file:./`
659-
. `optional:file:./config/`
660-
. `optional:file:./config/*/`
667+
. `optional:classpath:/;optional:classpath:/config/`
668+
. `optional:file:./;optional:file:./config/;optional:file:./config/*/`
661669
. `optional:classpath:custom-config/`
662670
. `optional:file:./custom-config/`
663671

@@ -718,6 +726,35 @@ Profile-specific properties are loaded from the same locations as standard `appl
718726
If several profiles are specified, a last-wins strategy applies.
719727
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`.
720728

729+
[NOTE]
730+
====
731+
The last-wins strategy applies at the <<boot-features-external-config-files-location-groups,location group>> level.
732+
A configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` will not have the same override rules as `classpath:/cfg/;classpath:/ext/`.
733+
734+
For example, continuing our `prod,live` example above, we might have the following files:
735+
736+
----
737+
/cfg
738+
application-live.properties
739+
/ext
740+
application-live.properties
741+
application-prod.properties
742+
----
743+
744+
When we have a configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` we process all `/cfg` files before all `/ext` files:
745+
746+
. `/cfg/application-live.properties`
747+
. `/ext/application-prod.properties`
748+
. `/ext/application-live.properties`
749+
750+
751+
When we have `classpath:/cfg/;classpath:/ext/` instead (with a `;` delimiter) we process `/cfg` and `/ext` at the same level:
752+
753+
. `/ext/application-prod.properties`
754+
. `/cfg/application-live.properties`
755+
. `/ext/application-live.properties`
756+
====
757+
721758
The `Environment` has a set of default profiles (by default, `[default]`) that are used if no active profiles are set.
722759
In other words, if no profiles are explicitly activated, then properties from `application-default` are considered.
723760

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,8 @@ class ConfigDataEnvironment {
8787
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
8888
static {
8989
List<ConfigDataLocation> locations = new ArrayList<>();
90-
locations.add(ConfigDataLocation.of("optional:classpath:/"));
91-
locations.add(ConfigDataLocation.of("optional:classpath:/config/"));
92-
locations.add(ConfigDataLocation.of("optional:file:./"));
93-
locations.add(ConfigDataLocation.of("optional:file:./config/"));
94-
locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
90+
locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));
91+
locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));
9592
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
9693
}
9794

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ public Origin getOrigin() {
9797
return this.origin;
9898
}
9999

100+
/**
101+
* Return an array of {@link ConfigDataLocation} elements built by splitting this
102+
* {@link ConfigDataLocation} around a delimiter of {@code ";"}.
103+
* @return the split locations
104+
* @since 2.4.7
105+
*/
106+
public ConfigDataLocation[] split() {
107+
return split(";");
108+
}
109+
110+
/**
111+
* Return an array of {@link ConfigDataLocation} elements built by splitting this
112+
* {@link ConfigDataLocation} around the specified delimiter.
113+
* @param delimiter the delimiter to split on
114+
* @return the split locations
115+
* @since 2.4.7
116+
*/
117+
public ConfigDataLocation[] split(String delimiter) {
118+
String[] values = StringUtils.delimitedListToStringArray(toString(), delimiter);
119+
ConfigDataLocation[] result = new ConfigDataLocation[values.length];
120+
for (int i = 0; i < values.length; i++) {
121+
result[i] = of(values[i]).withOrigin(getOrigin());
122+
}
123+
return result;
124+
}
125+
100126
@Override
101127
public boolean equals(Object obj) {
102128
if (this == obj) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,16 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat
115115
@Override
116116
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
117117
ConfigDataLocation location) throws ConfigDataNotFoundException {
118-
return resolve(getReferences(context, location));
118+
return resolve(getReferences(context, location.split()));
119+
}
120+
121+
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
122+
ConfigDataLocation[] configDataLocations) {
123+
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
124+
for (ConfigDataLocation configDataLocation : configDataLocations) {
125+
references.addAll(getReferences(context, configDataLocation));
126+
}
127+
return references;
119128
}
120129

121130
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
@@ -138,15 +147,17 @@ public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocatio
138147
if (context.getParent() != null) {
139148
return null;
140149
}
141-
return resolve(getProfileSpecificReferences(context, location, profiles));
150+
return resolve(getProfileSpecificReferences(context, location.split(), profiles));
142151
}
143152

144153
private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
145-
ConfigDataLocation configDataLocation, Profiles profiles) {
154+
ConfigDataLocation[] configDataLocations, Profiles profiles) {
146155
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
147-
String resourceLocation = getResourceLocation(context, configDataLocation);
148156
for (String profile : profiles) {
149-
references.addAll(getReferences(configDataLocation, resourceLocation, profile));
157+
for (ConfigDataLocation configDataLocation : configDataLocations) {
158+
String resourceLocation = getResourceLocation(context, configDataLocation);
159+
references.addAll(getReferences(configDataLocation, resourceLocation, profile));
160+
}
150161
}
151162
return references;
152163
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,21 @@ void runWhenHasProfileSpecificImportWithCustomImportDoesNotResolveProfileSpecifi
772772
assertThat(environment.containsProperty("test:boot:ps")).isFalse();
773773
}
774774

775+
@Test // gh-26593
776+
void runWhenHasFilesInRootAndConfigWithProfiles() {
777+
ConfigurableApplicationContext context = this.application
778+
.run("--spring.config.name=file-in-root-and-config-with-profile", "--spring.profiles.active=p1,p2");
779+
ConfigurableEnvironment environment = context.getEnvironment();
780+
assertThat(environment.containsProperty("file-in-root-and-config-with-profile")).isTrue();
781+
assertThat(environment.containsProperty("file-in-root-and-config-with-profile-p1")).isTrue();
782+
assertThat(environment.containsProperty("file-in-root-and-config-with-profile-p2")).isTrue();
783+
assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile")).isTrue();
784+
assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile-p1")).isTrue();
785+
assertThat(environment.containsProperty("config-file-in-root-and-config-with-profile-p2")).isTrue();
786+
assertThat(environment.getProperty("v1")).isEqualTo("config-file-in-root-and-config-with-profile-p2");
787+
assertThat(environment.getProperty("v2")).isEqualTo("file-in-root-and-config-with-profile-p2");
788+
}
789+
775790
private Condition<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
776791
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {
777792

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationTests.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -134,4 +134,36 @@ void ofReturnsLocation() {
134134
assertThat(ConfigDataLocation.of("test")).hasToString("test");
135135
}
136136

137+
@Test
138+
void splitWhenNoSemiColonReturnsSingleElement() {
139+
ConfigDataLocation location = ConfigDataLocation.of("test");
140+
ConfigDataLocation[] split = location.split();
141+
assertThat(split).containsExactly(ConfigDataLocation.of("test"));
142+
}
143+
144+
@Test
145+
void splitWhenSemiColonReturnsElements() {
146+
ConfigDataLocation location = ConfigDataLocation.of("one;two;three");
147+
ConfigDataLocation[] split = location.split();
148+
assertThat(split).containsExactly(ConfigDataLocation.of("one"), ConfigDataLocation.of("two"),
149+
ConfigDataLocation.of("three"));
150+
}
151+
152+
@Test
153+
void splitOnCharReturnsElements() {
154+
ConfigDataLocation location = ConfigDataLocation.of("one::two::three");
155+
ConfigDataLocation[] split = location.split("::");
156+
assertThat(split).containsExactly(ConfigDataLocation.of("one"), ConfigDataLocation.of("two"),
157+
ConfigDataLocation.of("three"));
158+
}
159+
160+
@Test
161+
void splitWhenHasOriginReturnsElementsWithOriginSet() {
162+
Origin origin = mock(Origin.class);
163+
ConfigDataLocation location = ConfigDataLocation.of("a;b").withOrigin(origin);
164+
ConfigDataLocation[] split = location.split();
165+
assertThat(split[0].getOrigin()).isEqualTo(origin);
166+
assertThat(split[1].getOrigin()).isEqualTo(origin);
167+
}
168+
137169
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config-file-in-root-and-config-with-profile-p1=true
2+
v1=config-file-in-root-and-config-with-profile-p1
3+
#v2 intentionally missing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config-file-in-root-and-config-with-profile-p2=true
2+
v1=config-file-in-root-and-config-with-profile-p2
3+
#v2 intentionally missing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config-file-in-root-and-config-with-profile=true
2+
v1=config-file-in-root-and-config-with-profile
3+
v2=config-file-in-root-and-config-with-profile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
file-in-root-and-config-with-profile-p1=true
2+
v1=file-in-root-and-config-with-profile-p1
3+
v2=file-in-root-and-config-with-profile-p1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
file-in-root-and-config-with-profile-p2=true
2+
v1=file-in-root-and-config-with-profile-p2
3+
v2=file-in-root-and-config-with-profile-p2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
file-in-root-and-config-with-profile=true
2+
v1=file-in-root-and-config-with-profile
3+
v2=file-in-root-and-config-with-profile

0 commit comments

Comments
 (0)