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
pull/18732/head
Stephane Nicoll 5 years ago
parent b60549d6ca
commit 3d253854e9

@ -16,12 +16,15 @@
package org.springframework.boot.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
@ -45,14 +48,19 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
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.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@ -302,15 +310,26 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Constructor<?> bindConstructor = findBindConstructor(beanDesc.getType().getRawClass());
for (BeanPropertyWriter writer : beanProperties) {
boolean readable = isReadable(beanDesc, writer);
if (readable) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
}
}
return result;
}
private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer,
Constructor<?> bindConstructor) {
if (bindConstructor != null) {
return Arrays.stream(bindConstructor.getParameters())
.anyMatch((parameter) -> parameter.getName().equals(writer.getName()));
}
else {
return isReadable(beanDesc, writer);
}
}
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
@ -351,6 +370,34 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
return StringUtils.capitalize(propertyName);
}
private Constructor<?> findBindConstructor(Class<?> type) {
boolean classConstructorBinding = MergedAnnotations
.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
.isPresent(ConstructorBinding.class);
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
if (constructor != null) {
return findBindConstructor(classConstructorBinding, constructor);
}
}
return findBindConstructor(classConstructorBinding, type.getDeclaredConstructors());
}
private Constructor<?> findBindConstructor(boolean classConstructorBinding, Constructor<?>... candidates) {
List<Constructor<?>> candidateConstructors = Arrays.stream(candidates)
.filter((constructor) -> constructor.getParameterCount() > 0).collect(Collectors.toList());
List<Constructor<?>> flaggedConstructors = candidateConstructors.stream()
.filter((candidate) -> MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class))
.collect(Collectors.toList());
if (flaggedConstructors.size() == 1) {
return flaggedConstructors.get(0);
}
if (classConstructorBinding && candidateConstructors.size() == 1) {
return candidateConstructors.get(0);
}
return null;
}
}
/**

@ -31,7 +31,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
@ -40,6 +42,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link ConfigurationPropertiesReportEndpoint}.
@ -55,11 +58,35 @@ class ConfigurationPropertiesReportEndpointTests {
.withUserConfiguration(EndpointConfig.class);
@Test
void descriptorDetectsRelevantProperties() {
void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() {
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
(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")));
}
@Test
void descriptorWithValueObjectBindMethodUseDedicatedConstructor() {
this.contextRunner.withUserConfiguration(MultiConstructorPropertiesConfiguration.class).run(assertProperties(
"multiconstructor", (properties) -> assertThat(properties).containsOnly(entry("name", "test"))));
}
@Test
void descriptorWithValueObjectBindMethodHandleNestedType() {
this.contextRunner.withPropertyValues("immutablenested.nested.name=nested", "immutablenested.nested.counter=42")
.withUserConfiguration(ImmutableNestedPropertiesConfiguration.class)
.run(assertProperties("immutablenested", (properties) -> {
assertThat(properties).containsOnlyKeys("name", "nested");
Map<String, Object> nested = (Map<String, Object>) properties.get("nested");
assertThat(nested).containsOnly(entry("name", "nested"), entry("counter", 42));
}));
}
@Test
void descriptorDoesNotIncludePropertyWithNullValue() {
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
@ -236,6 +263,8 @@ class ConfigurationPropertiesReportEndpointTests {
private Duration duration = Duration.ofSeconds(10);
private String ignored = "dummy";
public String getDbPassword() {
return this.dbPassword;
}
@ -268,6 +297,146 @@ class ConfigurationPropertiesReportEndpointTests {
this.duration = duration;
}
public String getIgnored() {
return this.ignored;
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ImmutableProperties.class)
static class ImmutablePropertiesConfiguration {
}
@ConfigurationProperties(prefix = "immutable")
@ConstructorBinding
public static class ImmutableProperties {
private final String dbPassword;
private final String myTestProperty;
private final String nullValue;
private final Duration duration;
private final String ignored;
ImmutableProperties(@DefaultValue("123456") String dbPassword, @DefaultValue("654321") String myTestProperty,
String nullValue, @DefaultValue("10s") Duration duration) {
this.dbPassword = dbPassword;
this.myTestProperty = myTestProperty;
this.nullValue = nullValue;
this.duration = duration;
this.ignored = "dummy";
}
public String getDbPassword() {
return this.dbPassword;
}
public String getMyTestProperty() {
return this.myTestProperty;
}
public String getNullValue() {
return this.nullValue;
}
public Duration getDuration() {
return this.duration;
}
public String getIgnored() {
return this.ignored;
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(MultiConstructorProperties.class)
static class MultiConstructorPropertiesConfiguration {
}
@ConfigurationProperties(prefix = "multiconstructor")
@ConstructorBinding
public static class MultiConstructorProperties {
private final String name;
private final int counter;
MultiConstructorProperties(String name, int counter) {
this.name = name;
this.counter = counter;
}
@ConstructorBinding
MultiConstructorProperties(@DefaultValue("test") String name) {
this.name = name;
this.counter = 42;
}
public String getName() {
return this.name;
}
public int getCounter() {
return this.counter;
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ImmutableNestedProperties.class)
static class ImmutableNestedPropertiesConfiguration {
}
@ConfigurationProperties("immutablenested")
@ConstructorBinding
public static class ImmutableNestedProperties {
private final String name;
private final Nested nested;
ImmutableNestedProperties(@DefaultValue("parent") String name, Nested nested) {
this.name = name;
this.nested = nested;
}
public String getName() {
return this.name;
}
public Nested getNested() {
return this.nested;
}
public static class Nested {
private final String name;
private final int counter;
Nested(String name, int counter) {
this.name = name;
this.counter = counter;
}
public String getName() {
return this.name;
}
public int getCounter() {
return this.counter;
}
}
}
@Configuration(proxyBeanMethods = false)

Loading…
Cancel
Save