Skip to content

Commit 695de2c

Browse files
committed
Polish end-to-end configuration properties tracing
See gh-14880
1 parent 830c2ef commit 695de2c

File tree

9 files changed

+143
-168
lines changed

9 files changed

+143
-168
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java

Lines changed: 61 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
import org.springframework.boot.actuate.endpoint.Sanitizer;
5555
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
5656
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
57+
import org.springframework.boot.context.properties.BoundConfigurationProperties;
5758
import org.springframework.boot.context.properties.ConfigurationProperties;
5859
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
59-
import org.springframework.boot.context.properties.ConfigurationPropertiesBoundPropertiesHolder;
6060
import org.springframework.boot.context.properties.ConstructorBinding;
6161
import org.springframework.boot.context.properties.source.ConfigurationProperty;
6262
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
@@ -111,46 +111,22 @@ public ApplicationConfigurationProperties configurationProperties() {
111111
}
112112

113113
private ApplicationConfigurationProperties extract(ApplicationContext context) {
114-
Map<String, ContextConfigurationProperties> contextProperties = new HashMap<>();
114+
ObjectMapper mapper = getObjectMapper();
115+
Map<String, ContextConfigurationProperties> contexts = new HashMap<>();
115116
ApplicationContext target = context;
116117
while (target != null) {
117-
contextProperties.put(target.getId(), describeConfigurationProperties(target, getObjectMapper()));
118+
contexts.put(target.getId(), describeBeans(mapper, target));
118119
target = target.getParent();
119120
}
120-
return new ApplicationConfigurationProperties(contextProperties);
121+
return new ApplicationConfigurationProperties(contexts);
121122
}
122123

123-
private ContextConfigurationProperties describeConfigurationProperties(ApplicationContext context,
124-
ObjectMapper mapper) {
125-
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
126-
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new HashMap<>();
127-
beans.forEach((beanName, bean) -> {
128-
String prefix = bean.getAnnotation().prefix();
129-
descriptors.put(beanName,
130-
new ConfigurationPropertiesBeanDescriptor(prefix,
131-
sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix)),
132-
getInputs(prefix, safeSerialize(mapper, bean.getInstance(), prefix))));
133-
});
134-
return new ContextConfigurationProperties(descriptors,
135-
(context.getParent() != null) ? context.getParent().getId() : null);
136-
}
137-
138-
/**
139-
* Cautiously serialize the bean to a map (returning a map with an error message
140-
* instead of throwing an exception if there is a problem).
141-
* @param mapper the object mapper
142-
* @param bean the source bean
143-
* @param prefix the prefix
144-
* @return the serialized instance
145-
*/
146-
@SuppressWarnings("unchecked")
147-
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
148-
try {
149-
return new HashMap<>(mapper.convertValue(bean, Map.class));
150-
}
151-
catch (Exception ex) {
152-
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
124+
private ObjectMapper getObjectMapper() {
125+
if (this.objectMapper == null) {
126+
this.objectMapper = new ObjectMapper();
127+
configureObjectMapper(this.objectMapper);
153128
}
129+
return this.objectMapper;
154130
}
155131

156132
/**
@@ -170,12 +146,10 @@ protected void configureObjectMapper(ObjectMapper mapper) {
170146
mapper.registerModule(new JavaTimeModule());
171147
}
172148

173-
private ObjectMapper getObjectMapper() {
174-
if (this.objectMapper == null) {
175-
this.objectMapper = new ObjectMapper();
176-
configureObjectMapper(this.objectMapper);
177-
}
178-
return this.objectMapper;
149+
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
150+
mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
151+
mapper.setFilterProvider(
152+
new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
179153
}
180154

181155
/**
@@ -188,10 +162,38 @@ private void applySerializationModifier(ObjectMapper mapper) {
188162
mapper.setSerializerFactory(factory);
189163
}
190164

191-
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
192-
mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
193-
mapper.setFilterProvider(
194-
new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
165+
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context) {
166+
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
167+
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new HashMap<>();
168+
beans.forEach((beanName, bean) -> descriptors.put(beanName, describeBean(mapper, bean)));
169+
return new ContextConfigurationProperties(descriptors,
170+
(context.getParent() != null) ? context.getParent().getId() : null);
171+
}
172+
173+
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean) {
174+
String prefix = bean.getAnnotation().prefix();
175+
Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
176+
Map<String, Object> properties = sanitize(prefix, serialized);
177+
Map<String, Object> inputs = getInputs(prefix, serialized);
178+
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
179+
}
180+
181+
/**
182+
* Cautiously serialize the bean to a map (returning a map with an error message
183+
* instead of throwing an exception if there is a problem).
184+
* @param mapper the object mapper
185+
* @param bean the source bean
186+
* @param prefix the prefix
187+
* @return the serialized instance
188+
*/
189+
@SuppressWarnings("unchecked")
190+
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
191+
try {
192+
return new HashMap<>(mapper.convertValue(bean, Map.class));
193+
}
194+
catch (Exception ex) {
195+
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
196+
}
195197
}
196198

