Skip to content

Commit 3d25385

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. Closes gh-18636
1 parent b60549d commit 3d25385

File tree

2 files changed

+219
-3
lines changed

2 files changed

+219
-3
lines changed

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

Lines changed: 49 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,26 @@ 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(beanDesc, writer, bindConstructor)) {
308316
result.add(writer);
309317
}
310318
}
311319
return result;
312320
}
313321

322+
private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer,
323+
Constructor<?> bindConstructor) {
324+
if (bindConstructor != null) {
325+
return Arrays.stream(bindConstructor.getParameters())
326+
.anyMatch((parameter) -> parameter.getName().equals(writer.getName()));
327+
}
328+
else {
329+
return isReadable(beanDesc, writer);
330+
}
331+
}
332+
314333
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
315334
Class<?> parentType = beanDesc.getType().getRawClass();
316335
Class<?> type = writer.getType().getRawClass();
@@ -351,6 +370,34 @@ private String determineAccessorSuffix(String propertyName) {
351370
return StringUtils.capitalize(propertyName);
352371
}
353372

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

356403
/**

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

Lines changed: 170 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,35 @@ 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.withPropertyValues("immutablenested.nested.name=nested", "immutablenested.nested.counter=42")
82+
.withUserConfiguration(ImmutableNestedPropertiesConfiguration.class)
83+
.run(assertProperties("immutablenested", (properties) -> {
84+
assertThat(properties).containsOnlyKeys("name", "nested");
85+
Map<String, Object> nested = (Map<String, Object>) properties.get("nested");
86+
assertThat(nested).containsOnly(entry("name", "nested"), entry("counter", 42));
87+
}));
88+
}
89+
6390
@Test
6491
void descriptorDoesNotIncludePropertyWithNullValue() {
6592
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
@@ -236,6 +263,8 @@ public static class TestProperties {
236263

237264
private Duration duration = Duration.ofSeconds(10);
238265

266+
private String ignored = "dummy";
267+
239268
public String getDbPassword() {
240269
return this.dbPassword;
241270
}
@@ -268,6 +297,146 @@ public void setDuration(Duration duration) {
268297
this.duration = duration;
269298
}
270299

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

273442
@Configuration(proxyBeanMethods = false)

0 commit comments

Comments
 (0)