diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index 6dcb6ba4d9b..701501eb8f5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -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 beanProperties) { List 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> candidateConstructors = Arrays.stream(candidates) - .filter((constructor) -> constructor.getParameterCount() > 0).collect(Collectors.toList()); - List> 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; - } - } /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java index b98c72b311f..86ac67d45c5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java index 3bb8722ad47..d10fdfec8bb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java @@ -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 { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java index f33880417c7..ee293000170 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java @@ -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 = 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); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java index 06398788350..b6fa5c7db9c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java @@ -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; - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/kotlin/org/springframework/boot/autoconfigure/diagnostics/analyzer/KotlinNoSuchBeanFailureAnalyzerNoKotlinReflectTests.kt b/spring-boot-project/spring-boot-autoconfigure/src/test/kotlin/org/springframework/boot/autoconfigure/diagnostics/analyzer/KotlinNoSuchBeanFailureAnalyzerNoKotlinReflectTests.kt deleted file mode 100644 index 9426bc192a9..00000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/kotlin/org/springframework/boot/autoconfigure/diagnostics/analyzer/KotlinNoSuchBeanFailureAnalyzerNoKotlinReflectTests.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc index 146a4421fff..aa9278b91bf 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc @@ -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. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc index e5c28bdfb3a..4948ea4e673 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -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. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc index 92662bccbe1..3b198df04df 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc @@ -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 <> supports classes with immutable `val` properties as shown in the following example: +`@ConfigurationProperties` when used in combination with <> 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, diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java index 5af0cde4b67..4284ab0f5d6 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java @@ -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 { diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java index 11566766f76..ad58ba61f80 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java @@ -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 { diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java index 27c1c1dde05..78c26b9536f 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java @@ -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... diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java index d1d4183e966..9bbba6251c2 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java @@ -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... diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleProperties.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleProperties.java index 7215ca98f7c..33dd79f4b1a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleProperties.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleProperties.java @@ -27,7 +27,6 @@ import org.springframework.boot.context.properties.bind.DefaultValue; * * @author Stephane Nicoll */ -@ConstructorBinding @ConfigurationProperties("example") public class ExampleProperties { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 7cc52dfac79..877bae8dab3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -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()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index c4cf34c06b9..fc0580285dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -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 endpointAnnotations, - String readOperationAnnotation, String nameAnnotation) { + String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation, + Set 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; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index 084e61f2bc0..618b169059d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -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 constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); - List boundConstructors = constructors.stream() - .filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList()); + List 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 getBoundConstructors(MetadataGenerationEnvironment env, + List 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 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); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 756d4b9a4ca..f981195fc0d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java index 6fbd0e42a2d..50a3714530d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java @@ -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 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))); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java index 592a5a5f45c..20e2d936ccf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaConstructorBinding.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Autowired.java similarity index 74% rename from spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaConstructorBinding.java rename to spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Autowired.java index 2b0df83ea4c..1d2560b6d9f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaConstructorBinding.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Autowired.java @@ -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 { } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConstructorBinding.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConstructorBinding.java index 48adcf7dc26..0f7bee8cdaa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConstructorBinding.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConstructorBinding.java @@ -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 { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java index dbef1d34efa..797d785173e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableClassConstructorBindingProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableClassConstructorBindingProperties.java index e5adef8229c..2776431c12b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableClassConstructorBindingProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableClassConstructorBindingProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableDeducedConstructorBindingProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableDeducedConstructorBindingProperties.java new file mode 100644 index 00000000000..fab1b2ce55d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableDeducedConstructorBindingProperties.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java index 0e4d0519831..08f41afd902 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/AutowiredProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/AutowiredProperties.java new file mode 100644 index 00000000000..4b673033f1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/AutowiredProperties.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java index 32ce81d1864..b638e421690 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java index b4c115c3a70..c2747b72f71 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java deleted file mode 100644 index 7e755ef9c9e..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java +++ /dev/null @@ -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; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java index b5628a5f687..be59be46351 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java @@ -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); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java index 86efdc9f081..edc0cae5467 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProvider.java @@ -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); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java index bbde4c7121c..64728837590 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java @@ -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`. *

* 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 { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java index 72a44356bec..9a0dffc878a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrarTests.java @@ -102,7 +102,6 @@ class ConfigurationPropertiesBeanRegistrarTests { } - @ConstructorBinding @ConfigurationProperties("valuecp") static class ValueObjectConfigurationProperties { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java index 6a26e696a3f..01b67591486 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java @@ -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 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) { + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 66d8b4d5a7a..77cdffd7103 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -1049,6 +1049,16 @@ class ConfigurationPropertiesTests { assertThat(bean.getNested().getOuter().getAge()).isEqualTo(5); } + @Test + void loadWhenConstructorBindingWithOuterClassAndNestedAutowiredShouldThrowException() { + MutablePropertySources sources = this.context.getEnvironment().getPropertySources(); + Map 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 { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java index 571fc63960a..aa51fd0e8e0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesRegistrarTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzerTests.java index d580a3ba99e..0cc9620de32 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzerTests.java @@ -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) { } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/ConfigurationPropertiesScanConfiguration.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/ConfigurationPropertiesScanConfiguration.java index 98e6babaabc..06c8ef0d3f5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/ConfigurationPropertiesScanConfiguration.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/ConfigurationPropertiesScanConfiguration.java @@ -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 { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/b/BScanConfiguration.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/b/BScanConfiguration.java index 4eb75ccc19e..dd06ac6ee8b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/b/BScanConfiguration.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/scan/valid/b/BScanConfiguration.java @@ -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 { diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProviderTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProviderTests.kt new file mode 100644 index 00000000000..b4df1706230 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/ConfigurationPropertiesBindConstructorProviderTests.kt @@ -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) + + } + +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt index 92ff037c6c4..b51333a88f0 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesBeanRegistrarTests.kt @@ -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) diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt index db95e2346ca..ec2c5ce58ca 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/KotlinConfigurationPropertiesTests.kt @@ -26,7 +26,6 @@ class KotlinConfigurationPropertiesTests { } @ConfigurationProperties(prefix = "foo") - @ConstructorBinding class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) { }