Skip to content

Commit f9785d2

Browse files
mbhavephilwebb
andcommitted
Fix constructor binding issues
This commit fixes a few bugs related to constructor binding. The ContructorFilter on the Bindable has been replaced with a Binder level BinderConstructorProvider so that it can be used to determine the constructor to use for nested properties as well. Fixes gh-18810 Fixes gh-18670 Closes gh-18685 Closes gh-18894 Co-authored-by: Phillip Webb <[email protected]>
1 parent 90e1046 commit f9785d2

16 files changed

+518
-165
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
21-
import java.lang.reflect.Constructor;
2221
import java.lang.reflect.Method;
2322
import java.util.Iterator;
2423
import java.util.LinkedHashMap;
2524
import java.util.Map;
2625
import java.util.concurrent.atomic.AtomicReference;
2726

2827
import org.springframework.aop.support.AopUtils;
29-
import org.springframework.beans.BeanUtils;
3028
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3129
import org.springframework.beans.factory.config.BeanDefinition;
3230
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -37,7 +35,6 @@
3735
import org.springframework.context.ApplicationContext;
3836
import org.springframework.context.ConfigurableApplicationContext;
3937
import org.springframework.context.annotation.Bean;
40-
import org.springframework.core.KotlinDetector;
4138
import org.springframework.core.ResolvableType;
4239
import org.springframework.core.annotation.MergedAnnotation;
4340
import org.springframework.core.annotation.MergedAnnotations;
@@ -73,12 +70,12 @@ public final class ConfigurationPropertiesBean {
7370
private final BindMethod bindMethod;
7471

7572
private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
76-
Bindable<?> bindTarget, BindMethod bindMethod) {
73+
Bindable<?> bindTarget) {
7774
this.name = name;
7875
this.instance = instance;
7976
this.annotation = annotation;
8077
this.bindTarget = bindTarget;
81-
this.bindMethod = bindMethod;
78+
this.bindMethod = BindMethod.forType(bindTarget.getType().resolve());
8279
}
8380

8481
/**
@@ -264,16 +261,13 @@ private static ConfigurationPropertiesBean create(String name, Object instance,
264261
Validated validated = findAnnotation(instance, type, factory, Validated.class);
265262
Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
266263
: new Annotation[] { annotation };
267-
Constructor<?> bindConstructor = BindMethod.findBindConstructor(type);
268-
BindMethod bindMethod = (bindConstructor != null) ? BindMethod.VALUE_OBJECT : BindMethod.forClass(type);
269264
ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
270265
: ResolvableType.forClass(type);
271-
Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations)
272-
.withConstructorFilter(ConfigurationPropertiesBean::isBindableConstructor);
266+
Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
273267
if (instance != null) {
274268
bindTarget = bindTarget.withExistingValue(instance);
275269
}
276-
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget, bindMethod);
270+
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
277271
}
278272

279273
private static <A extends Annotation> A findAnnotation(Object instance, Class<?> type, Method factory,
@@ -298,15 +292,6 @@ private static <A extends Annotation> MergedAnnotation<A> findMergedAnnotation(A
298292
: MergedAnnotation.missing();
299293
}
300294

301-
private static boolean isBindableConstructor(Constructor<?> constructor) {
302-
Class<?> declaringClass = constructor.getDeclaringClass();
303-
Constructor<?> bindConstructor = BindMethod.findBindConstructor(declaringClass);
304-
if (bindConstructor != null) {
305-
return bindConstructor.equals(constructor);
306-
}
307-
return BindMethod.forClass(declaringClass) == BindMethod.VALUE_OBJECT;
308-
}
309-
310295
/**
311296
* The binding method that is used for the bean.
312297
*/
@@ -322,40 +307,9 @@ public enum BindMethod {
322307
*/
323308
VALUE_OBJECT;
324309

325-
static BindMethod forClass(Class<?> type) {
326-
if (isConstructorBindingType(type) || findBindConstructor(type) != null) {
327-
return VALUE_OBJECT;
328-
}
329-
return JAVA_BEAN;
330-
}
331-
332-
private static boolean isConstructorBindingType(Class<?> type) {
333-
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
334-
.isPresent(ConstructorBinding.class);
335-
}
336-
337-
static Constructor<?> findBindConstructor(Class<?> type) {
338-
if (KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type)) {
339-
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
340-
if (constructor != null) {
341-
return findBindConstructor(type, constructor);
342-
}
343-
}
344-
return findBindConstructor(type, type.getDeclaredConstructors());
345-
}
346-
347-
private static Constructor<?> findBindConstructor(Class<?> type, Constructor<?>... candidates) {
348-
Constructor<?> constructor = null;
349-
for (Constructor<?> candidate : candidates) {
350-
if (MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class)) {
351-
Assert.state(candidate.getParameterCount() > 0,
352-
type.getName() + " declares @ConstructorBinding on a no-args constructor");
353-
Assert.state(constructor == null,
354-
type.getName() + " has more than one @ConstructorBinding constructor");
355-
constructor = candidate;
356-
}
357-
}
358-
return constructor;
310+
static BindMethod forType(Class<?> type) {
311+
return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type) != null)
312+
? VALUE_OBJECT : JAVA_BEAN;
359313
}
360314

