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) -> {
|
||||
|
||||
Reference in New Issue
Block a user