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; VALUE_OBJECT;
static BindMethod forType(Class<?> type) { static BindMethod forType(Class<?> type) {
return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type) != null) return (ConfigurationPropertiesBindConstructorProvider.INSTANCE.getBindConstructor(type, false) != null)
? VALUE_OBJECT : JAVA_BEAN; ? VALUE_OBJECT : JAVA_BEAN;
} }

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

@ -37,8 +37,10 @@ public interface BindConstructorProvider {
* Return the bind constructor to use for the given bindable, or {@code null} if * Return the bind constructor to use for the given bindable, or {@code null} if
* constructor binding is not supported. * constructor binding is not supported.
* @param bindable the bindable to check * @param bindable the bindable to check
* @param isNestedConstructorBinding if this binding is nested within a constructor
* binding
* @return the bind constructor or {@code null} * @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<?>> dataObjectBindings = new ArrayDeque<>();
private final Deque<Class<?>> constructorBindings = new ArrayDeque<>();
private ConfigurationProperty configurationProperty; private ConfigurationProperty configurationProperty;
Context() { Context() {
@ -582,6 +584,18 @@ public class Binder {
this.configurationProperty = null; this.configurationProperty = null;
} }
void pushConstructorBoundTypes(Class<?> value) {
this.constructorBindings.push(value);
}
boolean isNestedConstructorBinding() {
return !this.constructorBindings.isEmpty();
}
void popConstructorBoundTypes() {
this.constructorBindings.pop();
}
PlaceholdersResolver getPlaceholdersResolver() { PlaceholdersResolver getPlaceholdersResolver() {
return Binder.this.placeholdersResolver; return Binder.this.placeholdersResolver;
} }

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

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

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

@ -907,6 +907,18 @@ class ConfigurationPropertiesTests {
load(TestConfiguration.class); 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) { private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
return load(new Class<?>[] { configuration }, 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") @ConfigurationProperties("test")
static class MultiConstructorConfigurationListProperties { static class MultiConstructorConfigurationListProperties {

@ -339,7 +339,8 @@ class JavaBeanBinderTests {
@Test @Test
void bindToInstanceWhenNoDefaultConstructorShouldBind() { 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(); MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.value", "bar"); source.put("foo.value", "bar");
this.sources.add(source); this.sources.add(source);

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

Loading…
Cancel
Save