Skip to content

Commit

Permalink
Fix property ordering within '.' and '/config'
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Jun 7, 2021
1 parent c0cbef9 commit 7396e1e
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 28 deletions.
Expand Up @@ -606,25 +606,29 @@ This means that the JSON cannot override properties from lower order property so
=== External Application Properties [[boot-features-external-config-application-property-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:

[indent=0]
----
$ 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:

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

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).
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.
Files specified in `spring.config.location` are used as-is.
Whether specified directly or contained in a directory, configuration files must include a file extension in their name.
Typical extensions that are supported out-of-the-box are `.properties`, `.yaml`, and `.yml`.

When multiple locations are specified, the later ones can override the values of earlier ones.
In most situations, each configprop:spring.config.location[] item you add will reference a single file or directory.
Locations are processed in the order that they are defined and later ones can override the values of earlier ones.

[[boot-features-external-config-files-location-groups]]
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.
A location group is a collection of locations that are all considered at the same level.
For example, you might want to group all classpath locations, then all external locations.
Items within a location group should be separated with `;`.
See the example in the "`<<boot-features-external-config-files-profile-specific>>`" 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:
Expand All @@ -653,11 +664,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/`

Expand Down Expand Up @@ -718,6 +726,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 <<boot-features-external-config-files-location-groups,location group>> 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.

Expand Down
Expand Up @@ -87,11 +87,8 @@ class ConfigDataEnvironment {
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {
List<ConfigDataLocation> 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]);
}

Expand Down
Expand Up @@ -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) {
Expand Down
Expand Up @@ -115,7 +115,16 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) throws ConfigDataNotFoundException {
return resolve(getReferences(context, location));
return resolve(getReferences(context, location.split()));
}

private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation[] configDataLocations) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (ConfigDataLocation configDataLocation : configDataLocations) {
references.addAll(getReferences(context, configDataLocation));
}
return references;
}

private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
Expand All @@ -138,15 +147,17 @@ public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocatio
if (context.getParent() != null) {
return null;
}
return resolve(getProfileSpecificReferences(context, location, profiles));
return resolve(getProfileSpecificReferences(context, location.split(), profiles));
}

private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation, Profiles profiles) {
ConfigDataLocation[] configDataLocations, Profiles profiles) {
Set<StandardConfigDataReference> 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;
}
Expand Down
Expand Up @@ -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<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {

Expand Down
@@ -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.
Expand Down Expand Up @@ -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);
}

}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

0 comments on commit 7396e1e

Please sign in to comment.