197199
/**
@@ -204,7 +206,7 @@ private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
204206
@SuppressWarnings("unchecked")
205207
private Map<String, Object> sanitize(String prefix, Map<String, Object> map) {
206208
map.forEach((key, value) -> {
207-
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
209+
String qualifiedKey = getQualifiedKey(prefix, key);
208210
if (value instanceof Map) {
209211
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value));
210212
}
@@ -239,19 +241,20 @@ else if (item instanceof List) {
239241

240242
@SuppressWarnings("unchecked")
241243
private Map<String, Object> getInputs(String prefix, Map<String, Object> map) {
244+
Map<String, Object> augmented = new LinkedHashMap<>(map);
242245
map.forEach((key, value) -> {
243-
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
246+
String qualifiedKey = getQualifiedKey(prefix, key);
244247
if (value instanceof Map) {
245-
map.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
248+
augmented.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
246249
}
247250
else if (value instanceof List) {
248-
map.put(key, getInputs(qualifiedKey, (List<Object>) value));
251+
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value));
249252
}
250253
else {
251-
map.put(key, applyInput(qualifiedKey));
254+
augmented.put(key, applyInput(qualifiedKey));
252255
}
253256
});
254-
return map;
257+
return augmented;
255258
}
256259

257260
@SuppressWarnings("unchecked")
@@ -274,17 +277,14 @@ else if (item instanceof List) {
274277
}
275278

276279
private Map<String, Object> applyInput(String qualifiedKey) {
277-
if (!this.context.containsBean(ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME)) {
280+
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
281+
if (bound == null) {
278282
return Collections.emptyMap();
279283
}
280-
ConfigurationPropertiesBoundPropertiesHolder bean = this.context.getBean(
281-
ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME,
282-
ConfigurationPropertiesBoundPropertiesHolder.class);
283-
Map<ConfigurationPropertyName, ConfigurationProperty> boundProperties = bean.getProperties();
284284
ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.');
285-
ConfigurationProperty candidate = boundProperties.get(currentName);
285+
ConfigurationProperty candidate = bound.get(currentName);
286286
if (candidate == null && currentName.isLastElementIndexed()) {
287-
candidate = boundProperties.get(currentName.chop(currentName.getNumberOfElements() - 1));
287+
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
288288
}
289289
return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap();
290290
}
@@ -298,11 +298,14 @@ private Map<String, Object> getInput(String property, ConfigurationProperty cand
298298
return input;
299299
}
300300

301+
private String getQualifiedKey(String prefix, String key) {
302+
return (prefix.isEmpty() ? prefix : prefix + ".") + key;
303+
}
304+
301305
/**
302306
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
303307
* properties.
304308
*/
305-
@SuppressWarnings("serial")
306309
private static class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
307310

308311
@Override

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
* @author Stephane Nicoll
5353
* @author HaiTao Zhang
5454
*/
55+
@SuppressWarnings("unchecked")
5556
class ConfigurationPropertiesReportEndpointTests {
5657

5758
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -60,26 +61,20 @@ class ConfigurationPropertiesReportEndpointTests {
6061
@Test
6162
void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() {
6263
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
63-
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration"),
64-
(inputs) -> {
65-
}));
64+
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
6665
}
6766

6867
@Test
6968
void descriptorWithValueObjectBindMethodDetectsRelevantProperties() {
7069
this.contextRunner.withUserConfiguration(ImmutablePropertiesConfiguration.class).run(assertProperties(
7170
"immutable",
72-
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration"),
73-
(inputs) -> {
74-
}));
71+
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
7572
}
7673

7774
@Test
7875
void descriptorWithValueObjectBindMethodUseDedicatedConstructor() {
79-
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class)
80-
.run(assertProperties("multiconstructor",
81-
(properties) -> assertThat(properties).containsOnly(entry("name", "test")), (inputs) -> {
82-
}));
76+
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class).run(assertProperties(
77+
"multiconstructor", (properties) -> assertThat(properties).containsOnly(entry("name", "test"))));
8378
}
8479

8580
@Test
@@ -125,54 +120,44 @@ void descriptorWithSimpleList() {
125120

126121
@Test
127122
void descriptorDoesNotIncludePropertyWithNullValue() {
128-
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
129-
(properties) -> assertThat(properties).doesNotContainKey("nullValue"), (inputs) -> {
130-
}));
123+
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
124+
.run(assertProperties("test", (properties) -> assertThat(properties).doesNotContainKey("nullValue")));
131125
}
132126

