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;
|
||||
|
||||
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…
Reference in New Issue