Skip to content

Commit 353a30f

Browse files
committed
Handle constructor bound configuration properties in /configprops
This commit updates the configprops actuator endpoint to detect configuration properties that are bound using a constructor. See gh-18636
1 parent d254c62 commit 353a30f

File tree

2 files changed

+218
-3
lines changed

2 files changed

+218
-3
lines changed

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
package org.springframework.boot.actuate.context.properties;
1818

19+
import java.lang.reflect.Constructor;
1920
import java.util.ArrayList;
21+
import java.util.Arrays;
2022
import java.util.Collection;
2123
import java.util.Collections;
2224
import java.util.HashMap;
2325
import java.util.List;
2426
import java.util.Map;
27+
import java.util.stream.Collectors;
2528

2629
import com.fasterxml.jackson.annotation.JsonInclude.Include;
2730
import com.fasterxml.jackson.core.JsonGenerator;
@@ -45,14 +48,19 @@
4548
import org.apache.commons.logging.Log;
4649
import org.apache.commons.logging.LogFactory;
4750

51+
import org.springframework.beans.BeanUtils;
4852
import org.springframework.beans.BeansException;
4953
import org.springframework.boot.actuate.endpoint.Sanitizer;
5054
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
5155
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
5256
import org.springframework.boot.context.properties.ConfigurationProperties;
5357
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
58+
import org.springframework.boot.context.properties.ConstructorBinding;
5459
import org.springframework.context.ApplicationContext;
5560
import org.springframework.context.ApplicationContextAware;
61+
import org.springframework.core.KotlinDetector;
62+
import org.springframework.core.annotation.MergedAnnotations;
63+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
5664
import org.springframework.util.ClassUtils;
5765
import org.springframework.util.StringUtils;
5866

@@ -302,15 +310,30 @@ protected static class GenericSerializerModifier extends BeanSerializerModifier
302310
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
303311
List<BeanPropertyWriter> beanProperties) {
304312
List<BeanPropertyWriter> result = new ArrayList<>();
313+
Constructor<?> bindConstructor = findBindConstructor(beanDesc.getType().getRawClass());
305314
for (BeanPropertyWriter writer : beanProperties) {
306-
boolean readable = isReadable(beanDesc, writer);
307-
if (readable) {
315+
if (isCandidate(bindConstructor, beanDesc, writer)) {
308316
result.add(writer);
309317
}
310318
}
311319
return result;
312320
}
313321

322+
private boolean isCandidate(Constructor<?> bindConstructor, BeanDescription beanDesc,
323+
BeanPropertyWriter writer) {
324+
if (bindConstructor != null) {
325+
return isConstructorParameter(bindConstructor, writer);
326+
}
327+
else {
328+
return isReadable(beanDesc, writer);
329+
}
330+
}
331+
332+
private boolean isConstructorParameter(Constructor<?> bindConstructor, BeanPropertyWriter writer) {
333+
return Arrays.stream(bindConstructor.getParameters())
334+
.anyMatch((parameter) -> parameter.getName().equals(writer.getName()));
335+
}
336+
314337
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
315338
Class<?> parentType = beanDesc.getType().getRawClass();
316339
Class<?> type = writer.getType().getRawClass();
@@ -351,6 +374,34 @@ private String determineAccessorSuffix(String propertyName) {
351374
return StringUtils.capitalize(propertyName);
352375
}
353376

377+
private Constructor<?> findBindConstructor(Class<?> type) {
378+
boolean classConstructorBinding = MergedAnnotations
379+
.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
380+
.isPresent(ConstructorBinding.class);
381+
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
382+
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
383+
if (constructor != null) {
384+
return findBindConstructor(classConstructorBinding, constructor);
385+
}
386+
}
387+
return findBindConstructor(classConstructorBinding, type.getDeclaredConstructors());
388+
}
389+
390+
private Constructor<?> findBindConstructor(boolean classConstructorBinding, Constructor<?>... candidates) {
391+
List<Constructor<?>> allConstructors = Arrays.stream(candidates)
392+
.filter((constructor) -> constructor.getParameterCount() > 0).collect(Collectors.toList());
393+
List<Constructor<?>> flaggedConstructors = allConstructors.stream()
394+
.filter((candidate) -> MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class))
395+
.collect(Collectors.toList());
396+
if (flaggedConstructors.size() == 1) {
397+
return flaggedConstructors.get(0);
398+
}
399+
if (classConstructorBinding && allConstructors.size() == 1) {
400+
return allConstructors.get(0);
401+
}
402+
return null;
403+
}
404+
354405
}
355406

