Skip to content

Commit 0032c00

Browse files
committed
Initial POC for BootstrapEnvironmentPostProcessor
1 parent 09a2e62 commit 0032c00

File tree

4 files changed

+344
-1
lines changed

4 files changed

+344
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright 2013-2020 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.cloud.bootstrap;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
26+
import org.springframework.boot.Banner;
27+
import org.springframework.boot.SpringApplication;
28+
import org.springframework.boot.WebApplicationType;
29+
import org.springframework.boot.builder.SpringApplicationBuilder;
30+
import org.springframework.boot.env.EnvironmentPostProcessor;
31+
import org.springframework.boot.env.OriginTrackedMapPropertySource;
32+
import org.springframework.boot.origin.Origin;
33+
import org.springframework.boot.origin.OriginLookup;
34+
import org.springframework.cloud.bootstrap.support.OriginTrackedCompositePropertySource;
35+
import org.springframework.context.ConfigurableApplicationContext;
36+
import org.springframework.core.env.CompositePropertySource;
37+
import org.springframework.core.env.ConfigurableEnvironment;
38+
import org.springframework.core.env.MapPropertySource;
39+
import org.springframework.core.env.MutablePropertySources;
40+
import org.springframework.core.env.PropertySource;
41+
import org.springframework.core.env.StandardEnvironment;
42+
import org.springframework.core.env.SystemEnvironmentPropertySource;
43+
import org.springframework.util.StringUtils;
44+
45+
public class BootstrapEnvironmentPostProcessor implements EnvironmentPostProcessor {
46+
/**
47+
* Property source name for bootstrap.
48+
*/
49+
public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrap";
50+
/**
51+
* The name of the default properties.
52+
*/
53+
public static final String DEFAULT_PROPERTIES = "springCloudDefaultProperties";
54+
55+
@Override
56+
public void postProcessEnvironment(ConfigurableEnvironment environment,
57+
SpringApplication application) {
58+
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
59+
true)) {
60+
return;
61+
}
62+
// don't listen to events in a bootstrap context
63+
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
64+
return;
65+
}
66+
String configName = environment
67+
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
68+
69+
bootstrapServiceContext(environment, application, configName);
70+
}
71+
72+
private ConfigurableApplicationContext bootstrapServiceContext(
73+
ConfigurableEnvironment environment, final SpringApplication application,
74+
String configName) {
75+
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
76+
MutablePropertySources bootstrapProperties = bootstrapEnvironment
77+
.getPropertySources();
78+
// empty bootstrapProperties
79+
for (PropertySource<?> source : bootstrapProperties) {
80+
bootstrapProperties.remove(source.getName());
81+
}
82+
String configLocation = environment
83+
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
84+
String configAdditionalLocation = environment
85+
.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
86+
Map<String, Object> bootstrapMap = new HashMap<>();
87+
bootstrapMap.put("spring.config.name", configName);
88+
// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
89+
// will fail // force the environment to use none, because if though it is set
90+
// below in the builder the environment overrides it
91+
bootstrapMap.put("spring.main.web-application-type", "none");
92+
if (StringUtils.hasText(configLocation)) {
93+
bootstrapMap.put("spring.config.location", configLocation);
94+
}
95+
if (StringUtils.hasText(configAdditionalLocation)) {
96+
bootstrapMap.put("spring.config.additional-location",
97+
configAdditionalLocation);
98+
}
99+
bootstrapProperties.addFirst(
100+
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
101+
for (PropertySource<?> source : environment.getPropertySources()) {
102+
if (source instanceof PropertySource.StubPropertySource) {
103+
continue;
104+
}
105+
bootstrapProperties.addLast(source);
106+
}
107+
// TODO: is it possible or sensible to share a ResourceLoader?
108+
SpringApplicationBuilder builder = new SpringApplicationBuilder()
109+
.profiles(environment.getActiveProfiles()).bannerMode(Banner.Mode.OFF)
110+
.environment(bootstrapEnvironment)
111+
// Don't use the default properties in this builder
112+
.registerShutdownHook(false).logStartupInfo(false)
113+
.web(WebApplicationType.NONE);
114+
final SpringApplication builderApplication = builder.application();
115+
if (builderApplication.getMainApplicationClass() == null) {
116+
// gh_425:
117+
// SpringApplication cannot deduce the MainApplicationClass here
118+
// if it is booted from SpringBootServletInitializer due to the
119+
// absense of the "main" method in stackTraces.
120+
// But luckily this method's second parameter "application" here
121+
// carries the real MainApplicationClass which has been explicitly
122+
// set by SpringBootServletInitializer itself already.
123+
builder.main(application.getMainApplicationClass());
124+
}
125+
if (environment.getPropertySources().contains("refreshArgs")) {
126+
// If we are doing a context refresh, really we only want to refresh the
127+
// Environment, and there are some toxic listeners (like the
128+
// LoggingApplicationListener) that affect global static state, so we need a
129+
// way to switch those off.
130+
// builderApplication
131+
// .setListeners(filterListeners(builderApplication.getListeners()));
132+
}
133+
builder.sources(BootstrapImportSelectorConfiguration.class);
134+
final ConfigurableApplicationContext context = builder.run();
135+
// gh-214 using spring.application.name=bootstrap to set the context id via
136+
// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
137+
// spring.application.name
138+
// during the bootstrap phase.
139+
context.setId("bootstrap");
140+
141+
// Make the bootstrap context a parent of the app context
142+
// addAncestorInitializer(application, context);
143+
144+
// It only has properties in it now that we don't want in the parent so remove
145+
// it (and it will be added back later)
146+
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
147+
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
148+
return context;
149+
}
150+
151+
private void mergeDefaultProperties(MutablePropertySources environment,
152+
MutablePropertySources bootstrap) {
153+
String name = DEFAULT_PROPERTIES;
154+
if (bootstrap.contains(name)) {
155+
PropertySource<?> source = bootstrap.get(name);
156+
if (!environment.contains(name)) {
157+
environment.addLast(source);
158+
}
159+
else {
160+
PropertySource<?> target = environment.get(name);
161+
if (target instanceof MapPropertySource && target != source
162+
&& source instanceof MapPropertySource) {
163+
Map<String, Object> targetMap = ((MapPropertySource) target)
164+
.getSource();
165+
Map<String, Object> map = ((MapPropertySource) source).getSource();
166+
for (String key : map.keySet()) {
167+
if (!target.containsProperty(key)) {
168+
targetMap.put(key, map.get(key));
169+
}
170+
}
171+
}
172+
}
173+
}
174+
mergeAdditionalPropertySources(environment, bootstrap);
175+
}
176+
177+
private void mergeAdditionalPropertySources(MutablePropertySources environment,
178+
MutablePropertySources bootstrap) {
179+
PropertySource<?> defaultProperties = environment.get(DEFAULT_PROPERTIES);
180+
ExtendedDefaultPropertySource result = defaultProperties instanceof ExtendedDefaultPropertySource
181+
? (ExtendedDefaultPropertySource) defaultProperties
182+
: new ExtendedDefaultPropertySource(DEFAULT_PROPERTIES,
183+
defaultProperties);
184+
for (PropertySource<?> source : bootstrap) {
185+
if (!environment.contains(source.getName())) {
186+
result.add(source);
187+
}
188+
}
189+
for (String name : result.getPropertySourceNames()) {
190+
bootstrap.remove(name);
191+
}
192+
addOrReplace(environment, result);
193+
addOrReplace(bootstrap, result);
194+
}
195+
196+
private void addOrReplace(MutablePropertySources environment,
197+
PropertySource<?> result) {
198+
if (environment.contains(result.getName())) {
199+
environment.replace(result.getName(), result);
200+
}
201+
else {
202+
environment.addLast(result);
203+
}
204+
}
205+
206+
private static class ExtendedDefaultPropertySource
207+
extends SystemEnvironmentPropertySource implements OriginLookup<String> {
208+
209+
private final OriginTrackedCompositePropertySource sources;
210+
211+
private final List<String> names = new ArrayList<>();
212+
213+
ExtendedDefaultPropertySource(String name, PropertySource<?> propertySource) {
214+
super(name, findMap(propertySource));
215+
this.sources = new OriginTrackedCompositePropertySource(name);
216+
}
217+
218+
@SuppressWarnings("unchecked")
219+
private static Map<String, Object> findMap(PropertySource<?> propertySource) {
220+
if (propertySource instanceof MapPropertySource) {
221+
return (Map<String, Object>) propertySource.getSource();
222+
}
223+
return new LinkedHashMap<String, Object>();
224+
}
225+
226+
public CompositePropertySource getPropertySources() {
227+
return this.sources;
228+
}
229+
230+
public List<String> getPropertySourceNames() {
231+
return this.names;
232+
}
233+
234+
public void add(PropertySource<?> source) {
235+
// Only add map property sources added by boot, see gh-476
236+
if (source instanceof OriginTrackedMapPropertySource
237+
&& !this.names.contains(source.getName())) {
238+
this.sources.addPropertySource(source);
239+
this.names.add(source.getName());
240+
}
241+
}
242+
243+
@Override
244+
public Object getProperty(String name) {
245+
if (this.sources.containsProperty(name)) {
246+
return this.sources.getProperty(name);
247+
}
248+
return super.getProperty(name);
249+
}
250+
251+
@Override
252+
public boolean containsProperty(String name) {
253+
if (this.sources.containsProperty(name)) {
254+
return true;
255+
}
256+
return super.containsProperty(name);
257+
}
258+
259+
@Override
260+
public String[] getPropertyNames() {
261+
List<String> names = new ArrayList<>();
262+
names.addAll(Arrays.asList(this.sources.getPropertyNames()));
263+
names.addAll(Arrays.asList(super.getPropertyNames()));
264+
return names.toArray(new String[0]);
265+
}
266+
267+
@Override
268+
public Origin getOrigin(String name) {
269+
return this.sources.getOrigin(name);
270+
}
271+
272+
}
273+
274+
}