361315
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinitionValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public int getOrder() {
5454

5555
private void validate(ConfigurableListableBeanFactory beanFactory, String beanName) {
5656
Class<?> beanClass = beanFactory.getType(beanName, false);
57-
if (beanClass != null && BindMethod.forClass(beanClass) == BindMethod.VALUE_OBJECT) {
57+
if (beanClass != null && BindMethod.forType(beanClass) == BindMethod.VALUE_OBJECT) {
5858
throw new BeanCreationException(beanName,
5959
"@EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add "
6060
+ "@ConstructorBinding type " + beanClass.getName());

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrar.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private void registerBeanDefinition(String beanName, Class<?> type,
8888
}
8989

9090
private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
91-
if (BindMethod.forClass(type) == BindMethod.VALUE_OBJECT) {
91+
if (BindMethod.forType(type) == BindMethod.VALUE_OBJECT) {
9292
return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type);
9393
}
9494
GenericBeanDefinition definition = new GenericBeanDefinition();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties;
18+
19+
import java.lang.reflect.Constructor;
20+
21+
import org.springframework.beans.BeanUtils;
22+
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
23+
import org.springframework.boot.context.properties.bind.Bindable;
24+
import org.springframework.core.KotlinDetector;
25+
import org.springframework.core.annotation.MergedAnnotations;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* {@link BindConstructorProvider} used when binding
30+
* {@link ConfigurationProperties @ConfigurationProperties}.
31+
*
32+
* @author Madhura Bhave
33+
* @author Phillip Webb
34+
*/
35+
class ConfigurationPropertiesBindConstructorProvider implements BindConstructorProvider {
36+
37+
static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider();
38+
39+
@Override
40+
public Constructor<?> getBindConstructor(Bindable<?> bindable) {
41+
return getBindConstructor(bindable.getType().resolve());
42+
}
43+
44+
Constructor<?> getBindConstructor(Class<?> type) {
45+
if (type == null) {
46+
return null;
47+
}
48+
Constructor<?> constructor = findConstructorBindingAnnotatedConstructor(type);
49+
if (constructor == null && isConstructorBindingAnnotatedType(type)) {
50+
constructor = deduceBindConstructor(type);
51+
}
52+
return constructor;
53+
}
54+
55+
private Constructor<?> findConstructorBindingAnnotatedConstructor(Class<?> type) {
56+
if (isKotlinType(type)) {
57+
Constructor<?> constructor = BeanUtils.findPrimaryConstructor(type);
58+
if (constructor != null) {
59+
return findAnnotatedConstructor(type, constructor);
60+
}
61+
}
62+
return findAnnotatedConstructor(type, type.getDeclaredConstructors());
63+
}
64+
65+
private Constructor<?> findAnnotatedConstructor(Class<?> type, Constructor<?>... candidates) {
66+
Constructor<?> constructor = null;
67+
for (Constructor<?> candidate : candidates) {
68+
if (MergedAnnotations.from(candidate).isPresent(ConstructorBinding.class)) {
69+
Assert.state(candidate.getParameterCount() > 0,
70+
type.getName() + " declares @ConstructorBinding on a no-args constructor");
71+
Assert.state(constructor == null,
72+
type.getName() + " has more than one @ConstructorBinding constructor");
73+
constructor = candidate;
74+
}
75+
}
76+
return constructor;
77+
}
78+
79+
private boolean isConstructorBindingAnnotatedType(Class<?> type) {
80+
return MergedAnnotations.from(type, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)
81+
.isPresent(ConstructorBinding.class);
82+
}
83+
84+
private Constructor<?> deduceBindConstructor(Class<?> type) {
85+
if (isKotlinType(type)) {
86+
return deducedKotlinBindConstructor(type);
87+
}
88+
Constructor<?>[] constructors = type.getDeclaredConstructors();
89+
if (constructors.length == 1 && constructors[0].getParameterCount() > 0) {
90+
return constructors[0];
91+
}
92+
return null;
93+
}
94+
95+
private Constructor<?> deducedKotlinBindConstructor(Class<?> type) {
96+
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
97+
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
98+
return primaryConstructor;
99+
}
100+
return null;
101+
}
102+
103+
private boolean isKotlinType(Class<?> type) {
104+
return KotlinDetector.isKotlinPresent() && KotlinDetector.isKotlinType(type);
105+
}
106+
107+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors()
151151
private Binder getBinder() {
152152
if (this.binder == null) {
153153
this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
154-
getConversionService(), getPropertyEditorInitializer());
154+
getConversionService(), getPropertyEditorInitializer(), null,
155+
ConfigurationPropertiesBindConstructorProvider.INSTANCE);
155156
}
156157
return this.binder;
157158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties.bind;
18+
19+
import java.lang.reflect.Constructor;
20+
21+
/**
22+
* Strategy interface used to determine a specific constructor to use when binding.
23+
*
24+
* @author Madhura Bhave
25+
* @since 2.2.1
26+
*/
27+
@FunctionalInterface
28+
public interface BindConstructorProvider {
29+
30+
/**
31+
* Default {@link BindConstructorProvider} implementation that only returns a value
32+
* when there's a single constructor and when the bindable has no existing value.
33+
*/
34+
BindConstructorProvider DEFAULT = new DefaultBindConstructorProvider();
35+
36+
/**
37+
* Return the bind constructor to use for the given bindable, or {@code null} if
38+
* constructor binding is not supported.
39+
* @param bindable the bindable to check
40+
* @return the bind constructor or {@code null}
41+
*/
42+
Constructor<?> getBindConstructor(Bindable<?> bindable);
43+
44+
}

0 commit comments

Comments
 (0)