Allow @ConstructorBinding to be optional

This commit makes @ConstructorBinding optional for a type
that has a single parameterized constructor. An @Autowired annotation
on any of the constructors indicates that the type should not be constructor
bound.

Since @ConstructorBinding is now deduced for a single parameterized constructor,
the annotation is no longer needed at the type level.

Closes gh-23216
This commit is contained in:
Madhura Bhave
2021-12-10 09:45:48 -08:00
parent bc2c637d63
commit 44b88cc88c
44 changed files with 693 additions and 391 deletions

View File

@@ -19,7 +19,6 @@ package org.springframework.boot.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -52,7 +51,6 @@ 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.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer;
@@ -63,7 +61,8 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.context.properties.BoundConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
@@ -72,11 +71,9 @@ import org.springframework.boot.origin.Origin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -472,7 +469,9 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Constructor<?> bindConstructor = findBindConstructor(ClassUtils.getUserClass(beanClass));
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
@@ -540,34 +539,6 @@ 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;
}
}
/**

View File

@@ -28,6 +28,7 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
@@ -72,6 +73,12 @@ class ConfigurationPropertiesReportEndpointTests {
(properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration")));
}
@Test
void descriptorWithAutowiredConstructorBindMethodDetectsRelevantProperties() {
this.contextRunner.withUserConfiguration(AutowiredPropertiesConfiguration.class)
.run(assertProperties("autowired", (properties) -> assertThat(properties).containsOnlyKeys("counter")));
}
@Test
void descriptorWithValueObjectBindMethodDetectsRelevantProperties() {
this.contextRunner.withUserConfiguration(ImmutablePropertiesConfiguration.class).run(assertProperties(
@@ -489,7 +496,6 @@ class ConfigurationPropertiesReportEndpointTests {
}
@ConfigurationProperties(prefix = "immutable")
@ConstructorBinding
public static class ImmutableProperties {
private final String dbPassword;
@@ -540,7 +546,6 @@ class ConfigurationPropertiesReportEndpointTests {
}
@ConfigurationProperties(prefix = "multiconstructor")
@ConstructorBinding
public static class MultiConstructorProperties {
private final String name;
@@ -568,6 +573,43 @@ class ConfigurationPropertiesReportEndpointTests {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AutowiredProperties.class)
static class AutowiredPropertiesConfiguration {
@Bean
String hello() {
return "hello";
}
}
@ConfigurationProperties(prefix = "autowired")
public static class AutowiredProperties {
private final String name;
private int counter;
@Autowired
AutowiredProperties(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public int getCounter() {
return this.counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ImmutableNestedProperties.class)
static class ImmutableNestedPropertiesConfiguration {
@@ -575,7 +617,6 @@ class ConfigurationPropertiesReportEndpointTests {
}
@ConfigurationProperties("immutablenested")
@ConstructorBinding
public static class ImmutableNestedProperties {
private final String name;

View File

@@ -17,7 +17,6 @@
package org.springframework.boot.actuate.context.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.validation.annotation.Validated;
/**
@@ -27,7 +26,6 @@ import org.springframework.validation.annotation.Validated;
* @author Madhura Bhave
*/
@Validated
@ConstructorBinding
@ConfigurationProperties(prefix = "validated")
public class ValidatedConstructorBindingProperties {