Polish end-to-end configuration properties tracing

See gh-14880
This commit is contained in:
Phillip Webb
2019-12-13 11:31:24 -08:00
parent 830c2ef7f1
commit 695de2c6f5
9 changed files with 153 additions and 178 deletions

View File

@@ -54,9 +54,9 @@ import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.context.properties.BoundConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.boot.context.properties.ConfigurationPropertiesBoundPropertiesHolder;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
@@ -111,30 +111,73 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
}
private ApplicationConfigurationProperties extract(ApplicationContext context) {
Map<String, ContextConfigurationProperties> contextProperties = new HashMap<>();
ObjectMapper mapper = getObjectMapper();
Map<String, ContextConfigurationProperties> contexts = new HashMap<>();
ApplicationContext target = context;
while (target != null) {
contextProperties.put(target.getId(), describeConfigurationProperties(target, getObjectMapper()));
contexts.put(target.getId(), describeBeans(mapper, target));
target = target.getParent();
}
return new ApplicationConfigurationProperties(contextProperties);
return new ApplicationConfigurationProperties(contexts);
}
private ContextConfigurationProperties describeConfigurationProperties(ApplicationContext context,
ObjectMapper mapper) {
private ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
this.objectMapper = new ObjectMapper();
configureObjectMapper(this.objectMapper);
}
return this.objectMapper;
}
/**
* Configure Jackson's {@link ObjectMapper} to be used to serialize the
* {@link ConfigurationProperties @ConfigurationProperties} objects into a {@link Map}
* structure.
* @param mapper the object mapper
*/
protected void configureObjectMapper(ObjectMapper mapper) {
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
mapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
mapper.setSerializationInclusion(Include.NON_NULL);
applyConfigurationPropertiesFilter(mapper);
applySerializationModifier(mapper);
mapper.registerModule(new JavaTimeModule());
}
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
mapper.setFilterProvider(
new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param mapper the object mapper
*/
private void applySerializationModifier(ObjectMapper mapper) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
mapper.setSerializerFactory(factory);
}
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new HashMap<>();
beans.forEach((beanName, bean) -> {
String prefix = bean.getAnnotation().prefix();
descriptors.put(beanName,
new ConfigurationPropertiesBeanDescriptor(prefix,
sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix)),
getInputs(prefix, safeSerialize(mapper, bean.getInstance(), prefix))));
});
beans.forEach((beanName, bean) -> descriptors.put(beanName, describeBean(mapper, bean)));
return new ContextConfigurationProperties(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
}
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean) {
String prefix = bean.getAnnotation().prefix();
Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, Object> properties = sanitize(prefix, serialized);
Map<String, Object> inputs = getInputs(prefix, serialized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
}
/**
* Cautiously serialize the bean to a map (returning a map with an error message
* instead of throwing an exception if there is a problem).
@@ -153,47 +196,6 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
}
}
/**
* Configure Jackson's {@link ObjectMapper} to be used to serialize the
* {@link ConfigurationProperties @ConfigurationProperties} objects into a {@link Map}
* structure.
* @param mapper the object mapper
*/
protected void configureObjectMapper(ObjectMapper mapper) {
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
mapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
mapper.setSerializationInclusion(Include.NON_NULL);
applyConfigurationPropertiesFilter(mapper);
applySerializationModifier(mapper);
mapper.registerModule(new JavaTimeModule());
}
private ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
this.objectMapper = new ObjectMapper();
configureObjectMapper(this.objectMapper);
}
return this.objectMapper;
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param mapper the object mapper
*/
private void applySerializationModifier(ObjectMapper mapper) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
mapper.setSerializerFactory(factory);
}
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
mapper.setFilterProvider(
new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
}
/**
* Sanitize all unwanted configuration properties to avoid leaking of sensitive
* information.
@@ -204,7 +206,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
@SuppressWarnings("unchecked")
private Map<String, Object> sanitize(String prefix, Map<String, Object> map) {
map.forEach((key, value) -> {
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value));
}
@@ -239,19 +241,20 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
@SuppressWarnings("unchecked")
private Map<String, Object> getInputs(String prefix, Map<String, Object> map) {
Map<String, Object> augmented = new LinkedHashMap<>(map);
map.forEach((key, value) -> {
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
map.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
augmented.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
}
else if (value instanceof List) {
map.put(key, getInputs(qualifiedKey, (List<Object>) value));
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value));
}
else {
map.put(key, applyInput(qualifiedKey));
augmented.put(key, applyInput(qualifiedKey));
}
});
return map;
return augmented;
}
@SuppressWarnings("unchecked")
@@ -274,17 +277,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
}
private Map<String, Object> applyInput(String qualifiedKey) {
if (!this.context.containsBean(ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME)) {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
if (bound == null) {
return Collections.emptyMap();
}
ConfigurationPropertiesBoundPropertiesHolder bean = this.context.getBean(
ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME,
ConfigurationPropertiesBoundPropertiesHolder.class);
Map<ConfigurationPropertyName, ConfigurationProperty> boundProperties = bean.getProperties();
ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.');
ConfigurationProperty candidate = boundProperties.get(currentName);
ConfigurationProperty candidate = bound.get(currentName);
if (candidate == null && currentName.isLastElementIndexed()) {
candidate = boundProperties.get(currentName.chop(currentName.getNumberOfElements() - 1));
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
}
return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap();
}
@@ -298,11 +298,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
return input;
}
private String getQualifiedKey(String prefix, String key) {
return (prefix.isEmpty() ? prefix : prefix + ".") + key;
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
@SuppressWarnings("serial")
private static class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override

View File

@@ -52,6 +52,7 @@ import static org.assertj.core.api.Assertions.entry;
* @author Stephane Nicoll
* @author HaiTao Zhang
*/
@SuppressWarnings("unchecked")
class ConfigurationPropertiesReportEndpointTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -60,26 +61,20 @@ class ConfigurationPropertiesReportEndpointTests {
@Test
void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() {
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration"),
(inputs) -> {
}));
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
}
@Test
void descriptorWithValueObjectBindMethodDetectsRelevantProperties() {
this.contextRunner.withUserConfiguration(ImmutablePropertiesConfiguration.class).run(assertProperties(
"immutable",
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration"),
(inputs) -> {
}));
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
}
@Test
void descriptorWithValueObjectBindMethodUseDedicatedConstructor() {
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class)
.run(assertProperties("multiconstructor",
(properties) -> assertThat(properties).containsOnly(entry("name", "test")), (inputs) -> {
}));
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class).run(assertProperties(
"multiconstructor", (properties) -> assertThat(properties).containsOnly(entry("name", "test"))));
}
@Test
@@ -125,54 +120,44 @@ class ConfigurationPropertiesReportEndpointTests {
@Test
void descriptorDoesNotIncludePropertyWithNullValue() {
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
(properties) -> assertThat(properties).doesNotContainKey("nullValue"), (inputs) -> {
}));
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
.run(assertProperties("test", (properties) -> assertThat(properties).doesNotContainKey("nullValue")));
}
@Test
void descriptorWithDurationProperty() {
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
(properties) -> assertThat(properties.get("duration")).isEqualTo(Duration.ofSeconds(10).toString()),
(inputs) -> {
}));
(properties) -> assertThat(properties.get("duration")).isEqualTo(Duration.ofSeconds(10).toString())));
}
@Test
void descriptorWithNonCamelCaseProperty() {
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class)
.run(assertProperties("mixedcase",
(properties) -> assertThat(properties.get("myURL")).isEqualTo("https://example.com"),
(inputs) -> {
}));
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
"mixedcase", (properties) -> assertThat(properties.get("myURL")).isEqualTo("https://example.com")));
}
@Test
void descriptorWithMixedCaseProperty() {
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
"mixedcase", (properties) -> assertThat(properties.get("mIxedCase")).isEqualTo("mixed"), (inputs) -> {
}));
"mixedcase", (properties) -> assertThat(properties.get("mIxedCase")).isEqualTo("mixed")));
}
@Test
void descriptorWithSingleLetterProperty() {
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
"mixedcase", (properties) -> assertThat(properties.get("z")).isEqualTo("zzz"), (inputs) -> {
}));
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class)
.run(assertProperties("mixedcase", (properties) -> assertThat(properties.get("z")).isEqualTo("zzz")));
}
@Test
void descriptorWithSimpleBooleanProperty() {
this.contextRunner.withUserConfiguration(BooleanPropertiesConfiguration.class).run(assertProperties("boolean",
(properties) -> assertThat(properties.get("simpleBoolean")).isEqualTo(true), (inputs) -> {
}));
(properties) -> assertThat(properties.get("simpleBoolean")).isEqualTo(true)));
}
@Test
void descriptorWithMixedBooleanProperty() {
this.contextRunner.withUserConfiguration(BooleanPropertiesConfiguration.class).run(assertProperties("boolean",
(properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true), (inputs) -> {
}));
(properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true)));
}
@Test
@@ -181,7 +166,6 @@ class ConfigurationPropertiesReportEndpointTests {
.run(assertProperties("test", (properties) -> {
assertThat(properties.get("dbPassword")).isEqualTo("******");
assertThat(properties.get("myTestProperty")).isEqualTo("654321");
}, (inputs) -> {
}));
}
@@ -191,7 +175,6 @@ class ConfigurationPropertiesReportEndpointTests {
.withPropertyValues("test.keys-to-sanitize=property").run(assertProperties("test", (properties) -> {
assertThat(properties.get("dbPassword")).isEqualTo("123456");
assertThat(properties.get("myTestProperty")).isEqualTo("******");
}, (inputs) -> {
}));
}
@@ -201,7 +184,6 @@ class ConfigurationPropertiesReportEndpointTests {
.withPropertyValues("test.keys-to-sanitize=.*pass.*").run(assertProperties("test", (properties) -> {
assertThat(properties.get("dbPassword")).isEqualTo("******");
assertThat(properties.get("myTestProperty")).isEqualTo("654321");
}, (inputs) -> {
}));
}
@@ -215,7 +197,6 @@ class ConfigurationPropertiesReportEndpointTests {
assertThat(secrets.get("mine")).isEqualTo("******");
assertThat(secrets.get("yours")).isEqualTo("******");
assertThat(hidden.get("mine")).isEqualTo("******");
}, (inputs) -> {
}));
}
@@ -292,6 +273,12 @@ class ConfigurationPropertiesReportEndpointTests {
}));
}
private ContextConsumer<AssertableApplicationContext> assertProperties(String prefix,
Consumer<Map<String, Object>> properties) {
return assertProperties(prefix, properties, (inputs) -> {
});
}
private ContextConsumer<AssertableApplicationContext> assertProperties(String prefix,
Consumer<Map<String, Object>> properties, Consumer<Map<String, Object>> inputs) {
return (context) -> {

View File

@@ -16,48 +16,73 @@
package org.springframework.boot.context.properties;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.BoundPropertiesHolder;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
/**
* {@link BoundPropertiesHolder} for
* Bean to record and provide bound
* {@link ConfigurationProperties @ConfigurationProperties}.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public class ConfigurationPropertiesBoundPropertiesHolder implements BoundPropertiesHolder {
public class BoundConfigurationProperties {
private Map<ConfigurationPropertyName, ConfigurationProperty> properties = new LinkedHashMap<>();
/**
* The bean name that this class is registered with.
*/
public static final String BEAN_NAME = ConfigurationPropertiesBoundPropertiesHolder.class.getName();
private static final String BEAN_NAME = BoundConfigurationProperties.class.getName();
@Override
public void recordBinding(ConfigurationProperty configurationProperty) {
Assert.notNull(configurationProperty, "ConfigurationProperty should not be null");
void add(ConfigurationProperty configurationProperty) {
this.properties.put(configurationProperty.getName(), configurationProperty);
}
public Map<ConfigurationPropertyName, ConfigurationProperty> getProperties() {
return this.properties;
/**
* Get the configuration property bound to the given name.
* @param name the property name
* @return the bound property or {@code null}
*/
public ConfigurationProperty get(ConfigurationPropertyName name) {
return this.properties.get(name);
}
/**
* Get all bound properties.
* @return a map of all bound properties
*/
public Map<ConfigurationPropertyName, ConfigurationProperty> getAll() {
return Collections.unmodifiableMap(this.properties);
}
/**
* Return the {@link BoundConfigurationProperties} from the given
* {@link ApplicationContext} if it is available.
* @param context the context to search
* @return a {@link BoundConfigurationProperties} or {@code null}
*/
public static BoundConfigurationProperties get(ApplicationContext context) {
if (!context.containsBeanDefinition(BEAN_NAME)) {
return null;
}
return context.getBean(BEAN_NAME, BoundConfigurationProperties.class);
}
static void register(BeanDefinitionRegistry registry) {
Assert.notNull(registry, "Registry must not be null");
if (!registry.containsBeanDefinition(BEAN_NAME)) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBoundPropertiesHolder.class);
definition.setBeanClass(BoundConfigurationProperties.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN_NAME, definition);
}

View File

@@ -104,18 +104,9 @@ class ConfigurationPropertiesBinder {
return null;
}
private ConfigurationPropertiesBoundPropertiesHolder getBoundPropertiesHolder() {
if (this.applicationContext.containsBean(ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME)) {
return this.applicationContext.getBean(ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME,
ConfigurationPropertiesBoundPropertiesHolder.class);
}
return null;
}
private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
List<Validator> validators = getValidators(target);
ConfigurationPropertiesBoundPropertiesHolder holder = getBoundPropertiesHolder();
BindHandler handler = getHandler(holder);
BindHandler handler = getHandler();
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
@@ -132,9 +123,10 @@ class ConfigurationPropertiesBinder {
return handler;
}
private IgnoreTopLevelConverterNotFoundBindHandler getHandler(ConfigurationPropertiesBoundPropertiesHolder holder) {
return (holder != null)
? new IgnoreTopLevelConverterNotFoundBindHandler(new BoundPropertiesTrackingBindHandler(holder))
private IgnoreTopLevelConverterNotFoundBindHandler getHandler() {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.applicationContext);
return (bound != null)
? new IgnoreTopLevelConverterNotFoundBindHandler(new BoundPropertiesTrackingBindHandler(bound::add))
: new IgnoreTopLevelConverterNotFoundBindHandler();
}

View File

@@ -49,7 +49,7 @@ class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegi
@SuppressWarnings("deprecation")
static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
ConfigurationPropertiesBindingPostProcessor.register(registry);
ConfigurationPropertiesBoundPropertiesHolder.register(registry);
BoundConfigurationProperties.register(registry);
ConfigurationPropertiesBeanDefinitionValidator.register(registry);
ConfigurationBeanFactoryMetadata.register(registry);
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2012-2019 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.properties.bind;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
/**
* Record bound configuration properties.
*
* @author Madhura Bhave
* @since 2.3.0
*/
public interface BoundPropertiesHolder {
/**
* Record the bound configuration property.
* @param configurationProperty the bound property
*/
void recordBinding(ConfigurationProperty configurationProperty);
}

View File

@@ -16,6 +16,9 @@
package org.springframework.boot.context.properties.bind;
import java.util.function.Consumer;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.util.Assert;
@@ -27,18 +30,17 @@ import org.springframework.util.Assert;
*/
public class BoundPropertiesTrackingBindHandler extends AbstractBindHandler {
private final BoundPropertiesHolder holder;
private final Consumer<ConfigurationProperty> consumer;
public BoundPropertiesTrackingBindHandler(BoundPropertiesHolder holder) {
super();
Assert.notNull(holder, "Bound properties holder should not be null.");
this.holder = holder;
public BoundPropertiesTrackingBindHandler(Consumer<ConfigurationProperty> consumer) {
Assert.notNull(consumer, "Consumer must not be null");
this.consumer = consumer;
}
@Override
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
if (context.getConfigurationProperty() != null && name.equals(context.getConfigurationProperty().getName())) {
this.holder.recordBinding(context.getConfigurationProperty());
this.consumer.accept(context.getConfigurationProperty());
}
return super.onSuccess(name, target, context, result);
}

View File

@@ -923,11 +923,9 @@ class ConfigurationPropertiesTests {
@Test
void boundPropertiesShouldBeRecorded() {
load(NestedConfiguration.class, "name=foo", "nested.name=bar");
ConfigurationPropertiesBoundPropertiesHolder recorder = this.context.getBean(
ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME,
ConfigurationPropertiesBoundPropertiesHolder.class);
assertThat(recorder.getProperties().keySet().stream().map(ConfigurationPropertyName::toString)).contains("name",
"nested.name");
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
Set<ConfigurationPropertyName> keys = bound.getAll().keySet();
assertThat(keys.stream().map(ConfigurationPropertyName::toString)).contains("name", "nested.name");
}
private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {

View File

@@ -18,16 +18,18 @@ package org.springframework.boot.context.properties.bind;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -45,21 +47,22 @@ public class BoundPropertiesTrackingBindHandlerTests {
private Binder binder;
private BoundPropertiesHolder recorder;
@Mock
private Consumer<ConfigurationProperty> consumer;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.binder = new Binder(this.sources);
this.recorder = mock(BoundPropertiesHolder.class);
this.handler = new BoundPropertiesTrackingBindHandler(this.recorder);
this.handler = new BoundPropertiesTrackingBindHandler(this.consumer);
}
@Test
void handlerShouldCallRecordBindingIfConfigurationPropertyIsNotNull() {
this.sources.add(new MockConfigurationPropertySource("foo.age", 4));
this.binder.bind("foo", Bindable.of(ExampleBean.class), this.handler);
verify(this.recorder, times(1)).recordBinding(any(ConfigurationProperty.class));
verify(this.recorder, never()).recordBinding(null);
verify(this.consumer, times(1)).accept(any(ConfigurationProperty.class));
verify(this.consumer, never()).accept(null);
}
static class ExampleBean {