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) -> {