356407
/**

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

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
3232
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
3333
import org.springframework.boot.context.properties.ConfigurationProperties;
34+
import org.springframework.boot.context.properties.ConstructorBinding;
3435
import org.springframework.boot.context.properties.EnableConfigurationProperties;
36+
import org.springframework.boot.context.properties.bind.DefaultValue;
3537
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
3638
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3739
import org.springframework.boot.test.context.runner.ContextConsumer;
@@ -40,6 +42,7 @@
4042
import org.springframework.core.env.Environment;
4143

4244
import static org.assertj.core.api.Assertions.assertThat;
45+
import static org.assertj.core.api.Assertions.entry;
4346

4447
/**
4548
* Tests for {@link ConfigurationPropertiesReportEndpoint}.
@@ -55,11 +58,30 @@ class ConfigurationPropertiesReportEndpointTests {
5558
.withUserConfiguration(EndpointConfig.class);
5659

5760
@Test
58-
void descriptorDetectsRelevantProperties() {
61+
void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() {
5962
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
6063
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
6164
}
6265

66+
@Test
67+
void descriptorWithValueObjectBindMethodDetectsRelevantProperties() {
68+
this.contextRunner.withUserConfiguration(ImmutablePropertiesConfiguration.class).run(assertProperties(
69+
"immutable",
70+
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
71+
}
72+
73+
@Test
74+
void descriptorWithValueObjectBindMethodUseDedicatedConstructor() {
75+
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class).run(assertProperties(
76+
"multiconstructor", (properties) -> assertThat(properties).containsOnly(entry("name", "test"))));
77+
}
78+
79+
@Test
80+
void descriptorWithValueObjectBindMethodHandleNestedType() {
81+
this.contextRunner.withUserConfiguration(ImmutableNestedPropertiesConfiguration.class).run(assertProperties(
82+
"immutablenested", (properties) -> assertThat(properties).containsOnlyKeys("name", "nested")));
83+
}
84+
6385
@Test
6486
void descriptorDoesNotIncludePropertyWithNullValue() {
6587
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
@@ -236,6 +258,8 @@ public static class TestProperties {
236258

237259
private Duration duration = Duration.ofSeconds(10);
238260

261+
private String ignored = "dummy";
262+
239263
public String getDbPassword() {
240264
return this.dbPassword;
241265
}
@@ -268,6 +292,146 @@ public void setDuration(Duration duration) {
268292
this.duration = duration;
269293
}
270294

295+
public String getIgnored() {
296+
return this.ignored;
297+
}
298+
299+
}
300+
301+
@Configuration(proxyBeanMethods = false)
302+
@EnableConfigurationProperties(ImmutableProperties.class)
303+
static class ImmutablePropertiesConfiguration {
304+
305+
}
306+
307+
@ConfigurationProperties(prefix = "immutable")
308+
@ConstructorBinding
309+
public static class ImmutableProperties {
310+
311+
private final String dbPassword;
312+
313+
private final String myTestProperty;
314+
315+
private final String nullValue;
316+
317+
private final Duration duration;
318+
319+
private final String ignored;
320+
321+
ImmutableProperties(@DefaultValue("123456") String dbPassword, @DefaultValue("654321") String myTestProperty,
322+
String nullValue, @DefaultValue("10s") Duration duration) {
323+
this.dbPassword = dbPassword;
324+
this.myTestProperty = myTestProperty;
325+
this.nullValue = nullValue;
326+
this.duration = duration;
327+
this.ignored = "dummy";
328+
}
329+
330+
public String getDbPassword() {
331+
return this.dbPassword;
332+
}
333+
334+
public String getMyTestProperty() {
335+
return this.myTestProperty;
336+
}
337+
338+
public String getNullValue() {
339+
return this.nullValue;
340+
}
341+
342+
public Duration getDuration() {
343+
return this.duration;
344+
}
345+
346+
public String getIgnored() {
347+
return this.ignored;
348+
}
349+
350+
}
351+
352+
@Configuration(proxyBeanMethods = false)
353+
@EnableConfigurationProperties(MultiConstructorProperties.class)
354+
static class MultiConstructorPropertiesConfiguration {
355+
356+
}
357+
358+
@ConfigurationProperties(prefix = "multiconstructor")
359+
@ConstructorBinding
360+
public static class MultiConstructorProperties {
361+
362+
private final String name;
363+
364+
private final int counter;
365+
366+
MultiConstructorProperties(String name, int counter) {
367+
this.name = name;
368+
this.counter = counter;
369+
}
370+
371+
@ConstructorBinding
372+
MultiConstructorProperties(@DefaultValue("test") String name) {
373+
this.name = name;
374+
this.counter = 42;
375+
}
376+
377+
public String getName() {
378+
return this.name;
379+
}
380+
381+
public int getCounter() {
382+
return this.counter;
383+
}
384+
385+
}
386+
387+
@Configuration(proxyBeanMethods = false)
388+
@EnableConfigurationProperties(ImmutableNestedProperties.class)
389+
static class ImmutableNestedPropertiesConfiguration {
390+
391+
}
392+
393+
@ConfigurationProperties("immutablenested")
394+
@ConstructorBinding
395+
public static class ImmutableNestedProperties {
396+
397+
private final String name;
398+
399+
private final Nested nested;
400+
401+
ImmutableNestedProperties(@DefaultValue("parent") String name, Nested nested) {
402+
this.name = name;
403+
this.nested = nested;
404+
}
405+
406+
public String getName() {
407+
return this.name;
408+
}
409+
410+
public Nested getNested() {
411+
return this.nested;
412+
}
413+
414+
public static class Nested {
415+
416+
private final String name;
417+
418+
private final int counter;
419+
420+
Nested(@DefaultValue("nested") String name, int counter) {
421+
this.name = name;
422+
this.counter = counter;
423+
}
424+
425+
public String getName() {
426+
return this.name;
427+
}
428+
429+
public int getCounter() {
430+
return this.counter;
431+
}
432+
433+
}
434+
271435
}
272436

273437
@Configuration(proxyBeanMethods = false)

0 commit comments

Comments
 (0)