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 df636b3be9..8ac23d9cf2 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 @@ -993,6 +993,7 @@ The `YamlPropertiesFactoryBean` loads YAML as `Properties` and the `YamlMapFacto You can also use the `YamlPropertySourceLoader` class if you want to load YAML as a Spring `PropertySource`. + [[boot-features-external-config-random-values]] === Configuring Random Values The `RandomValuePropertySource` is useful for injecting random values (for example, into secrets or test cases). @@ -1013,13 +1014,15 @@ The `+random.int*+` syntax is `OPEN value (,max) CLOSE` where the `OPEN,CLOSE` a If `max` is provided, then `value` is the minimum value and `max` is the maximum value (exclusive). + [[boot-features-external-config-system-environment]] === Configuring System Environment Properties Spring Boot supports setting a prefix for environment properties. This is useful if the system environment is shared by multiple Spring Boot applications with different configuration requirements. The prefix for system environment properties can be set directly on `SpringApplication`. -For example, if you set the prefix to `input`, a property such as `foo.bar` will also be resolved as `input.foo.bar` in the system environment. +For example, if you set the prefix to `input`, a property such as `remote.timeout` will also be resolved as `input.remote.timeout` in the system environment. + [[boot-features-external-config-typesafe-configuration-properties]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index a3edea6655..d23ff29470 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -364,9 +364,8 @@ public class SpringApplication { listeners.environmentPrepared(bootstrapContext, environment); DefaultPropertiesPropertySource.moveToEnd(environment); configureAdditionalProfiles(environment); - if (environment.getProperty("spring.main.environment-prefix") != null) { - throw new IllegalStateException("Environment prefix cannot be set via properties."); - } + Assert.state(!environment.containsProperty("spring.main.environment-prefix"), + "Environment prefix cannot be set via properties."); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, @@ -1181,10 +1180,22 @@ public class SpringApplication { this.resourceLoader = resourceLoader; } + /** + * Return a prefix that should be applied when obtaining configuration properties from + * the system environment. + * @return the environment property prefix + * @since 2.5.0 + */ public String getEnvironmentPrefix() { return this.environmentPrefix; } + /** + * Set the prefix that should be applied when obtaining configuration properties from + * the system environment. + * @param environmentPrefix the environment property prefix to set + * @since 2.5.0 + */ public void setEnvironmentPrefix(String environmentPrefix) { this.environmentPrefix = environmentPrefix; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java index 0898756932..6ccb399e9e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.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. @@ -24,6 +24,7 @@ import java.util.Map; import java.util.function.Function; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A configuration property name composed of elements separated by dots. User created @@ -195,19 +196,32 @@ public final class ConfigurationPropertyName implements Comparable getNumberOfElements()) { + throw new IndexOutOfBoundsException("Offset: " + offset + ", NumberOfElements: " + getNumberOfElements()); + } + return new ConfigurationPropertyName(this.elements.subElements(offset)); + } + /** * Returns {@code true} if this element is an immediate parent of the specified name. * @param name the name to check @@ -718,7 +753,7 @@ public final class ConfigurationPropertyName implements Comparable stream() { - if (!StringUtils.hasText(getPrefix())) { - return getSource().stream(); - } - ConfigurationPropertyName prefix = ConfigurationPropertyName.of(getPrefix()); - return getSource().stream().map((propertyName) -> { - if (prefix.isAncestorOf(propertyName)) { - String name = propertyName.toString(); - return ConfigurationPropertyName.of(name.substring(getPrefix().length() + 1)); - } - return propertyName; - }); + return getSource().stream().map(this::stripPrefix); + } + + private ConfigurationPropertyName stripPrefix(ConfigurationPropertyName name) { + return (getPrefix().isAncestorOf(name)) ? name.subName(getPrefix().getNumberOfElements()) : name; } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java index 117b615bff..85aac1fd79 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.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. @@ -24,7 +24,7 @@ import java.util.NoSuchElementException; import java.util.Random; import java.util.function.Function; -import org.springframework.boot.env.Prefixed; +import org.springframework.boot.origin.OriginLookup; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; @@ -64,8 +64,8 @@ class SpringConfigurationPropertySources implements Iterable) source).getPrefix()); } this.cache.put(source, result); return result; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/Prefixed.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/Prefixed.java deleted file mode 100644 index 91ad4717ff..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/Prefixed.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.env; - -/** - * Interface that can be implemented by a - * {@link org.springframework.core.env.PropertySource} that can be used with a prefix. - * - * @author Madhura Bhave - * @since 2.5.0 - */ -@FunctionalInterface -public interface Prefixed { - - String getPrefix(); - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessor.java index 8389abc560..4effbc4704 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -32,7 +32,7 @@ import org.springframework.util.StringUtils; /** * An {@link EnvironmentPostProcessor} that replaces the systemEnvironment * {@link SystemEnvironmentPropertySource} with an - * {@link OriginAndPrefixAwareSystemEnvironmentPropertySource} that can track the + * {@link OriginAwareSystemEnvironmentPropertySource} that can track the * {@link SystemEnvironmentOrigin} for every system environment property. * * @author Madhura Bhave @@ -60,7 +60,7 @@ public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements private void replacePropertySource(ConfigurableEnvironment environment, String sourceName, PropertySource propertySource, String environmentPrefix) { Map originalSource = (Map) propertySource.getSource(); - SystemEnvironmentPropertySource source = new OriginAndPrefixAwareSystemEnvironmentPropertySource(sourceName, + SystemEnvironmentPropertySource source = new OriginAwareSystemEnvironmentPropertySource(sourceName, originalSource, environmentPrefix); environment.getPropertySources().replace(sourceName, source); } @@ -77,26 +77,24 @@ public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements /** * {@link SystemEnvironmentPropertySource} that also tracks {@link Origin}. */ - protected static class OriginAndPrefixAwareSystemEnvironmentPropertySource extends SystemEnvironmentPropertySource - implements OriginLookup, Prefixed { + protected static class OriginAwareSystemEnvironmentPropertySource extends SystemEnvironmentPropertySource + implements OriginLookup { - private final String environmentPrefix; + private final String prefix; - OriginAndPrefixAwareSystemEnvironmentPropertySource(String name, Map source, - String environmentPrefix) { + OriginAwareSystemEnvironmentPropertySource(String name, Map source, String environmentPrefix) { super(name, source); - this.environmentPrefix = getEnvironmentPrefix(environmentPrefix); + this.prefix = determinePrefix(environmentPrefix); } - private String getEnvironmentPrefix(String environmentPrefix) { - String prefix = environmentPrefix; + private String determinePrefix(String environmentPrefix) { if (!StringUtils.hasText(environmentPrefix)) { - return ""; + return null; } if (environmentPrefix.endsWith(".") || environmentPrefix.endsWith("_") || environmentPrefix.endsWith("-")) { - prefix = environmentPrefix.substring(0, environmentPrefix.length() - 1); + return environmentPrefix.substring(0, environmentPrefix.length() - 1); } - return prefix; + return environmentPrefix; } @Override @@ -120,7 +118,7 @@ public class SystemEnvironmentPropertySourceEnvironmentPostProcessor implements @Override public String getPrefix() { - return this.environmentPrefix; + return this.prefix; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java index b798fa640b..79dbb6138e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/OriginLookup.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -45,6 +45,19 @@ public interface OriginLookup { return false; } + /** + * Return the implicit prefix that is applied when performing a lookup or {@code null} + * if no prefix is used. Prefixes can be used to disambiguate keys that would + * otherwise clash. For example, if multiple applications are running on the same + * machine a different prefix can be set on each application to ensure that different + * environment variables are used. + * @return the prefix applied by the lookup class or {@code null}. + * @since 2.5.0 + */ + default String getPrefix() { + return null; + } + /** * Attempt to lookup the origin from the given source. If the source is not a * {@link OriginLookup} or if an exception occurs during lookup then {@code null} is diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java index 634dd4a5b7..a5176ca09d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.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. @@ -423,7 +423,20 @@ class ConfigurationPropertyNameTests { @Test void appendWhenElementNameIsNullShouldReturnName() { ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); - assertThat((Object) name.append(null)).isSameAs(name); + assertThat((Object) name.append((String) null)).isSameAs(name); + } + + @Test + void appendConfigurationPropertyNameShouldReturnAppendedName() { + ConfigurationPropertyName n1 = ConfigurationPropertyName.of("spring.boot"); + ConfigurationPropertyName n2 = ConfigurationPropertyName.of("tests.code"); + assertThat(n1.append(n2)).hasToString("spring.boot.tests.code"); + } + + @Test + void appendConfigurationPropertyNameWhenNullShouldReturnName() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); + assertThat((Object) name.append((ConfigurationPropertyName) null)).isSameAs(name); } @Test @@ -465,6 +478,37 @@ class ConfigurationPropertyNameTests { assertThat(name.chop(3)).isEqualTo(name); } + @Test + void subNameWhenOffsetLessThanSizeShouldReturnSubName() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.subName(1)).hasToString("bar.baz"); + assertThat(name.subName(2)).hasToString("baz"); + } + + @Test + void subNameWhenOffsetZeroShouldReturnName() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.subName(0)).isSameAs(name); + } + + @Test + void subNameWhenOffsetEqualToSizeShouldReturnEmpty() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThat(name.subName(3)).isSameAs(ConfigurationPropertyName.EMPTY); + } + + @Test + void subNameWhenOffsetMoreThanSizeShouldReturnEmpty() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> name.subName(4)); + } + + @Test + void subNameWhenOffsetNegativeShouldThrowException() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> name.subName(-1)); + } + @Test void isParentOfWhenSameShouldReturnFalse() { ConfigurationPropertyName name = ConfigurationPropertyName.of("foo"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PrefixedConfigurationPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PrefixedConfigurationPropertySourceTests.java index ecaa6772d6..f519e1c375 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PrefixedConfigurationPropertySourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/PrefixedConfigurationPropertySourceTests.java @@ -76,6 +76,20 @@ class PrefixedConfigurationPropertySourceTests { .isEqualTo(ConfigurationPropertyState.ABSENT); } + @Test + void withPrefixWhenPrefixIsNullReturnsOriginalSource() { + ConfigurationPropertySource source = new MockConfigurationPropertySource().nonIterable(); + ConfigurationPropertySource prefixed = source.withPrefix(null); + assertThat(prefixed).isSameAs(source); + } + + @Test + void withPrefixWhenPrefixIsEmptyReturnsOriginalSource() { + ConfigurationPropertySource source = new MockConfigurationPropertySource().nonIterable(); + ConfigurationPropertySource prefixed = source.withPrefix(""); + assertThat(prefixed).isSameAs(source); + } + private ConfigurationPropertyName getName(ConfigurationPropertySource source, String name) { ConfigurationProperty property = source.getConfigurationProperty(ConfigurationPropertyName.of(name)); return (property != null) ? property.getName() : null; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessorTests.java index b0ab37c04d..5b29b01970 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -22,7 +22,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor.OriginAndPrefixAwareSystemEnvironmentPropertySource; +import org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor.OriginAwareSystemEnvironmentPropertySource; import org.springframework.boot.origin.SystemEnvironmentOrigin; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; @@ -47,7 +47,7 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests { SystemEnvironmentPropertySourceEnvironmentPostProcessor postProcessor = new SystemEnvironmentPropertySourceEnvironmentPostProcessor(); postProcessor.postProcessEnvironment(this.environment, this.application); PropertySource replaced = this.environment.getPropertySources().get("systemEnvironment"); - assertThat(replaced).isInstanceOf(OriginAndPrefixAwareSystemEnvironmentPropertySource.class); + assertThat(replaced).isInstanceOf(OriginAwareSystemEnvironmentPropertySource.class); } @Test @@ -56,7 +56,7 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests { SystemEnvironmentPropertySourceEnvironmentPostProcessor postProcessor = new SystemEnvironmentPropertySourceEnvironmentPostProcessor(); PropertySource original = this.environment.getPropertySources().get("systemEnvironment"); postProcessor.postProcessEnvironment(this.environment, this.application); - OriginAndPrefixAwareSystemEnvironmentPropertySource replaced = (OriginAndPrefixAwareSystemEnvironmentPropertySource) this.environment + OriginAwareSystemEnvironmentPropertySource replaced = (OriginAwareSystemEnvironmentPropertySource) this.environment .getPropertySources().get("systemEnvironment"); Map originalMap = (Map) original.getSource(); Map replacedMap = replaced.getSource(); @@ -71,7 +71,7 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests { void replacedPropertySourceWhenPropertyAbsentShouldReturnNullOrigin() { SystemEnvironmentPropertySourceEnvironmentPostProcessor postProcessor = new SystemEnvironmentPropertySourceEnvironmentPostProcessor(); postProcessor.postProcessEnvironment(this.environment, this.application); - OriginAndPrefixAwareSystemEnvironmentPropertySource replaced = (OriginAndPrefixAwareSystemEnvironmentPropertySource) this.environment + OriginAwareSystemEnvironmentPropertySource replaced = (OriginAwareSystemEnvironmentPropertySource) this.environment .getPropertySources().get("systemEnvironment"); assertThat(replaced.getOrigin("NON_EXISTENT")).isNull(); } @@ -83,7 +83,7 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests { this.environment.getPropertySources().replace("systemEnvironment", new SystemEnvironmentPropertySource("systemEnvironment", source)); postProcessor.postProcessEnvironment(this.environment, this.application); - OriginAndPrefixAwareSystemEnvironmentPropertySource replaced = (OriginAndPrefixAwareSystemEnvironmentPropertySource) this.environment + OriginAwareSystemEnvironmentPropertySource replaced = (OriginAwareSystemEnvironmentPropertySource) this.environment .getPropertySources().get("systemEnvironment"); SystemEnvironmentOrigin origin = (SystemEnvironmentOrigin) replaced.getOrigin("foo.bar.baz"); assertThat(origin.getProperty()).isEqualTo("FOO_BAR_BAZ"); @@ -96,7 +96,7 @@ class SystemEnvironmentPropertySourceEnvironmentPostProcessorTests { SpringApplication application = new SpringApplication(); application.setEnvironmentPrefix("my"); postProcessor.postProcessEnvironment(this.environment, application); - OriginAndPrefixAwareSystemEnvironmentPropertySource replaced = (OriginAndPrefixAwareSystemEnvironmentPropertySource) this.environment + OriginAwareSystemEnvironmentPropertySource replaced = (OriginAwareSystemEnvironmentPropertySource) this.environment .getPropertySources().get("systemEnvironment"); assertThat(replaced.getPrefix()).isEqualTo("my"); }