From 2f467e58236ebf5cc0e060d3a99a7338c819d294 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Thu, 4 May 2023 15:06:20 -0400 Subject: [PATCH] Add application listener to locate property sources during bootstrap (#1228) * Add application listener to locate property sources during bootstrap Also adds support for activating profiles using spring.profiles.active from bootstrap property source listeners. Allow profiles to be passed from bootstrap context to main application context * Add application listener to locate property sources during bootstrap Also adds support for activating profiles using spring.profiles.active from bootstrap property source listeners. Allow profiles to be passed from bootstrap context to main application context Updating version in docs --- docs/src/main/asciidoc/_configprops.adoc | 1 + .../main/asciidoc/spring-cloud-commons.adoc | 10 + .../BootstrapApplicationListener.java | 7 + .../PropertySourceBootstrapConfiguration.java | 121 +++++- .../PropertySourceBootstrapProperties.java | 13 + .../config/BootstrapConfigurationTests.java | 393 +++++++++++++++--- 6 files changed, 469 insertions(+), 76 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index ea2dd79a..93facf98 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -4,6 +4,7 @@ |spring.cloud.compatibility-verifier.compatible-boot-versions | | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.4.x} |spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification. |spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. +|spring.cloud.config.initialize-on-context-refresh | `+++false+++` | Flag to initialize bootstrap configuration on context refresh event. Default false. |spring.cloud.config.override-none | `+++false+++` | Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is true, external properties should take lowest priority and should not override any existing property sources (including local config files). Default false. |spring.cloud.config.override-system-properties | `+++true+++` | Flag to indicate that the external properties should override system properties. Default true. |spring.cloud.decrypt-environment-post-processor.enabled | `+++true+++` | Enable the DecryptEnvironmentPostProcessor. diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index c6698e83..e26008e8 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -54,6 +54,10 @@ The additional property sources are: An example would be properties from the Spring Cloud Config Server. See "`<>`" for how to customize the contents of this property source. +NOTE: Prior to Spring Cloud 2021.0.7 `PropertySourceLocators` (including the ones for Spring Cloud Config) were run during +the main application context and not in the Bootstrap context. You can force `PropertySourceLocators` to be run during the +Bootstrap context by setting `spring.cloud.config.initialize-on-context-refresh=true` in `bootstrap.[properties | yaml]`. + * "`applicationConfig: [classpath:bootstrap.yml]`" (and related files if Spring profiles are active): If you have a `bootstrap.yml` (or `.properties`), those properties are used to configure the bootstrap context. Then they get added to the child context when its parent is set. They have lower precedence than the `application.yml` (or `.properties`) and any other property sources that are added to the child as a normal part of the process of creating a Spring Boot application. @@ -146,6 +150,12 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomP ---- ==== +As of Spring Cloud 2021.0.8, Spring Cloud will now call `PropertySourceLocators` twice. The first fetch +will retrieve any property sources without any profiles. These property sources will have the opportunity to +activate profiles using `spring.profiles.active`. After the main application context starts `PropertySourceLocators` +will be called a second time, this time with any active profiles allowing `PropertySourceLocators` to locate +any additional `PropertySources` with profiles. + === Logging Configuration If you use Spring Boot to configure log settings, you should place this configuration in `bootstrap.[yml | properties]` if you would like it to apply to all events. diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java index 76e2fba7..dd4d616c 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/BootstrapApplicationListener.java @@ -294,6 +294,13 @@ public class BootstrapApplicationListener implements ApplicationListener, Ordered { +public class PropertySourceBootstrapConfiguration implements ApplicationListener, + ApplicationContextInitializer, Ordered { /** * Bootstrap property source name. @@ -76,6 +81,9 @@ public class PropertySourceBootstrapConfiguration @Autowired(required = false) private List propertySourceLocators = new ArrayList<>(); + @Autowired + private PropertySourceBootstrapProperties bootstrapProperties; + @Override public int getOrder() { return this.order; @@ -85,8 +93,25 @@ public class PropertySourceBootstrapConfiguration this.propertySourceLocators = new ArrayList<>(propertySourceLocators); } + /* + * The ApplicationListener is called when the main application context is initialized. + * This will be called after the ApplicationListener ContextRefreshedEvent is fired + * during the bootstrap phase. This method is also what added PropertySources prior to + * Spring Cloud 2021.0.7, this is why it will be called when + * spring.cloud.config.initialize-on-context-refresh is false. When + * spring.cloud.config.initialize-on-context-refresh is true this method provides a + * "second fetch" of configuration data to fetch any additional configuration data + * from profiles that have been activated. + */ @Override public void initialize(ConfigurableApplicationContext applicationContext) { + if (!bootstrapProperties.isInitializeOnContextRefresh() || !applicationContext.getEnvironment() + .getPropertySources().contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + doInitialize(applicationContext); + } + } + + private void doInitialize(ConfigurableApplicationContext applicationContext) { List> composite = new ArrayList<>(); AnnotationAwareOrderComparator.sort(this.propertySourceLocators); boolean empty = true; @@ -122,7 +147,7 @@ public class PropertySourceBootstrapConfiguration insertPropertySources(propertySources, composite); reinitializeLoggingSystem(environment, logConfig, logFile); setLogLevels(applicationContext, environment); - handleIncludedProfiles(environment); + handleProfiles(environment); } } @@ -172,7 +197,12 @@ public class PropertySourceBootstrapConfiguration if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone() && remoteProperties.isOverrideSystemProperties())) { for (PropertySource p : reversedComposite) { - propertySources.addFirst(p); + if (propertySources.contains(DECRYPTED_PROPERTY_SOURCE_NAME)) { + propertySources.addAfter(DECRYPTED_PROPERTY_SOURCE_NAME, p); + } + else { + propertySources.addFirst(p); + } } return; } @@ -210,43 +240,98 @@ public class PropertySourceBootstrapConfiguration return environment; } - private void handleIncludedProfiles(ConfigurableEnvironment environment) { + private void handleProfiles(ConfigurableEnvironment environment) { + if (bootstrapProperties.isInitializeOnContextRefresh() && !environment.getPropertySources() + .contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + // In the case that spring.cloud.config.initialize-on-context-refresh is true + // this method will + // be called during the bootstrap phase and the main application startup. We + // only manipulate the environment profiles in the bootstrap phase as we are + // fetching + // any additional profile specific configuration when this method would be + // called during the + // main application startup, and it is not valid to activate profiles in + // profile specific + // configuration properties, so we should not run this method then. + return; + } Set includeProfiles = new TreeSet<>(); + List activeProfiles = new ArrayList<>(); + for (PropertySource propertySource : environment.getPropertySources()) { - addIncludedProfilesTo(includeProfiles, propertySource); + addIncludedProfilesTo(includeProfiles, propertySource, environment); + addActiveProfilesTo(activeProfiles, propertySource, environment); } - List activeProfiles = new ArrayList<>(); - Collections.addAll(activeProfiles, environment.getActiveProfiles()); // If it's already accepted we assume the order was set intentionally includeProfiles.removeAll(activeProfiles); - if (includeProfiles.isEmpty()) { - return; - } // Prepend each added profile (last wins in a property key clash) for (String profile : includeProfiles) { activeProfiles.add(0, profile); } + List activeProfilesFromEnvironment = Arrays.stream(environment.getActiveProfiles()) + .collect(Collectors.toList()); + if (!activeProfiles.containsAll(activeProfilesFromEnvironment)) { + activeProfiles.addAll(activeProfilesFromEnvironment); + + } environment.setActiveProfiles(activeProfiles.toArray(new String[activeProfiles.size()])); } - private Set addIncludedProfilesTo(Set profiles, PropertySource propertySource) { + private Set addIncludedProfilesTo(Set profiles, PropertySource propertySource, + ConfigurableEnvironment environment) { + return addProfilesTo(profiles, propertySource, Profiles.INCLUDE_PROFILES_PROPERTY_NAME, environment); + } + + private List addActiveProfilesTo(List profiles, PropertySource propertySource, + ConfigurableEnvironment environment) { + return addProfilesTo(profiles, propertySource, AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, environment); + } + + private > T addProfilesTo(T profiles, PropertySource propertySource, + String property, ConfigurableEnvironment environment) { if (propertySource instanceof CompositePropertySource) { for (PropertySource nestedPropertySource : ((CompositePropertySource) propertySource) .getPropertySources()) { - addIncludedProfilesTo(profiles, nestedPropertySource); + addProfilesTo(profiles, nestedPropertySource, property, environment); } } else { - Collections.addAll(profiles, getProfilesForValue( - propertySource.getProperty(ConfigFileApplicationListener.INCLUDE_PROFILES_PROPERTY))); + Collections.addAll(profiles, getProfilesForValue(propertySource.getProperty(property), environment)); } return profiles; } - private String[] getProfilesForValue(Object property) { + private String[] getProfilesForValue(Object property, ConfigurableEnvironment environment) { final String value = (property == null ? null : property.toString()); - return property == null ? new String[0] : StringUtils.tokenizeToStringArray(value, ","); + return property == null ? new String[0] : resolvePlaceholdersInProfiles(value, environment); + } + + private String[] resolvePlaceholdersInProfiles(String profiles, ConfigurableEnvironment environment) { + return Arrays.stream(StringUtils.tokenizeToStringArray(profiles, ",")).map(s -> { + if (s.startsWith("${") && s.endsWith("}")) { + return environment.resolvePlaceholders(s); + } + else { + return s; + } + }).toArray(String[]::new); + } + + /* + * The ConextRefreshedEvent gets called at the end of the boostrap phase after config + * data is loaded during bootstrap. This will run and do an "initial fetch" of + * configuration data during bootstrap but before the main applicaiton context starts. + */ + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (bootstrapProperties.isInitializeOnContextRefresh() + && event.getApplicationContext() instanceof ConfigurableApplicationContext) { + if (((ConfigurableApplicationContext) event.getApplicationContext()).getEnvironment().getPropertySources() + .contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + doInitialize((ConfigurableApplicationContext) event.getApplicationContext()); + } + } } } diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java index b442a80e..01164988 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapProperties.java @@ -46,6 +46,19 @@ public class PropertySourceBootstrapProperties { */ private boolean overrideNone = false; + /** + * Flag to initialize bootstrap configuration on context refresh event. Default false. + */ + private boolean initializeOnContextRefresh = false; + + public boolean isInitializeOnContextRefresh() { + return initializeOnContextRefresh; + } + + public void setInitializeOnContextRefresh(boolean initializeOnContextRefresh) { + this.initializeOnContextRefresh = initializeOnContextRefresh; + } + public boolean isOverrideNone() { return this.overrideNone; } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java index 7d9228ae..90209b2d 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/config/BootstrapConfigurationTests.java @@ -32,10 +32,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.bootstrap.BootstrapApplicationListener; import org.springframework.cloud.bootstrap.TestHigherPriorityBootstrapConfiguration; -import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -77,11 +78,20 @@ public class BootstrapConfigurationTests { @Test public void pickupOnlyExternalBootstrapProperties() { String externalPropertiesPath = getExternalProperties(); + pickupOnlyExternalBootstrapProperties("spring.cloud.bootstrap.location=" + externalPropertiesPath, + "spring.config.use-legacy-processing=true"); + } + + @Test + public void pickupOnlyExternalBootstrapPropertiesWithAppListener() { + String externalPropertiesPath = getExternalProperties(); + pickupOnlyExternalBootstrapProperties("spring.cloud.bootstrap.location=" + externalPropertiesPath, + "spring.config.use-legacy-processing=true", "spring.cloud.config.initialize-on-context-refresh=true"); + } + private void pickupOnlyExternalBootstrapProperties(String... properties) { this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).sources(BareConfiguration.class) - .properties("spring.cloud.bootstrap.location=" + externalPropertiesPath, - "spring.config.use-legacy-processing=true") - .run(); + .properties(properties).run(); then(this.context.getEnvironment().getProperty("info.name")).isEqualTo("externalPropertiesInfoName"); then(this.context.getEnvironment().getProperty("info.desc")).isNull(); then(this.context.getEnvironment().getPropertySources() @@ -92,11 +102,22 @@ public class BootstrapConfigurationTests { @Test public void pickupAdditionalExternalBootstrapProperties() { String externalPropertiesPath = getExternalProperties(); + pickupAdditionalExternalBootstrapProperties( + "spring.cloud.bootstrap.additional-location=" + externalPropertiesPath, + "spring.config.use-legacy-processing=true"); + } + + @Test + public void pickupAdditionalExternalBootstrapPropertiesWithAppListener() { + String externalPropertiesPath = getExternalProperties(); + pickupAdditionalExternalBootstrapProperties( + "spring.cloud.bootstrap.additional-location=" + externalPropertiesPath, + "spring.config.use-legacy-processing=true", "spring.cloud.config.initialize-on-context-refresh=true"); + } + private void pickupAdditionalExternalBootstrapProperties(String... properties) { this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).sources(BareConfiguration.class) - .properties("spring.cloud.bootstrap.additional-location=" + externalPropertiesPath, - "spring.config.use-legacy-processing=true") - .run(); + .properties(properties).run(); then(this.context.getEnvironment().getProperty("info.name")).isEqualTo("externalPropertiesInfoName"); then(this.context.getEnvironment().getProperty("info.desc")).isEqualTo("defaultPropertiesInfoDesc"); then(this.context.getEnvironment().getPropertySources() @@ -106,14 +127,20 @@ public class BootstrapConfigurationTests { @Test public void bootstrapPropertiesAvailableInInitializer() { - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class) - .initializers(new ApplicationContextInitializer() { - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - // This property is defined in bootstrap.properties - then(applicationContext.getEnvironment().getProperty("info.name")).isEqualTo("child"); - } + bootstrapPropertiesAvailableInInitializer("spring.config.use-legacy-processing=true"); + } + + @Test + public void bootstrapPropertiesAvailableInInitializerWithAppContext() { + bootstrapPropertiesAvailableInInitializer("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void bootstrapPropertiesAvailableInInitializer(String... properties) { + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).initializers(applicationContext -> { + // This property is defined in bootstrap.properties + then(applicationContext.getEnvironment().getProperty("info.name")).isEqualTo("child"); }).run(); then(this.context.getEnvironment().getPropertySources() .contains(PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME + "-testBootstrap")) @@ -132,9 +159,19 @@ public class BootstrapConfigurationTests { @Test public void picksUpAdditionalPropertySource() { + picksUpAdditionalPropertySource("spring.config.use-legacy-processing=true"); + } + + @Test + public void picksUpAdditionalPropertySourceWithAppContext() { + picksUpAdditionalPropertySource("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void picksUpAdditionalPropertySource(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("bar"); then(this.context.getEnvironment().getPropertySources() .contains(PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME + "-testBootstrap")) @@ -143,35 +180,77 @@ public class BootstrapConfigurationTests { @Test public void failsOnPropertySource() { + failsOnPropertySource("spring.config.use-legacy-processing=true"); + } + + @Test + public void failsOnPropertySourceWithAppContext() { + failsOnPropertySource("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void failsOnPropertySource(String... properties) { System.setProperty("expected.fail", "true"); Throwable throwable = Assertions.assertThrows(RuntimeException.class, () -> { - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); }); then(throwable.getMessage().equals("Planned")); } @Test public void overrideSystemPropertySourceByDefault() { + overrideSystemPropertySourceByDefault("spring.config.use-legacy-processing=true"); + + } + + @Test + public void overrideSystemPropertySourceByDefaultWithAppContext() { + overrideSystemPropertySourceByDefault("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + + } + + private void overrideSystemPropertySourceByDefault(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); System.setProperty("bootstrap.foo", "system"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("bar"); } @Test public void systemPropertyOverrideFalse() { + systemPropertyOverrideFalse("spring.config.use-legacy-processing=true"); + } + + @Test + public void systemPropertyOverrideFalseWithAppContext() { + systemPropertyOverrideFalse("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void systemPropertyOverrideFalse(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); PropertySourceConfiguration.MAP.put("spring.cloud.config.overrideSystemProperties", "false"); System.setProperty("bootstrap.foo", "system"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("system"); } @Test public void systemPropertyOverrideWhenOverrideDisallowed() { + systemPropertyOverrideWhenOverrideDisallowed("spring.config.use-legacy-processing=true"); + } + + @Test + public void systemPropertyOverrideWhenOverrideDisallowedWithAppContext() { + systemPropertyOverrideWhenOverrideDisallowed("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void systemPropertyOverrideWhenOverrideDisallowed(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); PropertySourceConfiguration.MAP.put("spring.cloud.config.overrideSystemProperties", "false"); // If spring.cloud.config.allowOverride=false is in the remote property sources @@ -179,42 +258,71 @@ public class BootstrapConfigurationTests { // their own remote property source. PropertySourceConfiguration.MAP.put("spring.cloud.config.allowOverride", "false"); System.setProperty("bootstrap.foo", "system"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("bar"); } @Test public void systemPropertyOverrideFalseWhenOverrideAllowed() { + systemPropertyOverrideFalseWhenOverrideAllowed("spring.config.use-legacy-processing=true"); + } + + @Test + public void systemPropertyOverrideFalseWhenOverrideAllowedWithAppContext() { + systemPropertyOverrideFalseWhenOverrideAllowed("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void systemPropertyOverrideFalseWhenOverrideAllowed(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); PropertySourceConfiguration.MAP.put("spring.cloud.config.overrideSystemProperties", "false"); PropertySourceConfiguration.MAP.put("spring.cloud.config.allowOverride", "true"); System.setProperty("bootstrap.foo", "system"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("system"); } @Test public void overrideAllWhenOverrideAllowed() { + overrideAllWhenOverrideAllowed("spring.config.use-legacy-processing=true"); + } + + @Test + public void overrideAllWhenOverrideAllowedWithAppContext() { + overrideAllWhenOverrideAllowed("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void overrideAllWhenOverrideAllowed(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); PropertySourceConfiguration.MAP.put("spring.cloud.config.overrideNone", "true"); PropertySourceConfiguration.MAP.put("spring.cloud.config.allowOverride", "true"); ConfigurableEnvironment environment = new StandardEnvironment(); environment.getPropertySources().addLast( new MapPropertySource("last", Collections.singletonMap("bootstrap.foo", "splat"))); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").environment(environment) - .sources(BareConfiguration.class).run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .environment(environment).sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("splat"); } @Test public void applicationNameInBootstrapAndMain() { + applicationNameInBootstrapAndMain("spring.cloud.bootstrap.name:other", + "spring.config.use-legacy-processing=true", "spring.config.name:plain"); + } + + @Test + public void applicationNameInBootstrapAndMainWithAppContext() { + applicationNameInBootstrapAndMain("spring.cloud.bootstrap.name:other", + "spring.config.use-legacy-processing=true", "spring.config.name:plain", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void applicationNameInBootstrapAndMain(String... properties) { System.setProperty("expected.name", "main"); - this.context = new SpringApplicationBuilder() - .web(WebApplicationType.NONE).properties("spring.cloud.bootstrap.name:other", - "spring.config.use-legacy-processing=true", "spring.config.name:plain") + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("spring.application.name")).isEqualTo("app"); // The parent is called "main" because spring.application.name is specified in @@ -228,10 +336,20 @@ public class BootstrapConfigurationTests { @Test public void applicationNameNotInBootstrap() { + applicationNameNotInBootstrap("spring.cloud.bootstrap.name:application", + "spring.config.use-legacy-processing=true", "spring.config.name:other"); + } + + @Test + public void applicationNameNotInBootstrapWithAppContext() { + applicationNameNotInBootstrap("spring.cloud.bootstrap.name:application", + "spring.config.use-legacy-processing=true", "spring.config.name:other", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void applicationNameNotInBootstrap(String... properties) { System.setProperty("expected.name", "main"); - this.context = new SpringApplicationBuilder() - .web(WebApplicationType.NONE).properties("spring.cloud.bootstrap.name:application", - "spring.config.use-legacy-processing=true", "spring.config.name:other") + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) .sources(BareConfiguration.class).run(); then(this.context.getEnvironment().getProperty("spring.application.name")).isEqualTo("main"); // The parent has no name because spring.application.name is not @@ -241,9 +359,18 @@ public class BootstrapConfigurationTests { @Test public void applicationNameOnlyInBootstrap() { + applicationNameOnlyInBootstrap("spring.cloud.bootstrap.name:other", "spring.config.use-legacy-processing=true"); + } + + @Test + public void applicationNameOnlyInBootstrapWithAppContext() { + applicationNameOnlyInBootstrap("spring.cloud.bootstrap.name:other", "spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void applicationNameOnlyInBootstrap(String... properties) { System.setProperty("expected.name", "main"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.cloud.bootstrap.name:other", "spring.config.use-legacy-processing=true") + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) .sources(BareConfiguration.class).run(); // The main context is called "main" because spring.application.name is specified // in other.properties (and not in the main config file) @@ -256,10 +383,20 @@ public class BootstrapConfigurationTests { @Test public void environmentEnrichedOnceWhenSharedWithChildContext() { + environmentEnrichedOnceWhenSharedWithChildContext("spring.config.use-legacy-processing=true"); + } + + @Test + public void environmentEnrichedOnceWhenSharedWithChildContextWithAppContext() { + environmentEnrichedOnceWhenSharedWithChildContext("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void environmentEnrichedOnceWhenSharedWithChildContext(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); - this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true").environment(new StandardEnvironment()) - .child(BareConfiguration.class).web(WebApplicationType.NONE).run(); + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class).properties(properties) + .environment(new StandardEnvironment()).child(BareConfiguration.class).web(WebApplicationType.NONE) + .run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("bar"); then(this.context.getParent().getEnvironment()).isEqualTo(this.context.getEnvironment()); MutablePropertySources sources = this.context.getEnvironment().getPropertySources(); @@ -271,11 +408,20 @@ public class BootstrapConfigurationTests { @Test public void onlyOneBootstrapContext() { + onlyOneBootstrapContext("spring.config.use-legacy-processing=true"); + } + + @Test + public void onlyOneBootstrapContextWithAppContext() { + onlyOneBootstrapContext("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void onlyOneBootstrapContext(String... properties) { TestHigherPriorityBootstrapConfiguration.count.set(0); PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); - this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true").child(BareConfiguration.class) - .web(WebApplicationType.NONE).run(); + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class).properties(properties) + .child(BareConfiguration.class).web(WebApplicationType.NONE).run(); then(TestHigherPriorityBootstrapConfiguration.count.get()).isEqualTo(1); then(this.context.getParent()).isNotNull(); then(this.context.getParent().getParent().getId()).isEqualTo("bootstrap"); @@ -285,9 +431,18 @@ public class BootstrapConfigurationTests { @Test public void listOverride() { - this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true").child(BareConfiguration.class) - .web(WebApplicationType.NONE).run(); + listOverride("spring.config.use-legacy-processing=true"); + } + + @Test + public void listOverrideWithAppContext() { + listOverride("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void listOverride(String... properties) { + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class).properties(properties) + .child(BareConfiguration.class).web(WebApplicationType.NONE).run(); ListProperties listProperties = new ListProperties(); Binder.get(this.context.getEnvironment()).bind("list", Bindable.ofInstance(listProperties)); then(listProperties.getFoo().size()).isEqualTo(1); @@ -296,10 +451,20 @@ public class BootstrapConfigurationTests { @Test public void bootstrapContextSharedBySiblings() { + bootstrapContextSharedBySiblings("spring.config.use-legacy-processing=true"); + } + + @Test + public void bootstrapContextSharedBySiblingsWithAppContext() { + bootstrapContextSharedBySiblings("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void bootstrapContextSharedBySiblings(String... properties) { TestHigherPriorityBootstrapConfiguration.count.set(0); PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); - SpringApplicationBuilder builder = new SpringApplicationBuilder() - .properties("spring.config.use-legacy-processing=true").sources(BareConfiguration.class); + SpringApplicationBuilder builder = new SpringApplicationBuilder().properties(properties) + .sources(BareConfiguration.class); this.sibling = builder.child(BareConfiguration.class).properties("spring.application.name=sibling") .web(WebApplicationType.NONE).run(); this.context = builder.child(BareConfiguration.class).properties("spring.application.name=context") @@ -319,10 +484,19 @@ public class BootstrapConfigurationTests { @Test public void environmentEnrichedInParentContext() { + environmentEnrichedInParentContext("spring.config.use-legacy-processing=true"); + } + + @Test + public void environmentEnrichedInParentContextWithAppContext() { + environmentEnrichedInParentContext("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void environmentEnrichedInParentContext(String... properties) { PropertySourceConfiguration.MAP.put("bootstrap.foo", "bar"); - this.context = new SpringApplicationBuilder().sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true").child(BareConfiguration.class) - .web(WebApplicationType.NONE).run(); + this.context = new SpringApplicationBuilder().sources(BareConfiguration.class).properties(properties) + .child(BareConfiguration.class).web(WebApplicationType.NONE).run(); then(this.context.getEnvironment().getProperty("bootstrap.foo")).isEqualTo("bar"); then(this.context.getParent().getEnvironment()).isNotSameAs(this.context.getEnvironment()); then(this.context.getEnvironment().getPropertySources() @@ -365,27 +539,118 @@ public class BootstrapConfigurationTests { @Test public void includeProfileFromBootstrapPropertySource() { + includeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true"); + } + + @Test + public void includeProfileFromBootstrapPropertySourceWithAppContext() { + includeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void includeProfileFromBootstrapPropertySource(String... properties) { PropertySourceConfiguration.MAP.put("spring.profiles.include", "bar,baz"); - this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE) - .properties("spring.config.use-legacy-processing=true").profiles("foo").sources(BareConfiguration.class) - .run(); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .profiles("foo").sources(BareConfiguration.class).run(); then(this.context.getEnvironment().acceptsProfiles("baz")).isTrue(); then(this.context.getEnvironment().acceptsProfiles("bar")).isTrue(); } + @Test + public void activeProfileFromBootstrapPropertySource() { + activeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true"); + } + + @Test + public void activeProfileFromBootstrapPropertySourceWithAppContext() { + activeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + then(this.context.getEnvironment().getActiveProfiles()).doesNotContain("after"); + } + + private void activeProfileFromBootstrapPropertySource(String... properties) { + PropertySourceConfiguration.MAP.put("spring.profiles.active", "bar,baz"); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .profiles("foo").sources(BareConfiguration.class).run(); + then(this.context.getEnvironment().acceptsProfiles("baz", "bar", "foo")).isTrue(); + then(this.context.getEnvironment().getActiveProfiles()).contains("baz", "bar", "foo"); + } + + @Test + public void activeAndIncludeProfileFromBootstrapPropertySource() { + activeAndIncludeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true"); + } + + @Test + public void activeAndIncludeProfileFromBootstrapPropertySourceWithAppContext() { + activeAndIncludeProfileFromBootstrapPropertySource("spring.config.use-legacy-processing=true", + "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void activeAndIncludeProfileFromBootstrapPropertySource(String... properties) { + PropertySourceConfiguration.MAP.put("spring.profiles.active", "bar,baz"); + PropertySourceConfiguration.MAP.put("spring.profiles.include", "bar,baz,hello"); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .profiles("foo").sources(BareConfiguration.class).run(); + then(this.context.getEnvironment().acceptsProfiles("baz", "bar", "hello", "foo")).isTrue(); + then(this.context.getEnvironment().getActiveProfiles()).contains("baz", "bar", "foo", "hello"); + } + + @Test + public void activeAndIncludeProfileFromBootstrapPropertySourceWithReplacement() { + activeAndIncludeProfileFromBootstrapPropertySourceWithReplacement("spring.config.use-legacy-processing=true", + "barreplacement=bar"); + } + + @Test + public void activeAndIncludeProfileFromBootstrapPropertySourceWithReplacementWithAppContext() { + activeAndIncludeProfileFromBootstrapPropertySourceWithReplacement("spring.config.use-legacy-processing=true", + "barreplacement=bar", "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void activeAndIncludeProfileFromBootstrapPropertySourceWithReplacement(String... properties) { + PropertySourceConfiguration.MAP.put("spring.profiles.active", "${barreplacement},baz"); + PropertySourceConfiguration.MAP.put("spring.profiles.include", "${barreplacement},baz,hello"); + this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties) + .profiles("foo").sources(BareConfiguration.class).run(); + then(this.context.getEnvironment().acceptsProfiles("baz", "bar", "hello", "foo")).isTrue(); + then(this.context.getEnvironment().getActiveProfiles()).contains("baz", "bar", "foo", "hello"); + } + @Test public void includeProfileFromBootstrapProperties() { + includeProfileFromBootstrapProperties("spring.config.use-legacy-processing=true", + "spring.cloud.bootstrap.name=local"); + } + + @Test + public void includeProfileFromBootstrapPropertiesWithAppContext() { + includeProfileFromBootstrapProperties("spring.config.use-legacy-processing=true", + "spring.cloud.bootstrap.name=local", "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void includeProfileFromBootstrapProperties(String... properties) { this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true", "spring.cloud.bootstrap.name=local").run(); + .properties(properties).run(); then(this.context.getEnvironment().acceptsProfiles("local")).isTrue(); then(this.context.getEnvironment().getProperty("added")).isEqualTo("Hello added!"); } @Test public void nonEnumerablePropertySourceWorks() { + nonEnumerablePropertySourceWorks("spring.config.use-legacy-processing=true", + "spring.cloud.bootstrap.name=nonenumerable"); + } + + @Test + public void nonEnumerablePropertySourceWorksWithAppContext() { + nonEnumerablePropertySourceWorks("spring.config.use-legacy-processing=true", + "spring.cloud.bootstrap.name=nonenumerable", "spring.cloud.config.initialize-on-context-refresh=true"); + } + + private void nonEnumerablePropertySourceWorks(String... properties) { this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE).sources(BareConfiguration.class) - .properties("spring.config.use-legacy-processing=true", "spring.cloud.bootstrap.name=nonenumerable") - .run(); + .properties(properties).run(); then(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar"); } @@ -425,12 +690,24 @@ public class BootstrapConfigurationTests { @Override public PropertySource locate(Environment environment) { + if (environment instanceof ConfigurableEnvironment) { + if (!((ConfigurableEnvironment) environment).getPropertySources() + .contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + if (MAP.containsKey(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME)) { + // This additional profile, after, should not be added when + // initialize-on-context-refresh=true + MAP.put(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, + MAP.get(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME) + ",after"); + } + } + } if (this.name != null) { then(this.name).isEqualTo(environment.getProperty("spring.application.name")); } if (this.fail) { throw new RuntimeException("Planned"); } + return new MapPropertySource("testBootstrap", MAP); }