Polish end-to-end configuration properties tracing
See gh-14880
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) -> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user