Commit abd92678 authored by Phillip Webb's avatar Phillip Webb

Merge branch '2.4.x'

Closes gh-26782
parents e45b1b65 7396e1e7
...@@ -103,25 +103,29 @@ This means that the JSON cannot override properties from lower order property so ...@@ -103,25 +103,29 @@ This means that the JSON cannot override properties from lower order property so
=== External Application Properties [[features.external-config.files]] === 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: Spring Boot will automatically find and load `application.properties` and `application.yaml` files from the following locations when your application starts:
. The classpath root . From the classpath
. The classpath `/config` package .. The classpath root
. The current directory .. The classpath `/config` package
. The `/config` subdirectory in the current directory . From the current directory
. Immediate child directories of the `/config` subdirectory .. 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). 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`. 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. 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). For example, to look for `myproject.properties` and `myproject.yaml` files you can run your application as follows:
The following example shows how to specify a different file name:
[source,shell,indent=0,subs="verbatim"] [source,shell,indent=0,subs="verbatim"]
---- ----
$ java -jar myproject.jar --spring.config.name=myproject $ 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"] [source,shell,indent=0,subs="verbatim"]
---- ----
...@@ -135,14 +139,19 @@ TIP: Use the prefix `optional:` if the <<features#features.external-config.files ...@@ -135,14 +139,19 @@ TIP: Use the prefix `optional:` if the <<features#features.external-config.files
WARNING: `spring.config.name`, `spring.config.location`, and `spring.config.additional-location` are used very early to determine which files have to be loaded. 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). 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 must end in `/` or the system-dependent `File.separator`. 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. At runtime they will be appended with the names generated from `spring.config.name` before being loaded.
If `spring.config.location` contains files, they are used as-is. Files specified in `spring.config.location` are used as-is.
Whether specified directly or contained in a directory, file references must include a file extension in their name. In most situations, each configprop:spring.config.location[] item you add will reference a single file or directory.
Typical extensions that are supported out-of-the-box are `.properties`, `.yaml`, and `.yml`. Locations are processed in the order that they are defined and later ones can override the values of earlier ones.
When multiple locations are specified, the later ones can override the values of earlier ones. [[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 "`<<features.external-config.files.profile-specific>>`" section for more details.
Locations configured by using `spring.config.location` replace the default locations. 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: 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 ...@@ -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. 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: 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:/;optional:classpath:/config/`
. `optional:classpath:/config/` . `optional:file:./;optional:file:./config/;optional:file:./config/*/`
. `optional:file:./`
. `optional:file:./config/`
. `optional:file:./config/*/`
. `optional:classpath:custom-config/` . `optional:classpath:custom-config/`
. `optional:file:./custom-config/` . `optional:file:./custom-config/`
...@@ -219,6 +225,35 @@ Profile-specific properties are loaded from the same locations as standard `appl ...@@ -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. 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`. 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 <<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. 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. In other words, if no profiles are explicitly activated, then properties from `application-default` are considered.
......
...@@ -87,11 +87,8 @@ class ConfigDataEnvironment { ...@@ -87,11 +87,8 @@ class ConfigDataEnvironment {
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS; static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static { static {
List<ConfigDataLocation> locations = new ArrayList<>(); List<ConfigDataLocation> locations = new ArrayList<>();
locations.add(ConfigDataLocation.of("optional:classpath:/")); locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));
locations.add(ConfigDataLocation.of("optional:classpath:/config/")); locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));
locations.add(ConfigDataLocation.of("optional:file:./"));
locations.add(ConfigDataLocation.of("optional:file:./config/"));
locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]); DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
} }
......
...@@ -97,6 +97,32 @@ public final class ConfigDataLocation implements OriginProvider { ...@@ -97,6 +97,32 @@ public final class ConfigDataLocation implements OriginProvider {
return this.origin; 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 @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj) {
......
...@@ -116,7 +116,16 @@ public class StandardConfigDataLocationResolver ...@@ -116,7 +116,16 @@ public class StandardConfigDataLocationResolver
@Override @Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) throws ConfigDataNotFoundException { 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, private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
...@@ -139,15 +148,17 @@ public class StandardConfigDataLocationResolver ...@@ -139,15 +148,17 @@ public class StandardConfigDataLocationResolver
if (context.getParent() != null) { if (context.getParent() != null) {
return null; return null;
} }
return resolve(getProfileSpecificReferences(context, location, profiles)); return resolve(getProfileSpecificReferences(context, location.split(), profiles));
} }
private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context, private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation, Profiles profiles) { ConfigDataLocation[] configDataLocations, Profiles profiles) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>(); Set<StandardConfigDataReference> references = new LinkedHashSet<>();
String resourceLocation = getResourceLocation(context, configDataLocation);
for (String profile : profiles) { 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; return references;
} }
......
...@@ -772,6 +772,21 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests { ...@@ -772,6 +772,21 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
assertThat(environment.containsProperty("test:boot:ps")).isFalse(); 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) { 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) {
......
/* /*
* 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"); * 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.
...@@ -134,4 +134,36 @@ class ConfigDataLocationTests { ...@@ -134,4 +134,36 @@ class ConfigDataLocationTests {
assertThat(ConfigDataLocation.of("test")).hasToString("test"); 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);
}
} }
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
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
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
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
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
file-in-root-and-config-with-profile=true
v1=file-in-root-and-config-with-profile
v2=file-in-root-and-config-with-profile
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment