Support constructor binding on 3rd party classes

Closes gh-18935
pull/19328/head
Madhura Bhave 5 years ago
parent 7d540543f9
commit b6ff0b7c5f

@ -308,7 +308,7 @@ public final class ConfigurationPropertiesBean {
VALUE_OBJECT;
static BindMethod forType(Class<?> type) {
return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type) != null)
return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type, false) != null)
? VALUE_OBJECT : JAVA_BEAN;
}

@ -37,16 +37,16 @@ class ConfigurationPropertiesBindConstructorProvider implements BindConstructorP
static final ConfigurationPropertiesBindConstructorProvider INSTANCE = new ConfigurationPropertiesBindConstructorProvider();
@Override
public Constructor<?> getBindConstructor(Bindable<?> bindable) {
return getBindConstructor(bindable.getType().resolve());
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
return getBindConstructor(bindable.getType().resolve(), isNestedConstructorBinding);
}
Constructor<?> getBindConstructor(Class<?> type) {
Constructor<?> getBindConstructor(Class<?> type, boolean isNestedConstructorBinding) {
if (type == null) {
return null;
}
Constructor<?> constructor = findConstructorBindingAnnotatedConstructor(type);
if (constructor == null && isConstructorBindingAnnotatedType(type)) {
if (constructor == null && (isConstructorBindingAnnotatedType(type) || isNestedConstructorBinding)) {
constructor = deduceBindConstructor(type);
}
return constructor;

@ -37,8 +37,10 @@ public interface BindConstructorProvider {
* Return the bind constructor to use for the given bindable, or {@code null} if
* constructor binding is not supported.
* @param bindable the bindable to check
* @param isNestedConstructorBinding if this binding is nested within a constructor
* binding
* @return the bind constructor or {@code null}
*/
Constructor<?> getBindConstructor(Bindable<?> bindable);
Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding);
}

@ -522,6 +522,8 @@ public class Binder {
private final Deque<Class<?>> dataObjectBindings = new ArrayDeque<>();
private final Deque<Class<?>> constructorBindings = new ArrayDeque<>();
private ConfigurationProperty configurationProperty;
Context() {
@ -582,6 +584,18 @@ public class Binder {
this.configurationProperty = null;
}
void pushConstructorBoundTypes(Class<?> value) {
this.constructorBindings.push(value);
}
boolean isNestedConstructorBinding() {
return !this.constructorBindings.isEmpty();
}
void popConstructorBoundTypes() {
this.constructorBindings.pop();
}
PlaceholdersResolver getPlaceholdersResolver() {
return Binder.this.placeholdersResolver;
}

@ -30,7 +30,7 @@ import org.springframework.core.KotlinDetector;
class DefaultBindConstructorProvider implements BindConstructorProvider {
@Override
public Constructor<?> getBindConstructor(Bindable<?> bindable) {
public Constructor<?> getBindConstructor(Bindable<?> bindable, boolean isNestedConstructorBinding) {
Class<?> type = bindable.getType().resolve();
if (bindable.getValue() != null || type == null) {
return null;

@ -53,10 +53,11 @@ class ValueObjectBinder implements DataObjectBinder {
@Override
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Context context,
DataObjectPropertyBinder propertyBinder) {
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider);
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context);
if (valueObject == null) {
return null;
}
context.pushConstructorBoundTypes(target.getType().resolve());
List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
List<Object> args = new ArrayList<>(parameters.size());
boolean bound = false;
@ -67,12 +68,13 @@ class ValueObjectBinder implements DataObjectBinder {
args.add(arg);
}
context.clearConfigurationProperty();
context.popConstructorBoundTypes();
return bound ? valueObject.instantiate(args) : null;
}
@Override
public <T> T create(Bindable<T> target, Binder.Context context) {
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider);
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context);
if (valueObject == null) {
return null;
}
@ -104,12 +106,14 @@ class ValueObjectBinder implements DataObjectBinder {
abstract List<ConstructorParameter> getConstructorParameters();
@SuppressWarnings("unchecked")
static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider constructorProvider) {
static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider constructorProvider,
Binder.Context context) {
Class<T> type = (Class<T>) bindable.getType().resolve();
if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
return null;
}
Constructor<?> bindConstructor = constructorProvider.getBindConstructor(bindable);
Constructor<?> bindConstructor = constructorProvider.getBindConstructor(bindable,
context.isNestedConstructorBinding());
if (bindConstructor == null) {
return null;
}

@ -213,7 +213,7 @@ class ConfigurationPropertiesBeanTests {
assertThat(target.getType()).isEqualTo(ResolvableType.forClass(ConstructorBindingOnConstructor.class));
assertThat(target.getValue()).isNull();
assertThat(ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(ConstructorBindingOnConstructor.class)).isNotNull();
.getBindConstructor(ConstructorBindingOnConstructor.class, false)).isNotNull();
}
@Test

@ -907,6 +907,18 @@ class ConfigurationPropertiesTests {
load(TestConfiguration.class);
}
@Test
void loadWhenConstructorBindingWithOuterClassDeducedConstructorBound() {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("test.nested.outer.age", "5");
sources.addLast(new MapPropertySource("test", source));
load(ConstructorBindingWithOuterClassConstructorBoundConfiguration.class);
ConstructorBindingWithOuterClassConstructorBoundProperties bean = this.context
.getBean(ConstructorBindingWithOuterClassConstructorBoundProperties.class);
assertThat(bean.getNested().getOuter().getAge()).isEqualTo(5);
}
private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
return load(new Class<?>[] { configuration }, inlinedProperties);
}
@ -2126,6 +2138,55 @@ class ConfigurationPropertiesTests {
}
@ConfigurationProperties("test")
@ConstructorBinding
static class ConstructorBindingWithOuterClassConstructorBoundProperties {
private final Nested nested;
ConstructorBindingWithOuterClassConstructorBoundProperties(Nested nested) {
this.nested = nested;
}
Nested getNested() {
return this.nested;
}
static class Nested {
private Outer outer;
Outer getOuter() {
return this.outer;
}
void setOuter(Outer nested) {
this.outer = nested;
}
}
}
static class Outer {
private int age;
Outer(int age) {
this.age = age;
}
int getAge() {
return this.age;
}
}
@EnableConfigurationProperties(ConstructorBindingWithOuterClassConstructorBoundProperties.class)
static class ConstructorBindingWithOuterClassConstructorBoundConfiguration {
}
@ConfigurationProperties("test")
static class MultiConstructorConfigurationListProperties {

@ -339,7 +339,8 @@ class JavaBeanBinderTests {
@Test
void bindToInstanceWhenNoDefaultConstructorShouldBind() {
Binder binder = new Binder(this.sources, null, null, null, null, (type) -> null);
Binder binder = new Binder(this.sources, null, null, null, null,
(bindable, isNestedConstructorBinding) -> null);
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.value", "bar");
this.sources.add(source);

@ -103,7 +103,8 @@ class ValueObjectBinderTests {
this.sources.add(source);
Constructor<?>[] constructors = MultipleConstructorsBean.class.getDeclaredConstructors();
Constructor<?> constructor = (constructors[0].getParameterCount() == 1) ? constructors[0] : constructors[1];
Binder binder = new Binder(this.sources, null, null, null, null, (type) -> constructor);
Binder binder = new Binder(this.sources, null, null, null, null,
(bindable, isNestedConstructorBinding) -> constructor);
MultipleConstructorsBean bound = binder.bind("foo", Bindable.of(MultipleConstructorsBean.class)).get();
assertThat(bound.getIntValue()).isEqualTo(12);
}

Loading…
Cancel
Save