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
This commit is contained in:
parent
b60549d6ca
commit
3d253854e9
|
|
@ -16,12 +16,15 @@
|
||||||
|
|
||||||
package org.springframework.boot.actuate.context.properties;
|
package org.springframework.boot.actuate.context.properties;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
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.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.BeansException;
|
import org.springframework.beans.BeansException;
|
||||||
import org.springframework.boot.actuate.endpoint.Sanitizer;
|
import org.springframework.boot.actuate.endpoint.Sanitizer;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ApplicationContextAware;
|
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.ClassUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
|
@ -302,15 +310,26 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||||
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
|
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
|
||||||
List<BeanPropertyWriter> beanProperties) {
|
List<BeanPropertyWriter> beanProperties) {
|
||||||
List<BeanPropertyWriter> result = new ArrayList<>();
|
List<BeanPropertyWriter> result = new ArrayList<>();
|
||||||
|
Constructor<?> bindConstructor = findBindConstructor(beanDesc.getType().getRawClass());
|
||||||
for (BeanPropertyWriter writer : beanProperties) {
|
for (BeanPropertyWriter writer : beanProperties) {
|
||||||
boolean readable = isReadable(beanDesc, writer);
|
if (isCandidate(beanDesc, writer, bindConstructor)) {
|
||||||
if (readable) {
|
|
||||||
result.add(writer);
|
result.add(writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
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) {
|
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
|
||||||
Class<?> parentType = beanDesc.getType().getRawClass();
|
Class<?> parentType = beanDesc.getType().getRawClass();
|
||||||
Class<?> type = writer.getType().getRawClass();
|
Class<?> type = writer.getType().getRawClass();
|
||||||
|
|
@ -351,6 +370,34 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||||
return StringUtils.capitalize(propertyName);
|
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.ConfigurationPropertiesBeanDescriptor;
|
||||||
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
|
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
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.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
import org.springframework.boot.test.context.runner.ContextConsumer;
|
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 org.springframework.core.env.Environment;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ConfigurationPropertiesReportEndpoint}.
|
* Tests for {@link ConfigurationPropertiesReportEndpoint}.
|
||||||
|
|
@ -55,11 +58,35 @@ class ConfigurationPropertiesReportEndpointTests {
|
||||||
.withUserConfiguration(EndpointConfig.class);
|
.withUserConfiguration(EndpointConfig.class);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void descriptorDetectsRelevantProperties() {
|
void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() {
|
||||||
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
|
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class).run(assertProperties("test",
|
||||||
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
|
(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
|
@Test
|
||||||
void descriptorDoesNotIncludePropertyWithNullValue() {
|
void descriptorDoesNotIncludePropertyWithNullValue() {
|
||||||
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
|
this.contextRunner.withUserConfiguration(TestPropertiesConfiguration.class)
|
||||||
|
|
@ -236,6 +263,8 @@ class ConfigurationPropertiesReportEndpointTests {
|
||||||
|
|
||||||
private Duration duration = Duration.ofSeconds(10);
|
private Duration duration = Duration.ofSeconds(10);
|
||||||
|
|
||||||
|
private String ignored = "dummy";
|
||||||
|
|
||||||
public String getDbPassword() {
|
public String getDbPassword() {
|
||||||
return this.dbPassword;
|
return this.dbPassword;
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +297,146 @@ class ConfigurationPropertiesReportEndpointTests {
|
||||||
this.duration = duration;
|
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)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue