Move @ConstructorBinding annotation and support add default support

Relocate `@ConstructorBinding` from the `boot.context.properties`
package to `boot.context.properties.bind` and update the
`DefaultBindConstructorProvider` to support it.

Closes gh-32660
This commit is contained in:
Phillip Webb 2022-10-17 20:36:14 -07:00
parent db248b80bb
commit e3df6c5b6f
19 changed files with 459 additions and 257 deletions

View File

@ -62,7 +62,7 @@ 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.ConfigurationPropertiesBindConstructorProvider;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
@ -476,8 +476,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(bindable, false);
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);

View File

@ -34,8 +34,8 @@ import org.springframework.boot.actuate.context.properties.ConfigurationProperti
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.Show;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.origin.Origin;

View File

@ -18,6 +18,7 @@ package org.springframework.boot.context.properties;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.LinkedHashMap;
@ -30,6 +31,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationContext;
@ -70,8 +72,8 @@ public final class ConfigurationPropertiesBean {
private final BindMethod bindMethod;
private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
Bindable<?> bindTarget) {
this(name, instance, annotation, bindTarget, BindMethod.forType(bindTarget.getType().resolve()));
Bindable<?> bindable) {
this(name, instance, annotation, bindable, BindMethod.get(bindable));
}
private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
@ -274,14 +276,14 @@ public final class ConfigurationPropertiesBean {
: new Annotation[] { annotation };
ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
: ResolvableType.forClass(type);
Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
Bindable<Object> bindable = Bindable.of(bindType).withAnnotations(annotations);
if (instance != null) {
bindTarget = bindTarget.withExistingValue(instance);
bindable = bindable.withExistingValue(instance);
}
if (factory != null) {
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget, BindMethod.JAVA_BEAN);
return new ConfigurationPropertiesBean(name, instance, annotation, bindable, BindMethod.JAVA_BEAN);
}
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
return new ConfigurationPropertiesBean(name, instance, annotation, bindable);
}
private static <A extends Annotation> A findAnnotation(Object instance, Class<?> type, Method factory,
@ -321,9 +323,16 @@ public final class ConfigurationPropertiesBean {
*/
VALUE_OBJECT;
static BindMethod forType(Class<?> type) {
return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type, false) != null)
? VALUE_OBJECT : JAVA_BEAN;
static BindMethod get(Class<?> type) {
return get(BindConstructorProvider.DEFAULT.getBindConstructor(type, false));
}
static BindMethod get(Bindable<?> bindable) {
return get(BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false));
}
private static BindMethod get(Constructor<?> bindConstructor) {
return (bindConstructor != null) ? VALUE_OBJECT : JAVA_BEAN;
}
}

View File

@ -89,7 +89,7 @@ final class ConfigurationPropertiesBeanRegistrar {
}
private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
BindMethod bindMethod = BindMethod.forType(type);
BindMethod bindMethod = BindMethod.get(type);
RootBeanDefinition definition = new RootBeanDefinition(type);
definition.setAttribute(BindMethod.class.getName(), bindMethod);
if (bindMethod == BindMethod.VALUE_OBJECT) {

View File

@ -1,163 +0,0 @@
/*
* 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.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;
/**
* {@link BindConstructorProvider} used when binding
* {@link ConfigurationProperties @ConfigurationProperties}.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 3.0.0
*/
public class ConfigurationPropertiesBindConstructorProvider implements BindConstructorProvider {
/**
* A shared singleton {@link ConfigurationPropertiesBindConstructorProvider} instance.
*/
public static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider();
@Override
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
return getBindConstructor(bindable.getType().resolve(), isNestedConstructorBinding);
}
Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstructorBinding) {
if (type == null) {
return null;
}
Constructors constructors = Constructors.getConstructors(type);
if (constructors.getBind() != null || isNestedConstructorBinding) {
Assert.state(!constructors.hasAutowired(),
() -> type.getName() + " declares @ConstructorBinding and @Autowired constructor");
}
return constructors.getBind();
}
/**
* 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);
}
if (bind == null && !hasAutowiredConstructor && isKotlinType(type)) {
bind = deduceKotlinBindConstructor(type);
}
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;
}
}
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");
Assert.state(constructor == null,
() -> type.getName() + " has more than one @ConstructorBinding constructor");
constructor = candidate;
}
return constructor;
}
private static boolean isKotlinType(Class<?> type) {
return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type);
}
private static Constructor<?> deduceKotlinBindConstructor(Class<?> type) {
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
return primaryConstructor;
}
return null;
}
}
}

View File

@ -164,8 +164,7 @@ class ConfigurationPropertiesBinder {
private Binder getBinder() {
if (this.binder == null) {
this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
getConversionServices(), getPropertyEditorInitializer(), null,
ConfigurationPropertiesBindConstructorProvider.INSTANCE);
getConversionServices(), getPropertyEditorInitializer(), null, null);
}
return this.binder;
}

View File

@ -33,6 +33,7 @@ import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.beans.BeanInfoFactory;
import org.springframework.beans.ExtendedBeanInfoFactory;
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotations;
@ -88,7 +89,7 @@ public final class ConfigurationPropertiesReflectionHintsProcessor {
private static Constructor<?> getBindConstructor(Class<?> type, boolean nestedType) {
Bindable<?> bindable = Bindable.of(type);
return ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(bindable, nestedType);
return BindConstructorProvider.DEFAULT.getBindConstructor(bindable, nestedType);
}
private void process(ReflectionHints reflectionHints) {

View File

@ -39,10 +39,14 @@ import java.lang.annotation.Target;
* @author Phillip Webb
* @since 2.2.0
* @see ConfigurationProperties
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
* {@link org.springframework.boot.context.properties.bind.ConstructorBinding}
*/
@Target(ElementType.CONSTRUCTOR)
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Deprecated(since = "3.0.0", forRemoval = true)
@org.springframework.boot.context.properties.bind.ConstructorBinding
public @interface ConstructorBinding {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer;
import org.springframework.core.Ordered;
@ -66,7 +67,7 @@ class NotConstructorBoundInjectionFailureAnalyzer
MergedAnnotation<ConfigurationProperties> configurationProperties = MergedAnnotations.from(declaringClass)
.get(ConfigurationProperties.class);
return configurationProperties.isPresent()
&& BindMethod.forType(constructor.getDeclaringClass()) == BindMethod.VALUE_OBJECT;
&& BindMethod.get(constructor.getDeclaringClass()) == BindMethod.VALUE_OBJECT;
}
return false;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,6 +33,19 @@ public interface BindConstructorProvider {
*/
BindConstructorProvider DEFAULT = new DefaultBindConstructorProvider();
/**
* Return the bind constructor to use for the given type, or {@code null} if
* constructor binding is not supported.
* @param type the type to check
* @param isNestedConstructorBinding if this binding is nested within a constructor
* binding
* @return the bind constructor or {@code null}
* @since 3.0.0
*/
default Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstructorBinding) {
return getBindConstructor(Bindable.of(type), isNestedConstructorBinding);
}
/**
* Return the bind constructor to use for the given bindable, or {@code null} if
* constructor binding is not supported.

View File

@ -0,0 +1,39 @@
/*
* 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.context.properties.bind;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 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`.
*
* @author Phillip Webb
* @since 3.0.0
*/
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,9 +18,13 @@ package org.springframework.boot.context.properties.bind;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.KotlinDetector;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.Assert;
/**
* Default {@link BindConstructorProvider} implementation.
@ -32,38 +36,140 @@ class DefaultBindConstructorProvider implements BindConstructorProvider {
@Override
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
Class<?> type = bindable.getType().resolve();
if (bindable.getValue() != null || type == null) {
return null;
}
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
return getDeducedKotlinConstructor(type);
}
Constructor<?>[] constructors = type.getDeclaredConstructors();
if (constructors.length == 1 && constructors[0].getParameterCount() > 0) {
return constructors[0];
}
Constructor<?> constructor = null;
for (Constructor<?> candidate : constructors) {
if (!Modifier.isPrivate(candidate.getModifiers())) {
if (constructor != null) {
return null;
}
constructor = candidate;
}
}
if (constructor != null && constructor.getParameterCount() > 0) {
return constructor;
}
return null;
return getBindConstructor(bindable.getType().resolve(), isNestedConstructorBinding);
}
private Constructor<?> getDeducedKotlinConstructor(Class<?> type) {
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
return primaryConstructor;
@Override
public Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstructorBinding) {
if (type == null) {
return null;
}
return null;
Constructors constructors = Constructors.getConstructors(type);
if (constructors.getBind() != null || isNestedConstructorBinding) {
Assert.state(!constructors.hasAutowired(),
() -> type.getName() + " declares @ConstructorBinding and @Autowired constructor");
}
return constructors.getBind();
}
/**
* 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);
MergedAnnotations[] candidateAnnotations = getAnnotations(candidates);
boolean hasAutowiredConstructor = isAutowiredPresent(candidateAnnotations);
Constructor<?> bind = getConstructorBindingAnnotated(type, candidates, candidateAnnotations);
if (bind == null && !hasAutowiredConstructor) {
bind = deduceBindConstructor(candidates);
}
if (bind == null && !hasAutowiredConstructor && isKotlinType(type)) {
bind = deduceKotlinBindConstructor(type);
}
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;
}
}
private static boolean isNonSynthetic(Constructor<?> constructor, Class<?> type) {
return !constructor.isSynthetic();
}
private static MergedAnnotations[] getAnnotations(Constructor<?>[] candidates) {
MergedAnnotations[] candidateAnnotations = new MergedAnnotations[candidates.length];
for (int i = 0; i < candidates.length; i++) {
candidateAnnotations[i] = MergedAnnotations.from(candidates[i]);
}
return candidateAnnotations;
}
private static boolean isAutowiredPresent(MergedAnnotations[] candidateAnnotations) {
for (MergedAnnotations annotations : candidateAnnotations) {
if (annotations.isPresent(Autowired.class)) {
return true;
}
}
return false;
}
private static Constructor<?> getConstructorBindingAnnotated(Class<?> type, Constructor<?>[] candidates,
MergedAnnotations[] mergedAnnotations) {
Constructor<?> result = null;
for (int i = 0; i < candidates.length; i++) {
if (mergedAnnotations[i].isPresent(ConstructorBinding.class)) {
Assert.state(candidates[i].getParameterCount() > 0,
() -> type.getName() + " declares @ConstructorBinding on a no-args constructor");
Assert.state(result == null,
() -> type.getName() + " has more than one @ConstructorBinding constructor");
result = candidates[i];
}
}
return result;
}
private static Constructor<?> deduceBindConstructor(Constructor<?>[] candidates) {
if (candidates.length == 1 && candidates[0].getParameterCount() > 0) {
return candidates[0];
}
Constructor<?> result = null;
for (Constructor<?> candidate : candidates) {
if (!Modifier.isPrivate(candidate.getModifiers())) {
if (result != null) {
return null;
}
result = candidate;
}
}
return (result != null && result.getParameterCount() > 0) ? result : null;
}
private static boolean isKotlinType(Class<?> type) {
return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type);
}
private static Constructor<?> deduceKotlinBindConstructor(Class<?> type) {
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
return primaryConstructor;
}
return null;
}
}
}

View File

@ -40,6 +40,7 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;

View File

@ -27,7 +27,9 @@ 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.BindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -227,8 +229,26 @@ class ConfigurationPropertiesBeanTests {
Bindable<?> target = propertiesBean.asBindTarget();
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(ConstructorBindingOnConstructor.class));
assertThat(target.getValue()).isNull();
assertThat(ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull();
assertThat(BindConstructorProvider.DEFAULT.getBindConstructor(ConstructorBindingOnConstructor.class, false))
.isNotNull();
}
@Test
@Deprecated(since = "3.0.0", forRemoval = true)
void forValueObjectWithDeprecatedConstructorBindingAnnotatedClassReturnsBean() {
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean
.forValueObject(DeprecatedConstructorBindingOnConstructor.class, "valueObjectBean");
assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean");
assertThat(propertiesBean.getInstance()).isNull();
assertThat(propertiesBean.getType()).isEqualTo(DeprecatedConstructorBindingOnConstructor.class);
assertThat(propertiesBean.getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT);
assertThat(propertiesBean.getAnnotation()).isNotNull();
Bindable<?> target = propertiesBean.asBindTarget();
assertThat(target.getType())
.isEqualTo(ResolvableType.forClass(DeprecatedConstructorBindingOnConstructor.class));
assertThat(target.getValue()).isNull();
assertThat(BindConstructorProvider.DEFAULT.getBindConstructor(DeprecatedConstructorBindingOnConstructor.class,
false)).isNotNull();
}
@Test
@ -249,8 +269,8 @@ class ConfigurationPropertiesBeanTests {
Bindable<?> target = propertiesBean.asBindTarget();
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(implicitConstructorBinding));
assertThat(target.getValue()).isNull();
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(implicitConstructorBinding, false);
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(implicitConstructorBinding,
false);
assertThat(bindConstructor).isNotNull();
assertThat(bindConstructor.getParameterTypes()).containsExactly(String.class, Integer.class);
}
@ -268,64 +288,64 @@ class ConfigurationPropertiesBeanTests {
}
@Test
void bindTypeForTypeWhenNoConstructorBindingReturnsJavaBean() {
BindMethod bindType = BindMethod.forType(NoConstructorBinding.class);
void bindMethodGetWhenNoConstructorBindingReturnsJavaBean() {
BindMethod bindType = BindMethod.get(NoConstructorBinding.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void bindTypeForTypeWhenConstructorBindingOnConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingOnConstructor.class);
void bindMethodGetWhenConstructorBindingOnConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.get(ConstructorBindingOnConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
}
@Test
void bindTypeForTypeWhenNoConstructorBindingAnnotationOnSingleParameterizedConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.forType(ConstructorBindingNoAnnotation.class);
void bindMethodGetWhenNoConstructorBindingAnnotationOnSingleParameterizedConstructorReturnsValueObject() {
BindMethod bindType = BindMethod.get(ConstructorBindingNoAnnotation.class);
assertThat(bindType).isEqualTo(BindMethod.VALUE_OBJECT);
}
@Test
void bindTypeForTypeWhenConstructorBindingOnMultipleConstructorsThrowsException() {
void bindMethodGetWhenConstructorBindingOnMultipleConstructorsThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> BindMethod.forType(ConstructorBindingOnMultipleConstructors.class))
.isThrownBy(() -> BindMethod.get(ConstructorBindingOnMultipleConstructors.class))
.withMessage(ConstructorBindingOnMultipleConstructors.class.getName()
+ " has more than one @ConstructorBinding constructor");
}
@Test
void bindTypeForTypeWithMultipleConstructorsReturnJavaBean() {
BindMethod bindType = BindMethod.forType(NoConstructorBindingOnMultipleConstructors.class);
void bindMethodGetWithMultipleConstructorsReturnJavaBean() {
BindMethod bindType = BindMethod.get(NoConstructorBindingOnMultipleConstructors.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void bindTypeForTypeWithNoArgConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.forType(JavaBeanWithNoArgConstructor.class);
void bindMethodGetWithNoArgConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.get(JavaBeanWithNoArgConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void bindTypeForTypeWithSingleArgAutowiredConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.forType(JavaBeanWithAutowiredConstructor.class);
void bindMethodGetWithSingleArgAutowiredConstructorReturnsJavaBean() {
BindMethod bindType = BindMethod.get(JavaBeanWithAutowiredConstructor.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void constructorBindingAndAutowiredConstructorsShouldThrowException() {
assertThatIllegalStateException()
.isThrownBy(() -> BindMethod.forType(ConstructorBindingAndAutowiredConstructors.class));
.isThrownBy(() -> BindMethod.get(ConstructorBindingAndAutowiredConstructors.class));
}
@Test
void innerClassWithSyntheticFieldShouldReturnJavaBean() {
BindMethod bindType = BindMethod.forType(Inner.class);
BindMethod bindType = BindMethod.get(Inner.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@Test
void innerClassWithParameterizedConstructorShouldReturnJavaBean() {
BindMethod bindType = BindMethod.forType(ParameterizedConstructorInner.class);
BindMethod bindType = BindMethod.get(ParameterizedConstructorInner.class);
assertThat(bindType).isEqualTo(BindMethod.JAVA_BEAN);
}
@ -533,6 +553,20 @@ class ConfigurationPropertiesBeanTests {
}
@ConfigurationProperties
@SuppressWarnings("removal")
static class DeprecatedConstructorBindingOnConstructor {
DeprecatedConstructorBindingOnConstructor(String name) {
this(name, -1);
}
@org.springframework.boot.context.properties.ConstructorBinding
DeprecatedConstructorBindingOnConstructor(String name, int age) {
}
}
@ConfigurationProperties
static class ConstructorBindingOnMultipleConstructors {

View File

@ -54,6 +54,7 @@ import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.context.properties.bind.validation.BindValidationException;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;

View File

@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

View File

@ -0,0 +1,173 @@
/*
* 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.context.properties.bind;
import java.lang.reflect.Constructor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link DefaultBindConstructorProvider}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class DefaultBindConstructorProviderTests {
private DefaultBindConstructorProvider provider = new DefaultBindConstructorProvider();
@Test
void getBindConstructorWhenHasOnlyDefaultConstructorReturnsNull() {
Constructor<?> constructor = this.provider.getBindConstructor(OnlyDefaultConstructor.class, false);
assertThat(constructor).isNull();
}
@Test
void getBindConstructorWhenHasMultipleAmbiguousConstructorsReturnsNull() {
Constructor<?> constructor = this.provider.getBindConstructor(MultipleAmbiguousConstructors.class, false);
assertThat(constructor).isNull();
}
@Test
void getBindConstructorWhenHasTwoConstructorsWithOneConstructorBindingReturnsConstructor() {
Constructor<?> constructor = this.provider.getBindConstructor(TwoConstructorsWithOneConstructorBinding.class,
false);
assertThat(constructor).isNotNull();
assertThat(constructor.getParameterCount()).isEqualTo(1);
}
@Test
void getBindConstructorWhenHasOneConstructorWithAutowiredReturnsNull() {
Constructor<?> constructor = this.provider.getBindConstructor(OneConstructorWithAutowired.class, false);
assertThat(constructor).isNull();
}
@Test
void getBindConstructorWhenHasTwoConstructorsWithOneAutowiredReturnsNull() {
Constructor<?> constructor = this.provider.getBindConstructor(TwoConstructorsWithOneAutowired.class, false);
assertThat(constructor).isNull();
}
@Test
void getBindConstructorWhenHasTwoConstructorsWithOneAutowiredAndOneConstructorBindingThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> this.provider
.getBindConstructor(TwoConstructorsWithOneAutowiredAndOneConstructorBinding.class, false))
.withMessageContaining("declares @ConstructorBinding and @Autowired");
}
@Test
void getBindConstructorWhenHasOneConstructorWithConstructorBindingReturnsConstructor() {
Constructor<?> constructor = this.provider.getBindConstructor(OneConstructorWithConstructorBinding.class,
false);
assertThat(constructor).isNotNull();
}
@Test
void getBindConstructorWhenHasTwoConstructorsWithBothConstructorBindingThrowsException() {
assertThatIllegalStateException()
.isThrownBy(
() -> this.provider.getBindConstructor(TwoConstructorsWithBothConstructorBinding.class, false))
.withMessageContaining("has more than one @ConstructorBinding");
}
static class OnlyDefaultConstructor {
}
static class MultipleAmbiguousConstructors {
MultipleAmbiguousConstructors() {
}
MultipleAmbiguousConstructors(String name) {
}
}
static class TwoConstructorsWithOneConstructorBinding {
@ConstructorBinding
TwoConstructorsWithOneConstructorBinding(String name) {
this(name, 100);
}
TwoConstructorsWithOneConstructorBinding(String name, int age) {
}
}
static class OneConstructorWithAutowired {
@Autowired
OneConstructorWithAutowired(String name, int age) {
}
}
static class TwoConstructorsWithOneAutowired {
@Autowired
TwoConstructorsWithOneAutowired(String name) {
this(name, 100);
}
TwoConstructorsWithOneAutowired(String name, int age) {
}
}
static class TwoConstructorsWithOneAutowiredAndOneConstructorBinding {
@Autowired
TwoConstructorsWithOneAutowiredAndOneConstructorBinding(String name) {
this(name, 100);
}
@ConstructorBinding
TwoConstructorsWithOneAutowiredAndOneConstructorBinding(String name, int age) {
}
}
static class OneConstructorWithConstructorBinding {
@ConstructorBinding
OneConstructorWithConstructorBinding(String name, int age) {
}
}
static class TwoConstructorsWithBothConstructorBinding {
@ConstructorBinding
TwoConstructorsWithBothConstructorBinding(String name) {
this(name, 100);
}
@ConstructorBinding
TwoConstructorsWithBothConstructorBinding(String name, int age) {
}
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.boot.context.properties.bind;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -366,7 +365,7 @@ class ValueObjectBinderTests {
}
@Test
void bindToRecordWithDefaultValue() throws IOException {
void bindToRecordWithDefaultValue() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("test.record.property1", "value-from-config-1");
this.sources.add(source);

View File

@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
package org.springframework.boot.context.properties;
package org.springframework.boot.context.properties.bind;
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalStateException
@ -22,14 +22,14 @@ import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
/**
* Tests for `ConfigurationPropertiesBindConstructorProvider`.
* Tests for `DefaultBindConstructorProvider`.
*
* @author Madhura Bhave
*/
@Suppress("unused")
class ConfigurationPropertiesBindConstructorProviderTests {
class KotlinDefaultBindConstructorProviderTests {
private val constructorProvider = ConfigurationPropertiesBindConstructorProvider()
private val constructorProvider = DefaultBindConstructorProvider()
@Test
fun `type with default constructor should register java bean`() {
@ -119,66 +119,55 @@ class ConfigurationPropertiesBindConstructorProviderTests {
assertThat(bindConstructor).isNotNull();
}
@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()
@ -187,7 +176,6 @@ class ConfigurationPropertiesBindConstructorProviderTests {
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingMultipleConstructors {
constructor(@Suppress("UNUSED_PARAMETER") bar: Int)
@ -197,7 +185,6 @@ class ConfigurationPropertiesBindConstructorProviderTests {
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingMultipleAnnotatedConstructors {
@ConstructorBinding
@ -208,7 +195,6 @@ class ConfigurationPropertiesBindConstructorProviderTests {
}
@ConfigurationProperties(prefix = "bing")
class ConstructorBindingSecondaryAndPrimaryAnnotatedConstructors @ConstructorBinding constructor(val name: String?, val counter: Int = 42) {
@ConstructorBinding
@ -216,7 +202,6 @@ class ConfigurationPropertiesBindConstructorProviderTests {
}
@ConfigurationProperties(prefix = "bing")
data class ConstructorBindingDataClassWithDefaultValues(val name: String = "Joan", val counter: Int = 42)
}