133127
@Test
134128
void descriptorWithDurationProperty() {
135129
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
136-
(properties) -> assertThat(properties.get("duration")).isEqualTo(Duration.ofSeconds(10).toString()),
137-
(inputs) -> {
138-
}));
130+
(properties) -> assertThat(properties.get("duration")).isEqualTo(Duration.ofSeconds(10).toString())));
139131
}
140132

141133
@Test
142134
void descriptorWithNonCamelCaseProperty() {
143-
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class)
144-
.run(assertProperties("mixedcase",
145-
(properties) -> assertThat(properties.get("myURL")).isEqualTo("https://example.com"),
146-
(inputs) -> {
147-
}));
135+
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
136+
"mixedcase", (properties) -> assertThat(properties.get("myURL")).isEqualTo("https://example.com")));
148137
}
149138

150139
@Test
151140
void descriptorWithMixedCaseProperty() {
152141
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
153-
"mixedcase", (properties) -> assertThat(properties.get("mIxedCase")).isEqualTo("mixed"), (inputs) -> {
154-
}));
142+
"mixedcase", (properties) -> assertThat(properties.get("mIxedCase")).isEqualTo("mixed")));
155143
}
156144

157145
@Test
158146
void descriptorWithSingleLetterProperty() {
159-
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class).run(assertProperties(
160-
"mixedcase", (properties) -> assertThat(properties.get("z")).isEqualTo("zzz"), (inputs) -> {
161-
}));
147+
this.contextRunner.withUserConfiguration(MixedCasePropertiesConfiguration.class)
148+
.run(assertProperties("mixedcase", (properties) -> assertThat(properties.get("z")).isEqualTo("zzz")));
162149
}
163150

164151
@Test
165152
void descriptorWithSimpleBooleanProperty() {
166153
this.contextRunner.withUserConfiguration(BooleanPropertiesConfiguration.class).run(assertProperties("boolean",
167-
(properties) -> assertThat(properties.get("simpleBoolean")).isEqualTo(true), (inputs) -> {
168-
}));
154+
(properties) -> assertThat(properties.get("simpleBoolean")).isEqualTo(true)));
169155
}
170156

171157
@Test
172158
void descriptorWithMixedBooleanProperty() {
173159
this.contextRunner.withUserConfiguration(BooleanPropertiesConfiguration.class).run(assertProperties("boolean",
174-
(properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true), (inputs) -> {
175-
}));
160+
(properties) -> assertThat(properties.get("mixedBoolean")).isEqualTo(true)));
176161
}
177162

178163
@Test
@@ -181,7 +166,6 @@ void sanitizeWithDefaultSettings() {
181166
.run(assertProperties("test", (properties) -> {
182167
assertThat(properties.get("dbPassword")).isEqualTo("******");
183168
assertThat(properties.get("myTestProperty")).isEqualTo("654321");
184-
}, (inputs) -> {
185169
}));
186170
}
187171

@@ -191,7 +175,6 @@ void sanitizeWithCustomKey() {
191175
.withPropertyValues("test.keys-to-sanitize=property").run(assertProperties("test", (properties) -> {
192176
assertThat(properties.get("dbPassword")).isEqualTo("123456");
193177
assertThat(properties.get("myTestProperty")).isEqualTo("******");
194-
}, (inputs) -> {
195178
}));
196179
}
197180

@@ -201,7 +184,6 @@ void sanitizeWithCustomKeyPattern() {
201184
.withPropertyValues("test.keys-to-sanitize=.*pass.*").run(assertProperties("test", (properties) -> {
202185
assertThat(properties.get("dbPassword")).isEqualTo("******");
203186
assertThat(properties.get("myTestProperty")).isEqualTo("654321");
204-
}, (inputs) -> {
205187
}));
206188
}
207189

@@ -215,7 +197,6 @@ void sanitizeWithCustomPatternUsingCompositeKeys() {
215197
assertThat(secrets.get("mine")).isEqualTo("******");
216198
assertThat(secrets.get("yours")).isEqualTo("******");
217199
assertThat(hidden.get("mine")).isEqualTo("******");
218-
}, (inputs) -> {
219200
}));
220201
}
221202

@@ -292,6 +273,12 @@ void listsOfListsAreSanitized() {
292273
}));
293274
}
294275

276+
private ContextConsumer<AssertableApplicationContext> assertProperties(String prefix,
277+
Consumer<Map<String, Object>> properties) {
278+
return assertProperties(prefix, properties, (inputs) -> {
279+
});
280+
}
281+
295282
private ContextConsumer<AssertableApplicationContext> assertProperties(String prefix,
296283
Consumer<Map<String, Object>> properties, Consumer<Map<String, Object>> inputs) {
297284
return (context) -> {

0 commit comments

Comments
 (0)