Skip to content

Commit a8592f3

Browse files
committed
Add prefix support for property source
We configure the `SystemEnvironmentPropertySource` as a `Prefixed` property source. When adapting this to a `ConfigurationPropertySource, a `PrefixedConfigurationPropertySource` will be created for it. A `PrefixedConfigurationPropertySource` will resolve property such as `foo.bar` to `my.foo.bar` for a prefix of `my`. Closes gh-3450
1 parent 888ca44 commit a8592f3

14 files changed

+416
-17
lines changed

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

+8
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,14 @@ The `+random.int*+` syntax is `OPEN value (,max) CLOSE` where the `OPEN,CLOSE` a
996996
If `max` is provided, then `value` is the minimum value and `max` is the maximum value (exclusive).
997997

998998

999+
[[boot-features-external-config-system-environment]]
1000+
=== Configuring System Environment Properties
1001+
Spring Boot supports setting a prefix for environment properties.
1002+
This is useful if the system environment is shared by multiple Spring Boot applications with different configuration requirements.
1003+
The prefix for system environment properties can be set directly on `SpringApplication`.
1004+
1005+
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.
1006+
9991007

10001008
[[boot-features-external-config-typesafe-configuration-properties]]
10011009
=== Type-safe Configuration Properties

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java

+13
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ public class SpringApplication {
246246

247247
private boolean lazyInitialization = false;
248248

249+
private String environmentPrefix;
250+
249251
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;
250252

251253
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
@@ -362,6 +364,9 @@ private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners
362364
listeners.environmentPrepared(bootstrapContext, environment);
363365
DefaultPropertiesPropertySource.moveToEnd(environment);
364366
configureAdditionalProfiles(environment);
367+
if (environment.getProperty("spring.main.environment-prefix") != null) {
368+
throw new IllegalStateException("Environment prefix cannot be set via properties.");
369+
}
365370
bindToSpringApplication(environment);
366371
if (!this.isCustomEnvironment) {
367372
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
@@ -1174,6 +1179,14 @@ public void setResourceLoader(ResourceLoader resourceLoader) {
11741179
this.resourceLoader = resourceLoader;
11751180
}
11761181

1182+
public String getEnvironmentPrefix() {
1183+
return this.environmentPrefix;
1184+
}
1185+
1186+
public void setEnvironmentPrefix(String environmentPrefix) {
1187+
this.environmentPrefix = environmentPrefix;
1188+
}
1189+
11771190
/**
11781191
* Sets the type of Spring {@link ApplicationContext} that will be created. If not
11791192
* specified defaults to {@link #DEFAULT_SERVLET_WEB_CONTEXT_CLASS} for web based

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySource.java

+9
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ default ConfigurationPropertySource withAliases(ConfigurationPropertyNameAliases
7474
return new AliasedConfigurationPropertySource(this, aliases);
7575
}
7676

77+
/**
78+
* Return a variant of this source that supports a prefix.
79+
* @param prefix the prefix for properties in the source
80+
* @return a {@link ConfigurationPropertySource} instance supporting a prefix
81+
*/
82+
default ConfigurationPropertySource withPrefix(String prefix) {
83+
return new PrefixedConfigurationPropertySource(this, prefix);
84+
}
85+
7786
/**
7887
* Return the underlying source that is actually providing the properties.
7988
* @return the underlying property source or {@code null}.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java

+5
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,9 @@ default IterableConfigurationPropertySource withAliases(ConfigurationPropertyNam
7373
return new AliasedIterableConfigurationPropertySource(this, aliases);
7474
}
7575

76+
@Override
77+
default IterableConfigurationPropertySource withPrefix(String prefix) {
78+
return new PrefixedIterableConfigurationPropertySource(this, prefix);
79+
}
80+
7681
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties.source;
18+
19+
import org.springframework.util.Assert;
20+
import org.springframework.util.StringUtils;
21+
22+
/**
23+
* A {@link ConfigurationPropertySource} supporting a prefix.
24+
*
25+
* @author Madhura Bhave
26+
*/
27+
class PrefixedConfigurationPropertySource implements ConfigurationPropertySource {
28+
29+
private final ConfigurationPropertySource source;
30+
31+
private final String prefix;
32+
33+
PrefixedConfigurationPropertySource(ConfigurationPropertySource source, String prefix) {
34+
Assert.notNull(source, "Source must not be null");
35+
Assert.notNull(prefix, "Prefix must not be null");
36+
this.source = source;
37+
this.prefix = prefix;
38+
}
39+
40+
@Override
41+
public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
42+
ConfigurationProperty configurationProperty = this.source.getConfigurationProperty(getPrefixedName(name));
43+
if (configurationProperty == null) {
44+
return null;
45+
}
46+
return ConfigurationProperty.of(name, configurationProperty.getValue(), configurationProperty.getOrigin());
47+
}
48+
49+
private ConfigurationPropertyName getPrefixedName(ConfigurationPropertyName name) {
50+
String prefix = (StringUtils.hasText(this.prefix)) ? this.prefix + "." : "";
51+
return ConfigurationPropertyName.of(prefix + name);
52+
}
53+
54+
@Override
55+
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) {
56+
return this.source.containsDescendantOf(getPrefixedName(name));
57+
}
58+
59+
@Override
60+
public Object getUnderlyingSource() {
61+
return this.source.getUnderlyingSource();
62+
}
63+
64+
protected ConfigurationPropertySource getSource() {
65+
return this.source;
66+
}
67+
68+
protected String getPrefix() {
69+
return this.prefix;
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties.source;
18+
19+
import java.util.stream.Stream;
20+
21+
/**
22+
* An iterable {@link PrefixedConfigurationPropertySource}.
23+
*
24+
* @author Madhura Bhave
25+
*/
26+
class PrefixedIterableConfigurationPropertySource extends PrefixedConfigurationPropertySource
27+
implements IterableConfigurationPropertySource {
28+
29+
PrefixedIterableConfigurationPropertySource(IterableConfigurationPropertySource source, String prefix) {
30+
super(source, prefix);
31+
}
32+
33+
@Override
34+
public Stream<ConfigurationPropertyName> stream() {
35+
ConfigurationPropertyName prefix = ConfigurationPropertyName.of(getPrefix());
36+
return getSource().stream().map((propertyName) -> {
37+
if (prefix.isAncestorOf(propertyName)) {
38+
String name = propertyName.toString();
39+
return ConfigurationPropertyName.of(name.substring(getPrefix().length() + 1));
40+
}
41+
return propertyName;
42+
});
43+
}
44+
45+
@Override
46+
protected IterableConfigurationPropertySource getSource() {
47+
return (IterableConfigurationPropertySource) super.getSource();
48+
}
49+
50+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySources.java

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Random;
2525
import java.util.function.Function;
2626

27+
import org.springframework.boot.env.Prefixed;
2728
import org.springframework.core.env.ConfigurableEnvironment;
2829
import org.springframework.core.env.MutablePropertySources;
2930
import org.springframework.core.env.PropertySource;
@@ -63,6 +64,9 @@ private ConfigurationPropertySource adapt(PropertySource<?> source) {
6364
return result;
6465
}
6566
result = SpringConfigurationPropertySource.from(source);
67+
if (source instanceof Prefixed) {
68+
result = result.withPrefix(((Prefixed) source).getPrefix());
69+
}
6670
this.cache.put(source, result);
6771
return result;
6872
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.env;
18+
19+
/**
20+
* Interface that can be implemented by a
21+
* {@link org.springframework.core.env.PropertySource} that can be used with a prefix.
22+
*
23+
* @author Madhura Bhave
24+
* @since 2.5.0
25+
*/
26+
@FunctionalInterface
27+
public interface Prefixed {
28+
29+
String getPrefix();
30+
31+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/SystemEnvironmentPropertySourceEnvironmentPostProcessor.java

+39-8
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
import org.springframework.core.env.PropertySource;
2828
import org.springframework.core.env.StandardEnvironment;
2929
import org.springframework.core.env.SystemEnvironmentPropertySource;
30+
import org.springframework.util.StringUtils;
3031

3132
/**
3233
* An {@link EnvironmentPostProcessor} that replaces the systemEnvironment
3334
* {@link SystemEnvironmentPropertySource} with an
34-
* {@link OriginAwareSystemEnvironmentPropertySource} that can track the
35+
* {@link OriginAndPrefixAwareSystemEnvironmentPropertySource} that can track the
3536
* {@link SystemEnvironmentOrigin} for every system environment property.
3637
*
3738
* @author Madhura Bhave
@@ -51,16 +52,16 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp
5152
String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
5253
PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
5354
if (propertySource != null) {
54-
replacePropertySource(environment, sourceName, propertySource);
55+
replacePropertySource(environment, sourceName, propertySource, application.getEnvironmentPrefix());
5556
}
5657
}
5758

5859
@SuppressWarnings("unchecked")
5960
private void replacePropertySource(ConfigurableEnvironment environment, String sourceName,
60-
PropertySource<?> propertySource) {
61+
PropertySource<?> propertySource, String environmentPrefix) {
6162
Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
62-
SystemEnvironmentPropertySource source = new OriginAwareSystemEnvironmentPropertySource(sourceName,
63-
originalSource);
63+
SystemEnvironmentPropertySource source = new OriginAndPrefixAwareSystemEnvironmentPropertySource(sourceName,
64+
originalSource, environmentPrefix);
6465
environment.getPropertySources().replace(sourceName, source);
6566
}
6667

@@ -76,11 +77,36 @@ public void setOrder(int order) {
7677
/**
7778
* {@link SystemEnvironmentPropertySource} that also tracks {@link Origin}.
7879
*/
79-
protected static class OriginAwareSystemEnvironmentPropertySource extends SystemEnvironmentPropertySource
80-
implements OriginLookup<String> {
80+
protected static class OriginAndPrefixAwareSystemEnvironmentPropertySource extends SystemEnvironmentPropertySource
81+
implements OriginLookup<String>, Prefixed {
8182

82-
OriginAwareSystemEnvironmentPropertySource(String name, Map<String, Object> source) {
83+
private final String environmentPrefix;
84+
85+
OriginAndPrefixAwareSystemEnvironmentPropertySource(String name, Map<String, Object> source,
86+
String environmentPrefix) {
8387
super(name, source);
88+
this.environmentPrefix = getEnvironmentPrefix(environmentPrefix);
89+
}
90+
91+
private String getEnvironmentPrefix(String environmentPrefix) {
92+
String prefix = environmentPrefix;
93+
if (!StringUtils.hasText(environmentPrefix)) {
94+
return "";
95+
}
96+
if (environmentPrefix.endsWith(".") || environmentPrefix.endsWith("_") || environmentPrefix.endsWith("-")) {
97+
prefix = environmentPrefix.substring(0, environmentPrefix.length() - 1);
98+
}
99+
return prefix;
100+
}
101+
102+
@Override
103+
public boolean containsProperty(String name) {
104+
return super.containsProperty(name);
105+
}
106+
107+
@Override
108+
public Object getProperty(String name) {
109+
return super.getProperty(name);
84110
}
85111

86112
@Override
@@ -92,6 +118,11 @@ public Origin getOrigin(String key) {
92118
return null;
93119
}
94120

121+
@Override
122+
public String getPrefix() {
123+
return this.environmentPrefix;
124+
}
125+
95126
}
96127

97128
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

+15
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,21 @@ void addBootstrapperCanRegisterBeans() {
12331233
assertThat(applicationContext.getBean("test")).isEqualTo("boot");
12341234
}
12351235

1236+
@Test
1237+
void settingEnvironmentPrefixViaPropertiesThrowsException() {
1238+
assertThatIllegalStateException()
1239+
.isThrownBy(() -> new SpringApplication().run("--spring.main.environment-prefix=my"));
1240+
}
1241+
1242+
@Test
1243+
void bindsEnvironmentPrefixToSpringApplication() {
1244+
SpringApplication application = new SpringApplication(ExampleConfig.class);
1245+
application.setEnvironmentPrefix("my");
1246+
application.setWebApplicationType(WebApplicationType.NONE);
1247+
this.context = application.run();
1248+
assertThat(application.getEnvironmentPrefix()).isEqualTo("my");
1249+
}
1250+
12361251
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
12371252
S state) {
12381253
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
5454
import org.springframework.beans.factory.support.GenericBeanDefinition;
5555
import org.springframework.beans.factory.support.RootBeanDefinition;
56+
import org.springframework.boot.SpringApplication;
5657
import org.springframework.boot.context.properties.bind.BindException;
5758
import org.springframework.boot.context.properties.bind.DefaultValue;
5859
import org.springframework.boot.context.properties.bind.validation.BindValidationException;
@@ -473,6 +474,22 @@ void loadWhenDotsInSystemEnvironmentPropertiesShouldBind() {
473474
assertThat(bean.getBar()).isEqualTo("baz");
474475
}
475476

477+
@Test
478+
@SuppressWarnings("unchecked")
479+
void loadWhenEnvironmentPrefixSetShouldBind() {
480+
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
481+
sources.replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
482+
new SystemEnvironmentPropertySource(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
483+
Collections.singletonMap("MY_SPRING_FOO_NAME", "Jane")));
484+
SpringApplication application = new SpringApplication(PrefixConfiguration.class);
485+
application.setApplicationContextFactory((webApplicationType) -> ConfigurationPropertiesTests.this.context);
486+
application.setEnvironmentPrefix("my");
487+
application.setEnvironment(this.context.getEnvironment());
488+
application.run();
489+
BasicProperties bean = this.context.getBean(BasicProperties.class);
490+
assertThat(bean.name).isEqualTo("Jane");
491+
}
492+
476493
@Test
477494
void loadWhenOverridingPropertiesShouldBind() {
478495
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();

0 commit comments

Comments
 (0)