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 {

View File

@ -17,7 +17,6 @@
package org.springframework.boot.autoconfigure.diagnostics.analyzer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -41,15 +40,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionEvaluationRepor
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer;
import org.springframework.context.annotation.Bean;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
@ -115,27 +109,6 @@ class NoSuchBeanDefinitionFailureAnalyzer extends AbstractInjectionFailureAnalyz
(!autoConfigurationResults.isEmpty() || !userConfigurationResults.isEmpty())
? "revisiting the entries above or defining" : "defining",
getBeanDescription(cause));
if (injectionPoint != null && injectionPoint.getMember() instanceof Constructor) {
Constructor<?> constructor = (Constructor<?>) injectionPoint.getMember();
Class<?> declaringClass = constructor.getDeclaringClass();
MergedAnnotation<ConfigurationProperties> configurationProperties = MergedAnnotations.from(declaringClass)
.get(ConfigurationProperties.class);
if (configurationProperties.isPresent()) {
if (KotlinDetector.isKotlinType(declaringClass) && !KotlinDetector.isKotlinReflectPresent()) {
action = String.format(
"%s%nConsider adding a dependency on kotlin-reflect so that the constructor used for @%s can be located. Also, ensure that @%s is present on '%s' if you intended to use constructor-based "
+ "configuration property binding.",
action, ConstructorBinding.class.getSimpleName(), ConstructorBinding.class.getSimpleName(),
constructor.getName());
}
else {
action = String.format(
"%s%nConsider adding @%s to %s if you intended to use constructor-based "
+ "configuration property binding.",
action, ConstructorBinding.class.getSimpleName(), constructor.getName());
}
}
}
return new FailureAnalysis(message.toString(), action, cause);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -30,8 +30,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionEvaluationRepor
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
import org.springframework.boot.system.JavaVersion;
@ -164,16 +162,6 @@ class NoSuchBeanDefinitionFailureAnalyzerTests {
return "@org.springframework.beans.factory.annotation.Qualifier\\(value=\"*alpha\"*\\)";
}
@Test
void failureAnalysisForConfigurationPropertiesThatMaybeShouldHaveBeenConstructorBound() {
FailureAnalysis analysis = analyzeFailure(
createFailure(ConstructorBoundConfigurationPropertiesConfiguration.class));
assertThat(analysis.getAction()).startsWith(
String.format("Consider defining a bean of type '%s' in your configuration.", String.class.getName()));
assertThat(analysis.getAction()).contains(
"Consider adding @ConstructorBinding to " + NeedsConstructorBindingProperties.class.getName());
}
private void assertDescriptionConstructorMissingType(FailureAnalysis analysis, Class<?> component, int index,
Class<?> type) {
String expected = String.format(
@ -379,25 +367,4 @@ class NoSuchBeanDefinitionFailureAnalyzerTests {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(NeedsConstructorBindingProperties.class)
static class ConstructorBoundConfigurationPropertiesConfiguration {
}
@ConfigurationProperties("test")
static class NeedsConstructorBindingProperties {
private final String name;
NeedsConstructorBindingProperties(String name) {
this.name = name;
}
String getName() {
return this.name;
}
}
}

View File

@ -1,67 +0,0 @@
package org.springframework.boot.autoconfigure.diagnostics.analyzer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.FatalBeanException
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.diagnostics.FailureAnalysis
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.boot.testsupport.classpath.ClassPathExclusions
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Configuration
/**
* Tests for {@link ConfigurationProperties @ConfigurationProperties}-annotated beans when kotlin-reflect is not present
* on the classpath.
*
* @author Madhura Bhave
*/
@ClassPathExclusions("kotlin-reflect*.jar")
class KotlinNoSuchBeanFailureAnalyzerNoKotlinReflectTests {
private val analyzer = NoSuchBeanDefinitionFailureAnalyzer()
@Test
fun failureAnalysisForConfigurationPropertiesThatMaybeShouldHaveBeenConstructorBound() {
val analysis = analyzeFailure(
createFailure(ConstructorBoundConfigurationPropertiesConfiguration::class.java))
assertThat(analysis!!.getAction()).startsWith(
java.lang.String.format("Consider defining a bean of type '%s' in your configuration.", String::class.java.getName()))
assertThat(analysis.getAction()).contains(java.lang.String.format(
"Consider adding a dependency on kotlin-reflect so that the constructor used for @ConstructorBinding can be located. Also, ensure that @ConstructorBinding is present on '%s' ",ConstructorBoundProperties::class.java.getName()))
}
private fun createFailure(config: Class<*>, vararg environment: String): FatalBeanException? {
try {
AnnotationConfigApplicationContext().use { context ->
this.analyzer.setBeanFactory(context.beanFactory)
TestPropertyValues.of(*environment).applyTo(context)
context.register(config)
context.refresh()
return null
}
} catch (ex: FatalBeanException) {
return ex
}
}
private fun analyzeFailure(failure: Exception?): FailureAnalysis? {
val analysis = this.analyzer.analyze(failure)
if (analysis != null) {
LoggingFailureAnalysisReporter().report(analysis)
}
return analysis
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ConstructorBoundProperties::class)
internal class ConstructorBoundConfigurationPropertiesConfiguration
@ConfigurationProperties("test")
@ConstructorBinding
internal class ConstructorBoundProperties(val name: String)
}

View File

@ -63,7 +63,8 @@ You could also let the AspectJ plugin run all the processing and disable annotat
=== Automatic Metadata Generation
The processor picks up both classes and methods that are annotated with `@ConfigurationProperties`.
If the class is also annotated with `@ConstructorBinding`, a single constructor is expected and one property is created per constructor parameter.
If the class has a single parameterized constructor, one property is created per constructor parameter, unless the constructor is annotated with `@Autowired`.
If the class has a constructor explicitly annotated with `@ConstructorBinding`, one property is created per constructor parameter for that constructor.
Otherwise, properties are discovered through the presence of standard getters and setters with special handling for collection and map types (that is detected even if only a getter is present).
The annotation processor also supports the use of the `@Data`, `@Value`, `@Getter`, and `@Setter` lombok annotations.

View File

@ -704,12 +704,14 @@ The example in the previous section can be rewritten in an immutable fashion as
include::code:MyProperties[]
In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used.
This means that the binder will expect to find a constructor with the parameters that you wish to have bound.
In this setup, the presence of a single parameterized constructor implies that constructor binding should be used.
This means that the binder will find a constructor with the parameters that you wish to have bound.
If your class has multiple constructors, the `@ConstructorBinding` annotation can be used to specify which constructor to use for constructor binding.
To opt out of constructor binding for a class with a single parameterized constructor, the constructor must be annotated with `@Autowired`.
If you are using Java 16 or later, constructor binding can be used with records.
In this case, unless your record has multiple constructors, there is no need to use `@ConstructorBinding`.
Unless your record has multiple constructors, there is no need to use `@ConstructorBinding`.
Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound through their constructor.
Nested members of a constructor bound class (such as `Security` in the example above) will also be bound through their constructor.
Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property.
By default, if no properties are bound to `Security`, the `MyProperties` instance will contain a `null` value for `security`.
@ -721,8 +723,6 @@ include::code:nonnull/MyProperties[tag=*]
NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning.
You cannot use constructor binding with beans that are created by the regular Spring mechanisms (for example `@Component` beans, beans created by using `@Bean` methods or beans loaded by using `@Import`)
TIP: If you have more than one constructor for your class you can also use `@ConstructorBinding` directly on the constructor that should be bound.
NOTE: The use of `java.util.Optional` with `@ConfigurationProperties` is not recommended as it is primarily intended for use as a return type.
As such, it is not well-suited to configuration property injection.
For consistency with properties of other types, if you do declare an `Optional` property and it has no value, `null` rather than an empty `Optional` will be bound.

View File

@ -108,11 +108,10 @@ TIP: `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency is provided b
[[features.kotlin.configuration-properties]]
=== @ConfigurationProperties
`@ConfigurationProperties` when used in combination with <<features#features.external-config.typesafe-configuration-properties.constructor-binding,`@ConstructorBinding`>> supports classes with immutable `val` properties as shown in the following example:
`@ConfigurationProperties` when used in combination with <<features#features.external-config.typesafe-configuration-properties.constructor-binding,constructor binding>> supports classes with immutable `val` properties as shown in the following example:
[source,kotlin,indent=0,subs="verbatim"]
----
@ConstructorBinding
@ConfigurationProperties("example.kotlin")
data class KotlinExampleProperties(
val name: String,

View File

@ -20,10 +20,8 @@ import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConstructorBinding
@ConfigurationProperties("my.service")
public class MyProperties {

View File

@ -20,10 +20,8 @@ import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConstructorBinding
@ConfigurationProperties("my.service")
public class MyProperties {

View File

@ -17,14 +17,12 @@
package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.datasizes.constructorbinding;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
@ConfigurationProperties("my")
@ConstructorBinding
public class MyProperties {
// @fold:on // fields...

View File

@ -20,12 +20,10 @@ import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
@ConfigurationProperties("my")
@ConstructorBinding
public class MyProperties {
// @fold:on // fields...

View File

@ -27,7 +27,6 @@ import org.springframework.boot.context.properties.bind.DefaultValue;
*
* @author Stephane Nicoll
*/
@ConstructorBinding
@ConfigurationProperties("example")
public class ExampleProperties {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -79,6 +79,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding";
static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired";
static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.context.properties.bind.DefaultValue";
static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint";
@ -122,6 +124,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return CONSTRUCTOR_BINDING_ANNOTATION;
}
protected String autowiredAnnotation() {
return AUTOWIRED_ANNOTATION;
}
protected String defaultValueAnnotation() {
return DEFAULT_VALUE_ANNOTATION;
}
@ -156,7 +162,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
constructorBindingAnnotation(), autowiredAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
readOperationAnnotation(), nameAnnotation());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -99,10 +99,12 @@ class MetadataGenerationEnvironment {
private final String nameAnnotation;
private final String autowiredAnnotation;
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
String constructorBindingAnnotation, String defaultValueAnnotation, Set<String> endpointAnnotations,
String readOperationAnnotation, String nameAnnotation) {
String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation,
Set<String> endpointAnnotations, String readOperationAnnotation, String nameAnnotation) {
this.typeUtils = new TypeUtils(environment);
this.elements = environment.getElementUtils();
this.messager = environment.getMessager();
@ -111,6 +113,7 @@ class MetadataGenerationEnvironment {
this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation;
this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation;
this.constructorBindingAnnotation = constructorBindingAnnotation;
this.autowiredAnnotation = autowiredAnnotation;
this.defaultValueAnnotation = defaultValueAnnotation;
this.endpointAnnotations = endpointAnnotations;
this.readOperationAnnotation = readOperationAnnotation;
@ -180,14 +183,14 @@ class MetadataGenerationEnvironment {
return new ItemDeprecation(reason, replacement);
}
boolean hasConstructorBindingAnnotation(TypeElement typeElement) {
return hasAnnotationRecursive(typeElement, this.constructorBindingAnnotation);
}
boolean hasConstructorBindingAnnotation(ExecutableElement element) {
return hasAnnotation(element, this.constructorBindingAnnotation);
}
boolean hasAutowiredAnnotation(ExecutableElement element) {
return hasAnnotation(element, this.autowiredAnnotation);
}
boolean hasAnnotation(Element element, String type) {
return getAnnotation(element, type) != null;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.boot.configurationprocessor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -191,16 +192,29 @@ class PropertyDescriptorResolver {
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
boolean constructorBoundType = isConstructorBoundType(type, env);
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
List<ExecutableElement> boundConstructors = constructors.stream()
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
List<ExecutableElement> boundConstructors = getBoundConstructors(env, constructors);
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
}
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
if (env.hasConstructorBindingAnnotation(type)
|| "java.lang.Record".equals(type.getSuperclass().toString())) {
return true;
private static List<ExecutableElement> getBoundConstructors(MetadataGenerationEnvironment env,
List<ExecutableElement> constructors) {
ExecutableElement bindConstructor = deduceBindConstructor(constructors, env);
if (bindConstructor != null) {
return Collections.singletonList(bindConstructor);
}
return constructors.stream().filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
}
private static ExecutableElement deduceBindConstructor(List<ExecutableElement> constructors,
MetadataGenerationEnvironment env) {
if (constructors.size() == 1 && constructors.get(0).getParameters().size() > 0
&& !env.hasAutowiredAnnotation(constructors.get(0))) {
return constructors.get(0);
}
return null;
}
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
if (type.getNestingKind() == NestingKind.MEMBER) {
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
}

View File

@ -410,21 +410,6 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
compile(RecursiveProperties.class);
}
@Test
@EnabledForJreRange(min = JRE.JAVA_16)
void explicityBoundRecordProperties(@TempDir File temp) throws IOException {
File exampleRecord = new File(temp, "ExampleRecord.java");
try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) {
writer.println("@org.springframework.boot.configurationsample.ConstructorBinding");
writer.println("@org.springframework.boot.configurationsample.ConfigurationProperties(\"explicit\")");
writer.println("public record ExampleRecord(String someString, Integer someInteger) {");
writer.println("}");
}
ConfigurationMetadata metadata = compile(exampleRecord);
assertThat(metadata).has(Metadata.withProperty("explicit.some-string"));
assertThat(metadata).has(Metadata.withProperty("explicit.some-integer"));
}
@Test
@EnabledForJreRange(min = JRE.JAVA_16)
void implicitlyBoundRecordProperties(@TempDir File temp) throws IOException {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -45,6 +45,7 @@ class MetadataGenerationEnvironmentFactory implements Function<ProcessingEnviron
TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.CONSTRUCTOR_BINDING_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.AUTOWIRED_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, endpointAnnotations,
TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.NAME_ANNOTATION);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -35,6 +35,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
import org.springframework.boot.configurationsample.immutable.ImmutableClassConstructorBindingProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableDeducedConstructorBindingProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableMultiConstructorProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties;
import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties;
@ -42,12 +43,11 @@ import org.springframework.boot.configurationsample.lombok.LombokExplicitPropert
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleValueProperties;
import org.springframework.boot.configurationsample.simple.AutowiredProperties;
import org.springframework.boot.configurationsample.simple.HierarchicalProperties;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesGrandparent;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesParent;
import org.springframework.boot.configurationsample.simple.SimpleProperties;
import org.springframework.boot.configurationsample.specific.MatchingConstructorNoDirectiveProperties;
import org.springframework.boot.configurationsample.specific.TwoConstructorsClassConstructorBindingExample;
import org.springframework.boot.configurationsample.specific.TwoConstructorsExample;
import org.springframework.boot.testsupport.compiler.TestCompiler;
@ -111,6 +111,14 @@ class PropertyDescriptorResolverTests {
(stream) -> assertThat(stream).containsExactly("name", "description", "counter", "number", "items")));
}
@Test
void propertiesWithDeducedConstructorBinding() throws IOException {
process(ImmutableDeducedConstructorBindingProperties.class,
propertyNames((stream) -> assertThat(stream).containsExactly("theName", "flag")));
process(ImmutableDeducedConstructorBindingProperties.class, properties((stream) -> assertThat(stream)
.allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor)));
}
@Test
void propertiesWithConstructorWithConstructorBinding() throws IOException {
process(ImmutableSimpleProperties.class, propertyNames(
@ -128,16 +136,9 @@ class PropertyDescriptorResolverTests {
}
@Test
void propertiesWithConstructorAndClassConstructorBindingAndSeveralCandidates() throws IOException {
process(TwoConstructorsClassConstructorBindingExample.class,
propertyNames((stream) -> assertThat(stream).isEmpty()));
}
@Test
void propertiesWithConstructorNoDirective() throws IOException {
process(MatchingConstructorNoDirectiveProperties.class,
propertyNames((stream) -> assertThat(stream).containsExactly("name")));
process(MatchingConstructorNoDirectiveProperties.class, properties((stream) -> assertThat(stream)
void propertiesWithAutowiredConstructor() throws IOException {
process(AutowiredProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("theName")));
process(AutowiredProperties.class, properties((stream) -> assertThat(stream)
.allMatch((predicate) -> predicate instanceof JavaBeanPropertyDescriptor)));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -59,6 +59,8 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
public static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.configurationsample.ConstructorBinding";
public static final String AUTOWIRED_ANNOTATION = "org.springframework.boot.configurationsample.Autowired";
public static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.configurationsample.DefaultValue";
public static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.ControllerEndpoint";
@ -105,6 +107,11 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
return CONSTRUCTOR_BINDING_ANNOTATION;
}
@Override
protected String autowiredAnnotation() {
return AUTOWIRED_ANNOTATION;
}
@Override
protected String defaultValueAnnotation() {
return DEFAULT_VALUE_ANNOTATION;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,10 +22,15 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
/**
* Alternative to Spring Framework's {@code @Autowired} for testing (removes the need for
* a dependency on the real annotation).
*
* @author Madhura Bhave
*/
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConstructorBinding
public @interface MetaConstructorBinding {
public @interface Autowired {
}

View File

@ -28,7 +28,7 @@ import java.lang.annotation.Target;
*
* @author Stephane Nicoll
*/
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {

View File

@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.DefaultValue;
/**
@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.DefaultValue;
* @author Phillip Webb
*/
@ConfigurationProperties("test")
@ConstructorBinding
public class DeducedImmutableClassProperties {
private final Nested nested;

View File

@ -16,15 +16,12 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* Simple immutable properties with several constructors.
*
* @author Stephane Nicoll
*/
@SuppressWarnings("unused")
@MetaConstructorBinding
public class ImmutableClassConstructorBindingProperties {
private final String name;

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.DefaultValue;
/**
* @author Madhura Bhave
*/
@ConfigurationProperties("immutable")
public class ImmutableDeducedConstructorBindingProperties {
/**
* The name of these properties.
*/
private final String theName;
/**
* A simple flag.
*/
private final boolean flag;
public ImmutableDeducedConstructorBindingProperties(@DefaultValue("boot") String theName, boolean flag) {
this.theName = theName;
this.flag = flag;
}
public String getTheName() {
return this.theName;
}
public boolean isFlag() {
return this.flag;
}
}

View File

@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.immutable;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.Name;
/**
@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.Name;
* @author Phillip Webb
*/
@ConfigurationProperties("named")
@ConstructorBinding
public class ImmutableNameAnnotationProperties {
private final String imports;

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.simple;
import org.springframework.boot.configurationsample.Autowired;
/**
* Properties with autowired constructor.
*
* @author Madhura Bhave
*/
public class AutowiredProperties {
/**
* The name of this simple properties.
*/
private String theName;
@Autowired
public AutowiredProperties(String theName) {
this.theName = theName;
}
public String getTheName() {
return this.theName;
}
public void setTheName(String name) {
this.theName = name;
}
}

View File

@ -17,7 +17,6 @@
package org.springframework.boot.configurationsample.specific;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.ConstructorBinding;
import org.springframework.boot.configurationsample.DefaultValue;
/**
@ -27,7 +26,6 @@ import org.springframework.boot.configurationsample.DefaultValue;
* @author Stephane Nicoll
*/
@ConfigurationProperties("test")
@ConstructorBinding
public class InvalidDefaultValueFloatingPointProperties {
private final Double ratio;

View File

@ -18,7 +18,6 @@ package org.springframework.boot.configurationsample.specific;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.DefaultValue;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* Demonstrates that an invalid default number value leads to a compilation failure.
@ -26,7 +25,6 @@ import org.springframework.boot.configurationsample.MetaConstructorBinding;
* @author Stephane Nicoll
*/
@ConfigurationProperties("test")
@MetaConstructorBinding
public class InvalidDefaultValueNumberProperties {
private final int counter;

View File

@ -1,51 +0,0 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.specific;
import org.springframework.boot.configurationsample.MetaConstructorBinding;
/**
* A type that declares constructor binding but with two available constructors.
*
* @author Stephane Nicoll
*/
@MetaConstructorBinding
@SuppressWarnings("unused")
public class TwoConstructorsClassConstructorBindingExample {
private String name;
private String description;
public TwoConstructorsClassConstructorBindingExample(String name) {
this(name, null);
}
public TwoConstructorsClassConstructorBindingExample(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -71,11 +71,16 @@ public final class ConfigurationPropertiesBean {
private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
Bindable<?> bindTarget) {
this(name, instance, annotation, bindTarget, BindMethod.forType(bindTarget.getType().resolve()));
}
private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
Bindable<?> bindTarget, BindMethod bindMethod) {
this.name = name;
this.instance = instance;
this.annotation = annotation;
this.bindTarget = bindTarget;
this.bindMethod = BindMethod.forType(bindTarget.getType().resolve());
this.bindMethod = bindMethod;
}
/**
@ -267,6 +272,9 @@ public final class ConfigurationPropertiesBean {
if (instance != null) {
bindTarget = bindTarget.withExistingValue(instance);
}
if (factory != null) {
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget, BindMethod.JAVA_BEAN);
}
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,11 +17,11 @@
package org.springframework.boot.context.properties;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.Assert;
@ -31,10 +31,14 @@ import org.springframework.util.Assert;
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 3.0.0
*/
class ConfigurationPropertiesBindConstructorProvider implements BindConstructorProvider {
public class ConfigurationPropertiesBindConstructorProvider implements BindConstructorProvider {
static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider();
/**
* A shared singleton {@link ConfigurationPropertiesBindConstructorProvider} instance.
*/
public static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider();
@Override
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
@ -45,26 +49,88 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP
if (type == null) {
return null;
}
Constructor<?> constructor = findConstructorBindingAnnotatedConstructor(type);
if (constructor == null && (isConstructorBindingType(type) || isNestedConstructorBinding)) {
constructor = deduceBindConstructor(type);
Constructors constructors = Constructors.getConstructors(type);
if (constructors.getBind() != null || isNestedConstructorBinding) {
Assert.state(!constructors.hasAutowired(),
() -> type.getName() + " declares @ConstructorBinding and @Autowired constructor");
}
return constructor;
return constructors.getBind();
}
private Constructor<?> findConstructorBindingAnnotatedConstructor(Class<?> type) {
if (isKotlinType(type)) {
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
if (constructor != null) {
return findAnnotatedConstructor(type, constructor);
/**
* Data holder for autowired and bind constructors.
*/
static final class Constructors {
private final boolean hasAutowired;
private final Constructor<?> bind;
private Constructors(boolean hasAutowired, Constructor<?> bind) {
this.hasAutowired = hasAutowired;
this.bind = bind;
}
boolean hasAutowired() {
return this.hasAutowired;
}
Constructor<?> getBind() {
return this.bind;
}
static Constructors getConstructors(Class<?> type) {
Constructor<?>[] candidates = getCandidateConstructors(type);
Constructor<?> deducedBind = deduceBindConstructor(candidates);
if (deducedBind != null) {
return new Constructors(false, deducedBind);
}
boolean hasAutowiredConstructor = false;
Constructor<?> bind = null;
for (Constructor<?> candidate : candidates) {
if (isAutowired(candidate)) {
hasAutowiredConstructor = true;
continue;
}
bind = findAnnotatedConstructor(type, bind, candidate);
}
return new Constructors(hasAutowiredConstructor, bind);
}
private static Constructor<?>[] getCandidateConstructors(Class<?> type) {
if (isInnerClass(type)) {
return new Constructor<?>[0];
}
return Arrays.stream(type.getDeclaredConstructors())
.filter((constructor) -> isNonSynthetic(constructor, type)).toArray(Constructor[]::new);
}
private static boolean isInnerClass(Class<?> type) {
try {
return type.getDeclaredField("this$0").isSynthetic();
}
catch (NoSuchFieldException ex) {
return false;
}
}
return findAnnotatedConstructor(type, type.getDeclaredConstructors());
}
private Constructor<?> findAnnotatedConstructor(Class<?> type, Constructor<?>... candidates) {
Constructor<?> constructor = null;
for (Constructor<?> candidate : candidates) {
private static boolean isNonSynthetic(Constructor<?> constructor, Class<?> type) {
return !constructor.isSynthetic();
}
private static Constructor<?> deduceBindConstructor(Constructor<?>[] constructors) {
if (constructors.length == 1 && constructors[0].getParameterCount() > 0 && !isAutowired(constructors[0])) {
return constructors[0];
}
return null;
}
private static boolean isAutowired(Constructor<?> candidate) {
return MergedAnnotations.from(candidate).isPresent(Autowired.class);
}
private static Constructor<?> findAnnotatedConstructor(Class<?> type, Constructor<?> constructor,
Constructor<?> candidate) {
if (MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class)) {
Assert.state(candidate.getParameterCount() > 0,
() -> type.getName() + " declares @ConstructorBinding on a no-args constructor");
@ -72,45 +138,9 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP
() -> type.getName() + " has more than one @ConstructorBinding constructor");
constructor = candidate;
}
return constructor;
}
return constructor;
}
private boolean isConstructorBindingType(Class<?> type) {
return isImplicitConstructorBindingType(type) || isConstructorBindingAnnotatedType(type);
}
private boolean isImplicitConstructorBindingType(Class<?> type) {
Class<?> superclass = type.getSuperclass();
return (superclass != null) && "java.lang.Record".equals(superclass.getName());
}
private boolean isConstructorBindingAnnotatedType(Class<?> type) {
return MergedAnnotations.from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
.isPresent(ConstructorBinding.class);
}
private Constructor<?> deduceBindConstructor(Class<?> type) {
if (isKotlinType(type)) {
return deducedKotlinBindConstructor(type);
}
Constructor<?>[] constructors = type.getDeclaredConstructors();
if (constructors.length == 1 && constructors[0].getParameterCount() > 0) {
return constructors[0];
}
return null;
}
private Constructor<?> deducedKotlinBindConstructor(Class<?> type) {
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
return primaryConstructor;
}
return null;
}
private boolean isKotlinType(Class<?> type) {
return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type);
}
}

View File

@ -23,9 +23,10 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that can be used to indicate that configuration properties should be bound
* using constructor arguments rather than by calling setters. Can be added at the type
* level (if there is an unambiguous constructor) or on the actual constructor to use.
* Annotation that can be used to indicate which constructor to use when binding
* configuration properties using constructor arguments rather than by calling setters. A
* single parameterized constructor implicitly indicates that constructor binding should
* be used unless the constructor is annotated with `@Autowired`.
* <p>
* Note: To use constructor binding the class must be enabled using
* {@link EnableConfigurationProperties @EnableConfigurationProperties} or configuration
@ -39,7 +40,7 @@ import java.lang.annotation.Target;
* @since 2.2.0
* @see ConfigurationProperties
*/
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {

View File

@ -102,7 +102,6 @@ class ConfigurationPropertiesBeanRegistrarTests {
}
@ConstructorBinding
@ConfigurationProperties("valuecp")
static class ValueObjectConfigurationProperties {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -27,6 +27,7 @@ import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -47,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link ConfigurationPropertiesBean}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class ConfigurationPropertiesBeanTests {
@ -266,14 +268,14 @@ class ConfigurationPropertiesBeanTests {
}
@Test
void bindTypeForTypeWhenNoConstructorBindingOnTypeReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingOnType.class);
void bindTypeForTypeWhenConstructorBindingOnConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
}
@Test
void bindTypeForTypeWhenNoConstructorBindingOnConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class);
void bindTypeForTypeWhenNoConstructorBindingAnnotationOnSingleParameterizedConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingNoAnnotation.class);
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
}
@ -285,6 +287,42 @@ class ConfigurationPropertiesBeanTests {
+ " has more than one @ConstructorBinding constructor");
}
@Test
void bindTypeForTypeWithMultipleConstructorsReturnJavaBean() {
BindMethod bindType = BindMethod.forType(NoConstructorBindingOnMultipleConstructors.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void bindTypeForTypeWithNoArgConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.forType(JavaBeanWithNoArgConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void bindTypeForTypeWithSingleArgAutowiredConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.forType(JavaBeanWithAutowiredConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void constructorBindingAndAutowiredConstructorsShouldThrowException() {
assertThatIllegalStateException()
.isThrownBy(() -> BindMethod.forType(ConstructorBindingAndAutowiredConstructors.class));
}
@Test
void innerClassWithSyntheticFieldShouldReturnJavaBean() {
BindMethod bindType = BindMethod.forType(Inner.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void innerClassWithParameterizedConstructorShouldReturnJavaBean() {
BindMethod bindType = BindMethod.forType(ParameterizedConstructorInner.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
private void get(Class<?> configuration, String beanName, ThrowingConsumer<ConfigurationPropertiesBean> consumer)
throws Throwable {
get(configuration, beanName, true, consumer);
@ -448,7 +486,6 @@ class ConfigurationPropertiesBeanTests {
}
@ConfigurationProperties
@ConstructorBinding
static class ValueObject {
ValueObject(String name) {
@ -470,10 +507,9 @@ class ConfigurationPropertiesBeanTests {
}
@ConfigurationProperties
@ConstructorBinding
static class ConstructorBindingOnType {
static class ConstructorBindingNoAnnotation {
ConstructorBindingOnType(String name) {
ConstructorBindingNoAnnotation(String name) {
}
}
@ -505,6 +541,48 @@ class ConfigurationPropertiesBeanTests {
}
@ConfigurationProperties
static class NoConstructorBindingOnMultipleConstructors {
NoConstructorBindingOnMultipleConstructors(String name) {
this(name, -1);
}
NoConstructorBindingOnMultipleConstructors(String name, int age) {
}
}
@ConfigurationProperties
static class JavaBeanWithAutowiredConstructor {
@Autowired
JavaBeanWithAutowiredConstructor(String name) {
}
}
@ConfigurationProperties
static class JavaBeanWithNoArgConstructor {
JavaBeanWithNoArgConstructor() {
}
}
@ConfigurationProperties
static class ConstructorBindingAndAutowiredConstructors {
@Autowired
ConstructorBindingAndAutowiredConstructors(String name) {
}
@ConstructorBinding
ConstructorBindingAndAutowiredConstructors(Integer age) {
}
}
@Configuration(proxyBeanMethods = false)
@Import(NonAnnotatedBeanConfigurationImportSelector.class)
static class NonAnnotatedBeanImportConfiguration {
@ -520,4 +598,17 @@ class ConfigurationPropertiesBeanTests {
}
@ConfigurationProperties
class Inner {
}
@ConfigurationProperties
class ParameterizedConstructorInner {
ParameterizedConstructorInner(Integer age) {
}
}
}

View File

@ -1049,6 +1049,16 @@ class ConfigurationPropertiesTests {
assertThat(bean.getNested().getOuter().getAge()).isEqualTo(5);
}
@Test
void loadWhenConstructorBindingWithOuterClassAndNestedAutowiredShouldThrowException() {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("test.nested.age", "5");
sources.addLast(new MapPropertySource("test", source));
assertThatExceptionOfType(ConfigurationPropertiesBindException.class).isThrownBy(
() -> load(ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowiredConfiguration.class));
}
@Test
void loadWhenConfigurationPropertiesPrefixMatchesPropertyInEnvironment() {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
@ -2092,7 +2102,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "test")
static class OtherInjectedProperties {
@ -2110,7 +2119,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "test")
@Validated
static class ConstructorParameterProperties {
@ -2135,7 +2143,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "test")
static class ConstructorParameterWithUnitProperties {
@ -2167,7 +2174,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "test")
static class ConstructorParameterWithFormatProperties {
@ -2192,7 +2198,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "test")
@Validated
static class ConstructorParameterValidatedProperties {
@ -2376,7 +2381,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties("test")
static class NestedConstructorProperties {
@ -2414,7 +2418,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties("test")
static class NestedMultipleConstructorProperties {
@ -2463,7 +2466,6 @@ class ConfigurationPropertiesTests {
}
@ConfigurationProperties("test")
@ConstructorBinding
static class ConstructorBindingWithOuterClassConstructorBoundProperties {
private final Nested nested;
@ -2492,6 +2494,36 @@ class ConfigurationPropertiesTests {
}
@ConfigurationProperties("test")
static class ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired {
private final Nested nested;
ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired(Nested nested) {
this.nested = nested;
}
Nested getNested() {
return this.nested;
}
static class Nested {
private int age;
@Autowired
Nested(int age) {
this.age = age;
}
int getAge() {
return this.age;
}
}
}
static class Outer {
private int age;
@ -2511,6 +2543,11 @@ class ConfigurationPropertiesTests {
}
@EnableConfigurationProperties(ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired.class)
static class ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowiredConfiguration {
}
@ConfigurationProperties("test")
static class MultiConstructorConfigurationListProperties {
@ -2613,7 +2650,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties("test")
static class SyntheticNestedConstructorProperties {
@ -2667,7 +2703,6 @@ class ConfigurationPropertiesTests {
}
@ConstructorBinding
@ConfigurationProperties("test")
static class DeducedNestedConstructorProperties {

View File

@ -61,7 +61,7 @@ class EnableConfigurationPropertiesRegistrarTests {
}
@Test
void typeWithConstructorBindingShouldRegisterConfigurationPropertiesBeanDefinition() {
void constructorBoundPropertiesShouldRegisterConfigurationPropertiesBeanDefinition() {
register(TestConfiguration.class);
BeanDefinition definition = this.beanFactory
.getBeanDefinition("bar-" + getClass().getName() + "$BarProperties");
@ -137,7 +137,6 @@ class EnableConfigurationPropertiesRegistrarTests {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "bar")
static class BarProperties {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.boot.context.properties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -75,7 +76,6 @@ class NotConstructorBoundInjectionFailureAnalyzerTests {
return analysis;
}
@ConstructorBinding
@ConfigurationProperties("test")
static class ConstructorBoundProperties {
@ -102,6 +102,7 @@ class NotConstructorBoundInjectionFailureAnalyzerTests {
private String name;
@Autowired
JavaBeanBoundProperties(String dependency) {
}

View File

@ -18,7 +18,6 @@ package org.springframework.boot.context.properties.scan.valid;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration;
@ -47,7 +46,6 @@ public class ConfigurationPropertiesScanConfiguration {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "bar")
static class BarProperties {

View File

@ -17,7 +17,6 @@
package org.springframework.boot.context.properties.scan.valid.b;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
/**
* @author Madhura Bhave
@ -29,7 +28,6 @@ public class BScanConfiguration {
}
@ConstructorBinding
@ConfigurationProperties(prefix = "b.first")
public static class BFirstProperties implements BProperties {

View File

@ -0,0 +1,213 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
package org.springframework.boot.context.properties;
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
/**
* Tests for `ConfigurationPropertiesBindConstructorProvider`.
*
* @author Madhura Bhave
*/
@Suppress("unused")
class ConfigurationPropertiesBindConstructorProviderTests {
private val constructorProvider = ConfigurationPropertiesBindConstructorProvider()
@Test
fun `type with default constructor should register java bean`() {
val bindConstructor = this.constructorProvider.getBindConstructor(FooProperties::class.java, false)
assertThat(bindConstructor).isNull()
}
@Test
fun `type with no primary constructor should register java bean`() {
val bindConstructor = this.constructorProvider.getBindConstructor(MultipleAmbiguousConstructors::class.java, false)
assertThat(bindConstructor).isNull()
}
@Test
fun `type with primary and secondary annotated constructor should use secondary constructor for binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(ConstructorBindingOnSecondaryWithPrimaryConstructor::class.java, false)
assertThat(bindConstructor).isNotNull();
}
@Test
fun `type with primary constructor with autowired should not use constructor binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(AutowiredPrimaryProperties::class.java, false)
assertThat(bindConstructor).isNull()
}
@Test
fun `type with primary and secondary constructor with autowired should not use constructor binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(PrimaryWithAutowiredSecondaryProperties::class.java, false)
assertThat(bindConstructor).isNull()
}
@Test
fun `type with autowired secondary constructor should not use constructor binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(AutowiredSecondaryProperties::class.java, false)
assertThat(bindConstructor).isNull()
}
@Test
fun `type with autowired primary and constructor binding on secondary constructor should throw exception`() {
assertThatIllegalStateException().isThrownBy {
this.constructorProvider.getBindConstructor(ConstructorBindingOnSecondaryAndAutowiredPrimaryProperties::class.java, false)
}
}
@Test
fun `type with autowired secondary and constructor binding on primary constructor should throw exception`() {
assertThatIllegalStateException().isThrownBy {
this.constructorProvider.getBindConstructor(ConstructorBindingOnPrimaryAndAutowiredSecondaryProperties::class.java, false)
}
}
@Test
fun `type with primary constructor and no annotation should use constructor binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(ConstructorBindingPrimaryConstructorNoAnnotation::class.java, false)
assertThat(bindConstructor).isNotNull()
}
@Test
fun `type with secondary constructor and no annotation should use constructor binding`() {
val bindConstructor = this.constructorProvider.getBindConstructor(ConstructorBindingSecondaryConstructorNoAnnotation::class.java, false)
assertThat(bindConstructor).isNotNull()
}
@Test
fun `type with multiple constructors`() {
val bindConstructor = this.constructorProvider.getBindConstructor(ConstructorBindingMultipleConstructors::class.java, false)
assertThat(bindConstructor).isNotNull()
}
@Test
fun `type with multiple annotated constructors should throw exception`() {
assertThatIllegalStateException().isThrownBy {
this.constructorProvider.getBindConstructor(ConstructorBindingMultipleAnnotatedConstructors::class.java, false)
}
}
@Test
fun `type with secondary and primary annotated constructors should throw exception`() {
assertThatIllegalStateException().isThrownBy {
this.constructorProvider.getBindConstructor(ConstructorBindingSecondaryAndPrimaryAnnotatedConstructors::class.java, false)
}
}
@ConfigurationProperties(prefix = "foo")
class FooProperties
@ConfigurationProperties(prefix = "bar")
class PrimaryWithAutowiredSecondaryProperties constructor(val name: String?, val counter: Int = 42) {
@Autowired
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
@ConfigurationProperties(prefix = "bar")
class AutowiredSecondaryProperties {
@Autowired
constructor(@Suppress("UNUSED_PARAMETER") foo: String)
}
@ConfigurationProperties(prefix = "bar")
class AutowiredPrimaryProperties @Autowired constructor(val name: String?, val counter: Int = 42) {
}
@ConfigurationProperties(prefix = "bar")
class ConstructorBindingOnSecondaryAndAutowiredPrimaryProperties @Autowired constructor(val name: String?, val counter: Int = 42) {
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
@ConfigurationProperties(prefix = "bar")
class ConstructorBindingOnPrimaryAndAutowiredSecondaryProperties @ConstructorBinding constructor(val name: String?, val counter: Int = 42) {
@Autowired
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingOnSecondaryWithPrimaryConstructor constructor(val name: String?, val counter: Int = 42) {
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingOnPrimaryWithSecondaryConstructor @ConstructorBinding constructor(val name: String?, val counter: Int = 42) {
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingPrimaryConstructorNoAnnotation(val name: String?, val counter: Int = 42)
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingSecondaryConstructorNoAnnotation {
constructor(@Suppress("UNUSED_PARAMETER") foo: String)
}
@ConfigurationProperties(prefix = "bing")
class MultipleAmbiguousConstructors {
constructor()
constructor(@Suppress("UNUSED_PARAMETER") foo: String)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingMultipleConstructors {
constructor(@Suppress("UNUSED_PARAMETER") bar: Int)
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") foo: String)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingMultipleAnnotatedConstructors {
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") bar: Int)
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") foo: String)
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingSecondaryAndPrimaryAnnotatedConstructors @ConstructorBinding constructor(val name: String?, val counter: Int = 42) {
@ConstructorBinding
constructor(@Suppress("UNUSED_PARAMETER") foo: String) : this(foo, 21)
}
}

View File

@ -32,7 +32,7 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
"bar-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BarProperties")
assertThat(beanDefinition.hasAttribute(ConfigurationPropertiesBean.BindMethod::class.java.name)).isTrue()
assertThat(beanDefinition.getAttribute(ConfigurationPropertiesBean.BindMethod::class.java.name))
.isEqualTo(ConfigurationPropertiesBean.BindMethod.VALUE_OBJECT)
.isEqualTo(ConfigurationPropertiesBean.BindMethod.VALUE_OBJECT)
}
@Test
@ -46,7 +46,6 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
@ConfigurationProperties(prefix = "foo")
class FooProperties
@ConstructorBinding
@ConfigurationProperties(prefix = "bar")
class BarProperties(val name: String?, val counter: Int = 42)

View File

@ -26,7 +26,6 @@ class KotlinConfigurationPropertiesTests {
}
@ConfigurationProperties(prefix = "foo")
@ConstructorBinding
class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) {
}