Add ConfigDataEnvironmentUpdateListener support

Add an overloaded `ConfigDataEnvironmentPostProcessor.applyTo` method
that accepts a listener that can used to track the updates that were
applied to the `Environment`.

The listener can be used to track the which `ConfigDataLocation` and
the `ConfigDataResource` were used to add a `PropertySource`. The lister
can also be used to tell which profiles were applied.

This enhancement is being added in a patch release because it's will
be useful for Spring Cloud 2020.0.0.

Closes gh-24504
pull/24597/head
Phillip Webb 4 years ago
parent 5e1a69e90e
commit 38e4c2a179

@ -119,6 +119,8 @@ class ConfigDataEnvironment {
private final Collection<String> additionalProfiles;
private final ConfigDataEnvironmentUpdateListener environmentUpdateListener;
private final ConfigDataLoaders loaders;
private final ConfigDataEnvironmentContributors contributors;
@ -130,9 +132,13 @@ class ConfigDataEnvironment {
* @param environment the Spring {@link Environment}.
* @param resourceLoader {@link ResourceLoader} to load resource locations
* @param additionalProfiles any additional profiles to activate
* @param environmentUpdateListener optional
* {@link ConfigDataEnvironmentUpdateListener} that can be used to track
* {@link Environment} updates.
*/
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
Binder binder = Binder.get(environment);
UseLegacyConfigProcessingException.throwIfRequested(binder);
this.logFactory = logFactory;
@ -143,6 +149,8 @@ class ConfigDataEnvironment {
this.environment = environment;
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
this.additionalProfiles = additionalProfiles;
this.environmentUpdateListener = (environmentUpdateListener != null) ? environmentUpdateListener
: ConfigDataEnvironmentUpdateListener.NONE;
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
this.contributors = createContributors(binder);
}
@ -301,16 +309,18 @@ class ConfigDataEnvironment {
MutablePropertySources propertySources = this.environment.getPropertySources();
this.logger.trace("Applying config data environment contributions");
for (ConfigDataEnvironmentContributor contributor : contributors) {
if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT
&& contributor.getPropertySource() != null) {
PropertySource<?> propertySource = contributor.getPropertySource();
if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) {
if (!contributor.isActive(activationContext)) {
this.logger.trace(LogMessage.format("Skipping inactive property source '%s'",
contributor.getPropertySource().getName()));
this.logger.trace(
LogMessage.format("Skipping inactive property source '%s'", propertySource.getName()));
}
else {
this.logger.trace(LogMessage.format("Adding imported property source '%s'",
contributor.getPropertySource().getName()));
propertySources.addLast(contributor.getPropertySource());
this.logger
.trace(LogMessage.format("Adding imported property source '%s'", propertySource.getName()));
propertySources.addLast(propertySource);
this.environmentUpdateListener.onPropertySourceAdded(propertySource, contributor.getLocation(),
contributor.getResource());
}
}
}
@ -320,6 +330,7 @@ class ConfigDataEnvironment {
this.environment.setDefaultProfiles(StringUtils.toStringArray(profiles.getDefault()));
this.logger.trace(LogMessage.format("Setting active profiles: %s", profiles.getActive()));
this.environment.setActiveProfiles(StringUtils.toStringArray(profiles.getActive()));
this.environmentUpdateListener.onSetProfiles(profiles);
}
private void checkForInvalidProperties(ConfigDataEnvironmentContributors contributors) {

@ -63,11 +63,20 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
private final ConfigurableBootstrapContext bootstrapContext;
private final ConfigDataEnvironmentUpdateListener environmentUpdateListener;
public ConfigDataEnvironmentPostProcessor(DeferredLogFactory logFactory,
ConfigurableBootstrapContext bootstrapContext) {
this(logFactory, bootstrapContext, null);
}
public ConfigDataEnvironmentPostProcessor(DeferredLogFactory logFactory,
ConfigurableBootstrapContext bootstrapContext,
ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
this.logFactory = logFactory;
this.logger = logFactory.getLog(getClass());
this.bootstrapContext = bootstrapContext;
this.environmentUpdateListener = environmentUpdateListener;
}
@Override
@ -97,7 +106,7 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader,
additionalProfiles);
additionalProfiles, this.environmentUpdateListener);
}
private void postProcessUsingLegacyApplicationListener(ConfigurableEnvironment environment,
@ -154,6 +163,29 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
postProcessor.postProcessEnvironment(environment, resourceLoader, additionalProfiles);
}
/**
* Apply {@link ConfigData} post-processing to an existing {@link Environment}. This
* method can be useful when working with an {@link Environment} that has been created
* directly and not necessarily as part of a {@link SpringApplication}.
* @param environment the environment to apply {@link ConfigData} to
* @param resourceLoader the resource loader to use
* @param bootstrapContext the bootstrap context to use or {@code null} to use a
* throw-away context
* @param additionalProfiles any additional profiles that should be applied
* @param environmentUpdateListener optional
* {@link ConfigDataEnvironmentUpdateListener} that can be used to track
* {@link Environment} updates.
*/
public static void applyTo(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
ConfigurableBootstrapContext bootstrapContext, Collection<String> additionalProfiles,
ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
DeferredLogFactory logFactory = Supplier::get;
bootstrapContext = (bootstrapContext != null) ? bootstrapContext : new DefaultBootstrapContext();
ConfigDataEnvironmentPostProcessor postProcessor = new ConfigDataEnvironmentPostProcessor(logFactory,
bootstrapContext, environmentUpdateListener);
postProcessor.postProcessEnvironment(environment, resourceLoader, additionalProfiles);
}
@SuppressWarnings("deprecation")
static class LegacyConfigFileApplicationListener extends ConfigFileApplicationListener {

@ -0,0 +1,56 @@
/*
* Copyright 2012-2020 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.context.config;
import java.util.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
/**
* {@link EventListener} to listen to {@link Environment} updates triggered by the
* {@link ConfigDataEnvironmentPostProcessor}.
*
* @author Phillip Webb
* @since 2.4.2
*/
public interface ConfigDataEnvironmentUpdateListener extends EventListener {
/**
* A {@link ConfigDataEnvironmentUpdateListener} that does nothing.
*/
ConfigDataEnvironmentUpdateListener NONE = new ConfigDataEnvironmentUpdateListener() {
};
/**
* Called when a new {@link PropertySource} is added to the {@link Environment}.
* @param propertySource the {@link PropertySource} that was added
* @param location the original {@link ConfigDataLocation} of the source.
* @param resource the {@link ConfigDataResource} of the source.
*/
default void onPropertySourceAdded(PropertySource<?> propertySource, ConfigDataLocation location,
ConfigDataResource resource) {
}
/**
* Called when {@link Environment} profiles are set.
* @param profiles the profiles being set
*/
default void onSetProfiles(Profiles profiles) {
}
}

@ -16,6 +16,7 @@
package org.springframework.boot.context.config;
import java.util.Collections;
import java.util.Set;
import java.util.function.Supplier;
@ -119,9 +120,13 @@ class ConfigDataEnvironmentPostProcessorTests {
@Test
void applyToAppliesPostProcessing() {
int before = this.environment.getPropertySources().size();
ConfigDataEnvironmentPostProcessor.applyTo(this.environment, null, null, "dev");
TestConfigDataEnvironmentUpdateListener listener = new TestConfigDataEnvironmentUpdateListener();
ConfigDataEnvironmentPostProcessor.applyTo(this.environment, null, null, Collections.singleton("dev"),
listener);
assertThat(this.environment.getPropertySources().size()).isGreaterThan(before);
assertThat(this.environment.getActiveProfiles()).containsExactly("dev");
assertThat(listener.getAddedPropertySources()).hasSizeGreaterThan(0);
assertThat(listener.getProfiles().getActive()).containsExactly("dev");
}
}

@ -30,6 +30,7 @@ import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.DefaultBootstrapContext;
import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.ImportPhase;
import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.Kind;
import org.springframework.boot.context.config.TestConfigDataEnvironmentUpdateListener.AddedPropertySource;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.env.ConfigurableEnvironment;
@ -65,14 +66,14 @@ class ConfigDataEnvironmentTests {
this.environment.setProperty("spring.config.use-legacy-processing", "true");
assertThatExceptionOfType(UseLegacyConfigProcessingException.class)
.isThrownBy(() -> new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, this.environment,
this.resourceLoader, this.additionalProfiles));
this.resourceLoader, this.additionalProfiles, null));
}
@Test
void createExposesEnvironmentBinderToConfigDataLocationResolvers() {
this.environment.setProperty("spring", "boot");
TestConfigDataEnvironment configDataEnvironment = new TestConfigDataEnvironment(this.logFactory,
this.bootstrapContext, this.environment, this.resourceLoader, this.additionalProfiles);
this.bootstrapContext, this.environment, this.resourceLoader, this.additionalProfiles, null);
assertThat(configDataEnvironment.getConfigDataLocationResolversBinder().bind("spring", String.class).get())
.isEqualTo("boot");
}
@ -86,7 +87,7 @@ class ConfigDataEnvironmentTests {
this.environment.getPropertySources().addLast(propertySource2);
this.environment.getPropertySources().addLast(propertySource3);
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
List<ConfigDataEnvironmentContributor> children = configDataEnvironment.getContributors().getRoot()
.getChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION);
Object[] wrapped = children.stream().filter((child) -> child.getKind() == Kind.EXISTING)
@ -105,7 +106,7 @@ class ConfigDataEnvironmentTests {
this.environment.getPropertySources().addLast(propertySource1);
this.environment.getPropertySources().addLast(propertySource2);
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
List<ConfigDataEnvironmentContributor> children = configDataEnvironment.getContributors().getRoot()
.getChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION);
Object[] wrapped = children.stream().filter((child) -> child.getKind() == Kind.EXISTING)
@ -121,7 +122,7 @@ class ConfigDataEnvironmentTests {
this.environment.setProperty("spring.config.additional-location", "a1,a2");
this.environment.setProperty("spring.config.import", "i1,i2");
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
List<ConfigDataEnvironmentContributor> children = configDataEnvironment.getContributors().getRoot()
.getChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION);
Object[] imports = children.stream().filter((child) -> child.getKind() == Kind.INITIAL_IMPORT)
@ -133,7 +134,7 @@ class ConfigDataEnvironmentTests {
void processAndApplyAddsImportedSourceToEnvironment(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
assertThat(this.environment.getProperty("spring")).isEqualTo("boot");
}
@ -142,7 +143,7 @@ class ConfigDataEnvironmentTests {
void processAndApplyOnlyAddsActiveContributors(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
assertThat(this.environment.getProperty("spring")).isEqualTo("boot");
assertThat(this.environment.getProperty("other")).isNull();
@ -154,7 +155,7 @@ class ConfigDataEnvironmentTests {
this.environment.getPropertySources().addFirst(defaultPropertySource);
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
List<PropertySource<?>> sources = this.environment.getPropertySources().stream().collect(Collectors.toList());
assertThat(sources.get(sources.size() - 1)).isSameAs(defaultPropertySource);
@ -164,7 +165,7 @@ class ConfigDataEnvironmentTests {
void processAndApplySetsDefaultProfiles(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
assertThat(this.environment.getDefaultProfiles()).containsExactly("one", "two", "three");
}
@ -173,7 +174,7 @@ class ConfigDataEnvironmentTests {
void processAndApplySetsActiveProfiles(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
assertThat(this.environment.getActiveProfiles()).containsExactly("one", "two", "three");
}
@ -182,7 +183,7 @@ class ConfigDataEnvironmentTests {
void processAndApplySetsActiveProfilesAndProfileGroups(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
configDataEnvironment.processAndApply();
assertThat(this.environment.getActiveProfiles()).containsExactly("one", "four", "five", "two", "three");
}
@ -192,11 +193,36 @@ class ConfigDataEnvironmentTests {
void processAndApplyWhenHasInvalidPropertyThrowsException() {
this.environment.setProperty("spring.profile", "a");
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles);
this.environment, this.resourceLoader, this.additionalProfiles, null);
assertThatExceptionOfType(InvalidConfigDataPropertyException.class)
.isThrownBy(() -> configDataEnvironment.processAndApply());
}
@Test
void processAndApplyWhenHasListenerCallsOnPropertySourceAdded(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
TestConfigDataEnvironmentUpdateListener listener = new TestConfigDataEnvironmentUpdateListener();
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles, listener);
configDataEnvironment.processAndApply();
assertThat(listener.getAddedPropertySources()).hasSize(1);
AddedPropertySource addedPropertySource = listener.getAddedPropertySources().get(0);
assertThat(addedPropertySource.getPropertySource().getProperty("spring")).isEqualTo("boot");
assertThat(addedPropertySource.getLocation().toString()).isEqualTo(getConfigLocation(info));
assertThat(addedPropertySource.getResource().toString()).contains("class path resource")
.contains(info.getTestMethod().get().getName());
}
@Test
void processAndApplyWhenHasListenerCallsOnSetProfiles(TestInfo info) {
this.environment.setProperty("spring.config.location", getConfigLocation(info));
TestConfigDataEnvironmentUpdateListener listener = new TestConfigDataEnvironmentUpdateListener();
ConfigDataEnvironment configDataEnvironment = new ConfigDataEnvironment(this.logFactory, this.bootstrapContext,
this.environment, this.resourceLoader, this.additionalProfiles, listener);
configDataEnvironment.processAndApply();
assertThat(listener.getProfiles().getActive()).containsExactly("one", "two", "three");
}
private String getConfigLocation(TestInfo info) {
return "optional:classpath:" + info.getTestClass().get().getName().replace('.', '/') + "-"
+ info.getTestMethod().get().getName() + ".properties";
@ -208,8 +234,9 @@ class ConfigDataEnvironmentTests {
TestConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
super(logFactory, bootstrapContext, environment, resourceLoader, additionalProfiles);
Collection<String> additionalProfiles, ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
super(logFactory, bootstrapContext, environment, resourceLoader, additionalProfiles,
environmentUpdateListener);
}
@Override

@ -0,0 +1,79 @@
/*
* Copyright 2012-2020 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.context.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.core.env.PropertySource;
class TestConfigDataEnvironmentUpdateListener implements ConfigDataEnvironmentUpdateListener {
private final List<AddedPropertySource> addedPropertySources = new ArrayList<>();
private Profiles profiles;
@Override
public void onPropertySourceAdded(PropertySource<?> propertySource, ConfigDataLocation location,
ConfigDataResource resource) {
this.addedPropertySources.add(new AddedPropertySource(propertySource, location, resource));
}
@Override
public void onSetProfiles(Profiles profiles) {
this.profiles = profiles;
}
List<AddedPropertySource> getAddedPropertySources() {
return Collections.unmodifiableList(this.addedPropertySources);
}
Profiles getProfiles() {
return this.profiles;
}
static class AddedPropertySource {
private final PropertySource<?> propertySource;
private final ConfigDataLocation location;
private final ConfigDataResource resource;
AddedPropertySource(PropertySource<?> propertySource, ConfigDataLocation location,
ConfigDataResource resource) {
this.propertySource = propertySource;
this.location = location;
this.resource = resource;
}
PropertySource<?> getPropertySource() {
return this.propertySource;
}
ConfigDataLocation getLocation() {
return this.location;
}
ConfigDataResource getResource() {
return this.resource;
}
}
}
Loading…
Cancel
Save