spring-cloud-context/src/main/resources/META-INF/spring.factories

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration,\
77
org.springframework.cloud.autoconfigure.WritableEnvironmentEndpointAutoConfiguration
88
# Application Listeners
99
org.springframework.context.ApplicationListener=\
10-
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
1110
org.springframework.cloud.bootstrap.LoggingSystemShutdownListener,\
1211
org.springframework.cloud.context.restart.RestartListener
1312
# Bootstrap components
@@ -16,3 +15,6 @@ org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,
1615
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
1716
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
1817
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
18+
# Environment post processors
19+
org.springframework.boot.env.EnvironmentPostProcessor=\
20+
org.springframework.cloud.bootstrap.BootstrapEnvironmentPostProcessor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2013-2019 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.cloud.bootstrap;
18+
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.boot.SpringBootConfiguration;
26+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.context.ConfigurableApplicationContext;
30+
import org.springframework.core.env.ConfigurableEnvironment;
31+
import org.springframework.test.context.junit4.SpringRunner;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.fail;
35+
36+
@RunWith(SpringRunner.class)
37+
@SpringBootTest
38+
public class BootstrapEnvironmentPostProcessorTests {
39+
40+
@Autowired
41+
private ConfigurableEnvironment env;
42+
43+
@Autowired
44+
private ConfigurableApplicationContext context;
45+
46+
@Test
47+
public void contextLoads() {
48+
// from bootstrap.properties
49+
assertThat(env.getProperty("test.property")).isEqualTo("from bootstrap.properties");
50+
51+
try {
52+
assertThat(context.getBean("foo-during-bootstrap"));
53+
fail("bean created in bootstrap found");
54+
}
55+
catch (NoSuchBeanDefinitionException e) {
56+
// shouldn't exist
57+
}
58+
}
59+
60+
@EnableAutoConfiguration
61+
@SpringBootConfiguration
62+
protected static class TestConfig {
63+
64+
}
65+
66+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
spring.main.sources:org.springframework.cloud.bootstrap.config.BootstrapConfigurationTests.PropertySourceConfiguration,org.springframework.cloud.bootstrap.config.BootstrapConfigurationTests.CompositePropertySourceConfiguration
22
info.name:child
33
info.desc: defaultPropertiesInfoDesc
4+
test.property: from bootstrap.properties

0 commit comments

Comments
 (0)