mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-09-03 04:26:12 +08:00
Allow @ConstructorBinding to be optional
This commit makes @ConstructorBinding optional for a type that has a single parameterized constructor. An @Autowired annotation on any of the constructors indicates that the type should not be constructor bound. Since @ConstructorBinding is now deduced for a single parameterized constructor, the annotation is no longer needed at the type level. Closes gh-23216
This commit is contained in:
parent
bc2c637d63
commit
44b88cc88c
@ -19,7 +19,6 @@ package org.springframework.boot.actuate.context.properties;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -52,7 +51,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.boot.actuate.endpoint.SanitizableData;
|
||||
import org.springframework.boot.actuate.endpoint.Sanitizer;
|
||||
@ -63,7 +61,8 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.context.properties.BoundConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
|
||||
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationProperty;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||
@ -72,11 +71,9 @@ import org.springframework.boot.origin.Origin;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@ -472,7 +469,9 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||
List<BeanPropertyWriter> beanProperties) {
|
||||
List<BeanPropertyWriter> result = new ArrayList<>();
|
||||
Class<?> beanClass = beanDesc.getType().getRawClass();
|
||||
Constructor<?> bindConstructor = findBindConstructor(ClassUtils.getUserClass(beanClass));
|
||||
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
|
||||
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
|
||||
.getBindConstructor(bindable, false);
|
||||
for (BeanPropertyWriter writer : beanProperties) {
|
||||
if (isCandidate(beanDesc, writer, bindConstructor)) {
|
||||
result.add(writer);
|
||||
@ -540,34 +539,6 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||
return StringUtils.capitalize(propertyName);
|
||||
}
|
||||
|
||||
private Constructor<?> findBindConstructor(Class<?> type) {
|
||||
boolean classConstructorBinding = MergedAnnotations
|
||||
.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
|
||||
.isPresent(ConstructorBinding.class);
|
||||
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
|
||||
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
|
||||
if (constructor != null) {
|
||||
return findBindConstructor(classConstructorBinding, constructor);
|
||||
}
|
||||
}
|
||||
return findBindConstructor(classConstructorBinding, type.getDeclaredConstructors());
|
||||
}
|
||||
|
||||
private Constructor<?> findBindConstructor(boolean classConstructorBinding, Constructor<?>... candidates) {
|
||||
List<Constructor<?>> candidateConstructors = Arrays.stream(candidates)
|
||||
.filter((constructor) -> constructor.getParameterCount() > 0).collect(Collectors.toList());
|
||||
List<Constructor<?>> flaggedConstructors = candidateConstructors.stream()
|
||||
.filter((candidate) -> MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class))
|
||||
.collect(Collectors.toList());
|
||||
if (flaggedConstructors.size() == 1) {
|
||||
return flaggedConstructors.get(0);
|
||||
}
|
||||
if (classConstructorBinding && candidateConstructors.size() == 1) {
|
||||
return candidateConstructors.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.springframework.boot.autoconfigure.diagnostics.analyzer;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -41,15 +40,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionEvaluationRepor
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||
import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
@ -115,27 +109,6 @@ class NoSuchBeanDefinitionFailureAnalyzer extends AbstractInjectionFailureAnalyz
|
||||
(!autoConfigurationResults.isEmpty() || !userConfigurationResults.isEmpty())
|
||||
? "revisiting the entries above or defining" : "defining",
|
||||
getBeanDescription(cause));
|
||||
if (injectionPoint != null && injectionPoint.getMember() instanceof Constructor) {
|
||||
Constructor<?> constructor = (Constructor<?>) injectionPoint.getMember();
|
||||
Class<?> declaringClass = constructor.getDeclaringClass();
|
||||
MergedAnnotation<ConfigurationProperties> configurationProperties = MergedAnnotations.from(declaringClass)
|
||||
.get(ConfigurationProperties.class);
|
||||
if (configurationProperties.isPresent()) {
|
||||
if (KotlinDetector.isKotlinType(declaringClass) && !KotlinDetector.isKotlinReflectPresent()) {
|
||||
action = String.format(
|
||||
"%s%nConsider adding a dependency on kotlin-reflect so that the constructor used for @%s can be located. Also, ensure that @%s is present on '%s' if you intended to use constructor-based "
|
||||
+ "configuration property binding.",
|
||||
action, ConstructorBinding.class.getSimpleName(), ConstructorBinding.class.getSimpleName(),
|
||||
constructor.getName());
|
||||
}
|
||||
else {
|
||||
action = String.format(
|
||||
"%s%nConsider adding @%s to %s if you intended to use constructor-based "
|
||||
+ "configuration property binding.",
|
||||
action, ConstructorBinding.class.getSimpleName(), constructor.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FailureAnalysis(message.toString(), action, cause);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -108,11 +108,10 @@ TIP: `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency is provided b
|
||||
|
||||
[[features.kotlin.configuration-properties]]
|
||||
=== @ConfigurationProperties
|
||||
`@ConfigurationProperties` when used in combination with <<features#features.external-config.typesafe-configuration-properties.constructor-binding,`@ConstructorBinding`>> supports classes with immutable `val` properties as shown in the following example:
|
||||
`@ConfigurationProperties` when used in combination with <<features#features.external-config.typesafe-configuration-properties.constructor-binding,constructor binding>> supports classes with immutable `val` properties as shown in the following example:
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("example.kotlin")
|
||||
data class KotlinExampleProperties(
|
||||
val name: String,
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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...
|
||||
|
@ -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...
|
||||
|
@ -27,7 +27,6 @@ import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("example")
|
||||
public class ExampleProperties {
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -99,10 +99,12 @@ class MetadataGenerationEnvironment {
|
||||
|
||||
private final String nameAnnotation;
|
||||
|
||||
private final String autowiredAnnotation;
|
||||
|
||||
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
|
||||
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
|
||||
String constructorBindingAnnotation, String defaultValueAnnotation, Set<String> endpointAnnotations,
|
||||
String readOperationAnnotation, String nameAnnotation) {
|
||||
String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation,
|
||||
Set<String> endpointAnnotations, String readOperationAnnotation, String nameAnnotation) {
|
||||
this.typeUtils = new TypeUtils(environment);
|
||||
this.elements = environment.getElementUtils();
|
||||
this.messager = environment.getMessager();
|
||||
@ -111,6 +113,7 @@ class MetadataGenerationEnvironment {
|
||||
this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation;
|
||||
this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation;
|
||||
this.constructorBindingAnnotation = constructorBindingAnnotation;
|
||||
this.autowiredAnnotation = autowiredAnnotation;
|
||||
this.defaultValueAnnotation = defaultValueAnnotation;
|
||||
this.endpointAnnotations = endpointAnnotations;
|
||||
this.readOperationAnnotation = readOperationAnnotation;
|
||||
@ -180,14 +183,14 @@ class MetadataGenerationEnvironment {
|
||||
return new ItemDeprecation(reason, replacement);
|
||||
}
|
||||
|
||||
boolean hasConstructorBindingAnnotation(TypeElement typeElement) {
|
||||
return hasAnnotationRecursive(typeElement, this.constructorBindingAnnotation);
|
||||
}
|
||||
|
||||
boolean hasConstructorBindingAnnotation(ExecutableElement element) {
|
||||
return hasAnnotation(element, this.constructorBindingAnnotation);
|
||||
}
|
||||
|
||||
boolean hasAutowiredAnnotation(ExecutableElement element) {
|
||||
return hasAnnotation(element, this.autowiredAnnotation);
|
||||
}
|
||||
|
||||
boolean hasAnnotation(Element element, String type) {
|
||||
return getAnnotation(element, type) != null;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.configurationprocessor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -191,16 +192,29 @@ class PropertyDescriptorResolver {
|
||||
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
|
||||
boolean constructorBoundType = isConstructorBoundType(type, env);
|
||||
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
|
||||
List<ExecutableElement> boundConstructors = constructors.stream()
|
||||
.filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
|
||||
List<ExecutableElement> boundConstructors = getBoundConstructors(env, constructors);
|
||||
return new ConfigurationPropertiesTypeElement(type, constructorBoundType, constructors, boundConstructors);
|
||||
}
|
||||
|
||||
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
|
||||
if (env.hasConstructorBindingAnnotation(type)
|
||||
|| "java.lang.Record".equals(type.getSuperclass().toString())) {
|
||||
return true;
|
||||
private static List<ExecutableElement> getBoundConstructors(MetadataGenerationEnvironment env,
|
||||
List<ExecutableElement> constructors) {
|
||||
ExecutableElement bindConstructor = deduceBindConstructor(constructors, env);
|
||||
if (bindConstructor != null) {
|
||||
return Collections.singletonList(bindConstructor);
|
||||
}
|
||||
return constructors.stream().filter(env::hasConstructorBindingAnnotation).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ExecutableElement deduceBindConstructor(List<ExecutableElement> constructors,
|
||||
MetadataGenerationEnvironment env) {
|
||||
if (constructors.size() == 1 && constructors.get(0).getParameters().size() > 0
|
||||
&& !env.hasAutowiredAnnotation(constructors.get(0))) {
|
||||
return constructors.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isConstructorBoundType(TypeElement type, MetadataGenerationEnvironment env) {
|
||||
if (type.getNestingKind() == NestingKind.MEMBER) {
|
||||
return isConstructorBoundType((TypeElement) type.getEnclosingElement(), env);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -45,6 +45,7 @@ class MetadataGenerationEnvironmentFactory implements Function<ProcessingEnviron
|
||||
TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION,
|
||||
TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION,
|
||||
TestConfigurationMetadataAnnotationProcessor.CONSTRUCTOR_BINDING_ANNOTATION,
|
||||
TestConfigurationMetadataAnnotationProcessor.AUTOWIRED_ANNOTATION,
|
||||
TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, endpointAnnotations,
|
||||
TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION,
|
||||
TestConfigurationMetadataAnnotationProcessor.NAME_ANNOTATION);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -35,6 +35,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester;
|
||||
import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor;
|
||||
import org.springframework.boot.configurationsample.immutable.ImmutableClassConstructorBindingProperties;
|
||||
import org.springframework.boot.configurationsample.immutable.ImmutableDeducedConstructorBindingProperties;
|
||||
import org.springframework.boot.configurationsample.immutable.ImmutableMultiConstructorProperties;
|
||||
import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties;
|
||||
import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties;
|
||||
@ -42,12 +43,11 @@ import org.springframework.boot.configurationsample.lombok.LombokExplicitPropert
|
||||
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
|
||||
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
|
||||
import org.springframework.boot.configurationsample.lombok.LombokSimpleValueProperties;
|
||||
import org.springframework.boot.configurationsample.simple.AutowiredProperties;
|
||||
import org.springframework.boot.configurationsample.simple.HierarchicalProperties;
|
||||
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesGrandparent;
|
||||
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesParent;
|
||||
import org.springframework.boot.configurationsample.simple.SimpleProperties;
|
||||
import org.springframework.boot.configurationsample.specific.MatchingConstructorNoDirectiveProperties;
|
||||
import org.springframework.boot.configurationsample.specific.TwoConstructorsClassConstructorBindingExample;
|
||||
import org.springframework.boot.configurationsample.specific.TwoConstructorsExample;
|
||||
import org.springframework.boot.testsupport.compiler.TestCompiler;
|
||||
|
||||
@ -111,6 +111,14 @@ class PropertyDescriptorResolverTests {
|
||||
(stream) -> assertThat(stream).containsExactly("name", "description", "counter", "number", "items")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertiesWithDeducedConstructorBinding() throws IOException {
|
||||
process(ImmutableDeducedConstructorBindingProperties.class,
|
||||
propertyNames((stream) -> assertThat(stream).containsExactly("theName", "flag")));
|
||||
process(ImmutableDeducedConstructorBindingProperties.class, properties((stream) -> assertThat(stream)
|
||||
.allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertiesWithConstructorWithConstructorBinding() throws IOException {
|
||||
process(ImmutableSimpleProperties.class, propertyNames(
|
||||
@ -128,16 +136,9 @@ class PropertyDescriptorResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertiesWithConstructorAndClassConstructorBindingAndSeveralCandidates() throws IOException {
|
||||
process(TwoConstructorsClassConstructorBindingExample.class,
|
||||
propertyNames((stream) -> assertThat(stream).isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertiesWithConstructorNoDirective() throws IOException {
|
||||
process(MatchingConstructorNoDirectiveProperties.class,
|
||||
propertyNames((stream) -> assertThat(stream).containsExactly("name")));
|
||||
process(MatchingConstructorNoDirectiveProperties.class, properties((stream) -> assertThat(stream)
|
||||
void propertiesWithAutowiredConstructor() throws IOException {
|
||||
process(AutowiredProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("theName")));
|
||||
process(AutowiredProperties.class, properties((stream) -> assertThat(stream)
|
||||
.allMatch((predicate) -> predicate instanceof JavaBeanPropertyDescriptor)));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,10 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that can be used to indicate that configuration properties should be bound
|
||||
* using constructor arguments rather than by calling setters. Can be added at the type
|
||||
* level (if there is an unambiguous constructor) or on the actual constructor to use.
|
||||
* Annotation that can be used to indicate which constructor to use when binding
|
||||
* configuration properties using constructor arguments rather than by calling setters. A
|
||||
* single parameterized constructor implicitly indicates that constructor binding should
|
||||
* be used unless the constructor is annotated with `@Autowired`.
|
||||
* <p>
|
||||
* Note: To use constructor binding the class must be enabled using
|
||||
* {@link EnableConfigurationProperties @EnableConfigurationProperties} or configuration
|
||||
@ -39,7 +40,7 @@ import java.lang.annotation.Target;
|
||||
* @since 2.2.0
|
||||
* @see ConfigurationProperties
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
|
||||
@Target(ElementType.CONSTRUCTOR)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ConstructorBinding {
|
||||
|
@ -102,7 +102,6 @@ class ConfigurationPropertiesBeanRegistrarTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("valuecp")
|
||||
static class ValueObjectConfigurationProperties {
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -27,6 +27,7 @@ import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.function.ThrowingConsumer;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
@ -47,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
* Tests for {@link ConfigurationPropertiesBean}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class ConfigurationPropertiesBeanTests {
|
||||
|
||||
@ -266,14 +268,14 @@ class ConfigurationPropertiesBeanTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindTypeForTypeWhenNoConstructorBindingOnTypeReturnsValueObject() {
|
||||
BindMethod bindType = BindMethod.forType(ConstructorBindingOnType.class);
|
||||
void bindTypeForTypeWhenConstructorBindingOnConstructorReturnsValueObject() {
|
||||
BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindTypeForTypeWhenNoConstructorBindingOnConstructorReturnsValueObject() {
|
||||
BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class);
|
||||
void bindTypeForTypeWhenNoConstructorBindingAnnotationOnSingleParameterizedConstructorReturnsValueObject() {
|
||||
BindMethod bindType = BindMethod.forType(ConstructorBindingNoAnnotation.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
|
||||
}
|
||||
|
||||
@ -285,6 +287,42 @@ class ConfigurationPropertiesBeanTests {
|
||||
+ " has more than one @ConstructorBinding constructor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindTypeForTypeWithMultipleConstructorsReturnJavaBean() {
|
||||
BindMethod bindType = BindMethod.forType(NoConstructorBindingOnMultipleConstructors.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindTypeForTypeWithNoArgConstructorReturnsJavaBean() {
|
||||
BindMethod bindType = BindMethod.forType(JavaBeanWithNoArgConstructor.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindTypeForTypeWithSingleArgAutowiredConstructorReturnsJavaBean() {
|
||||
BindMethod bindType = BindMethod.forType(JavaBeanWithAutowiredConstructor.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorBindingAndAutowiredConstructorsShouldThrowException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> BindMethod.forType(ConstructorBindingAndAutowiredConstructors.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void innerClassWithSyntheticFieldShouldReturnJavaBean() {
|
||||
BindMethod bindType = BindMethod.forType(Inner.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void innerClassWithParameterizedConstructorShouldReturnJavaBean() {
|
||||
BindMethod bindType = BindMethod.forType(ParameterizedConstructorInner.class);
|
||||
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
|
||||
}
|
||||
|
||||
private void get(Class<?> configuration, String beanName, ThrowingConsumer<ConfigurationPropertiesBean> consumer)
|
||||
throws Throwable {
|
||||
get(configuration, beanName, true, consumer);
|
||||
@ -448,7 +486,6 @@ class ConfigurationPropertiesBeanTests {
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
@ConstructorBinding
|
||||
static class ValueObject {
|
||||
|
||||
ValueObject(String name) {
|
||||
@ -470,10 +507,9 @@ class ConfigurationPropertiesBeanTests {
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
@ConstructorBinding
|
||||
static class ConstructorBindingOnType {
|
||||
static class ConstructorBindingNoAnnotation {
|
||||
|
||||
ConstructorBindingOnType(String name) {
|
||||
ConstructorBindingNoAnnotation(String name) {
|
||||
}
|
||||
|
||||
}
|
||||
@ -505,6 +541,48 @@ class ConfigurationPropertiesBeanTests {
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
static class NoConstructorBindingOnMultipleConstructors {
|
||||
|
||||
NoConstructorBindingOnMultipleConstructors(String name) {
|
||||
this(name, -1);
|
||||
}
|
||||
|
||||
NoConstructorBindingOnMultipleConstructors(String name, int age) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
static class JavaBeanWithAutowiredConstructor {
|
||||
|
||||
@Autowired
|
||||
JavaBeanWithAutowiredConstructor(String name) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
static class JavaBeanWithNoArgConstructor {
|
||||
|
||||
JavaBeanWithNoArgConstructor() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
static class ConstructorBindingAndAutowiredConstructors {
|
||||
|
||||
@Autowired
|
||||
ConstructorBindingAndAutowiredConstructors(String name) {
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
ConstructorBindingAndAutowiredConstructors(Integer age) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(NonAnnotatedBeanConfigurationImportSelector.class)
|
||||
static class NonAnnotatedBeanImportConfiguration {
|
||||
@ -520,4 +598,17 @@ class ConfigurationPropertiesBeanTests {
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
class Inner {
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties
|
||||
class ParameterizedConstructorInner {
|
||||
|
||||
ParameterizedConstructorInner(Integer age) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1049,6 +1049,16 @@ class ConfigurationPropertiesTests {
|
||||
assertThat(bean.getNested().getOuter().getAge()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenConstructorBindingWithOuterClassAndNestedAutowiredShouldThrowException() {
|
||||
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
|
||||
Map<String, Object> source = new HashMap<>();
|
||||
source.put("test.nested.age", "5");
|
||||
sources.addLast(new MapPropertySource("test", source));
|
||||
assertThatExceptionOfType(ConfigurationPropertiesBindException.class).isThrownBy(
|
||||
() -> load(ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowiredConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenConfigurationPropertiesPrefixMatchesPropertyInEnvironment() {
|
||||
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
|
||||
@ -2092,7 +2102,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
static class OtherInjectedProperties {
|
||||
|
||||
@ -2110,7 +2119,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
@Validated
|
||||
static class ConstructorParameterProperties {
|
||||
@ -2135,7 +2143,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
static class ConstructorParameterWithUnitProperties {
|
||||
|
||||
@ -2167,7 +2174,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
static class ConstructorParameterWithFormatProperties {
|
||||
|
||||
@ -2192,7 +2198,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties(prefix = "test")
|
||||
@Validated
|
||||
static class ConstructorParameterValidatedProperties {
|
||||
@ -2376,7 +2381,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("test")
|
||||
static class NestedConstructorProperties {
|
||||
|
||||
@ -2414,7 +2418,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("test")
|
||||
static class NestedMultipleConstructorProperties {
|
||||
|
||||
@ -2463,7 +2466,6 @@ class ConfigurationPropertiesTests {
|
||||
}
|
||||
|
||||
@ConfigurationProperties("test")
|
||||
@ConstructorBinding
|
||||
static class ConstructorBindingWithOuterClassConstructorBoundProperties {
|
||||
|
||||
private final Nested nested;
|
||||
@ -2492,6 +2494,36 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties("test")
|
||||
static class ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired {
|
||||
|
||||
private final Nested nested;
|
||||
|
||||
ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired(Nested nested) {
|
||||
this.nested = nested;
|
||||
}
|
||||
|
||||
Nested getNested() {
|
||||
return this.nested;
|
||||
}
|
||||
|
||||
static class Nested {
|
||||
|
||||
private int age;
|
||||
|
||||
@Autowired
|
||||
Nested(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Outer {
|
||||
|
||||
private int age;
|
||||
@ -2511,6 +2543,11 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableConfigurationProperties(ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowired.class)
|
||||
static class ConstructorBindingWithOuterClassConstructorBoundAndNestedAutowiredConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@ConfigurationProperties("test")
|
||||
static class MultiConstructorConfigurationListProperties {
|
||||
|
||||
@ -2613,7 +2650,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("test")
|
||||
static class SyntheticNestedConstructorProperties {
|
||||
|
||||
@ -2667,7 +2703,6 @@ class ConfigurationPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
@ConstructorBinding
|
||||
@ConfigurationProperties("test")
|
||||
static class DeducedNestedConstructorProperties {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -26,7 +26,6 @@ class KotlinConfigurationPropertiesTests {
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "foo")
|
||||
@ConstructorBinding
|
||||
class BingProperties(@Suppress("UNUSED_PARAMETER") bar: String) {
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user