From f4db8c89d43ff96c7f4f2b6a45d9da0cc8835577 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 3 Dec 2019 09:25:48 -0800 Subject: [PATCH] Use generic type when binding constructor parameters Fixes gh-19156 --- .../properties/bind/ValueObjectBinder.java | 40 +++++++++++-------- .../bind/ValueObjectBinderTests.java | 29 ++++++++++++++ .../KotlinConstructorParametersBinderTests.kt | 18 +++++++++ 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 73ac82f1e6..e884736312 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -31,6 +31,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; @@ -118,9 +119,9 @@ class ValueObjectBinder implements DataObjectBinder { return null; } if (KotlinDetector.isKotlinType(type)) { - return KotlinValueObject.get((Constructor) bindConstructor); + return KotlinValueObject.get((Constructor) bindConstructor, bindable.getType()); } - return DefaultValueObject.get(bindConstructor); + return DefaultValueObject.get(bindConstructor, bindable.getType()); } } @@ -132,19 +133,22 @@ class ValueObjectBinder implements DataObjectBinder { private final List constructorParameters; - private KotlinValueObject(Constructor primaryConstructor, KFunction kotlinConstructor) { + private KotlinValueObject(Constructor primaryConstructor, KFunction kotlinConstructor, + ResolvableType type) { super(primaryConstructor); - this.constructorParameters = parseConstructorParameters(kotlinConstructor); + this.constructorParameters = parseConstructorParameters(kotlinConstructor, type); } - private List parseConstructorParameters(KFunction kotlinConstructor) { + private List parseConstructorParameters(KFunction kotlinConstructor, + ResolvableType type) { List parameters = kotlinConstructor.getParameters(); List result = new ArrayList<>(parameters.size()); for (KParameter parameter : parameters) { String name = parameter.getName(); - ResolvableType type = ResolvableType.forType(ReflectJvmMapping.getJavaType(parameter.getType())); + ResolvableType parameterType = ResolvableType + .forType(ReflectJvmMapping.getJavaType(parameter.getType()), type); Annotation[] annotations = parameter.getAnnotations().toArray(new Annotation[0]); - result.add(new ConstructorParameter(name, type, annotations)); + result.add(new ConstructorParameter(name, parameterType, annotations)); } return Collections.unmodifiableList(result); } @@ -154,12 +158,12 @@ class ValueObjectBinder implements DataObjectBinder { return this.constructorParameters; } - static ValueObject get(Constructor bindConstructor) { + static ValueObject get(Constructor bindConstructor, ResolvableType type) { KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(bindConstructor); if (kotlinConstructor != null) { - return new KotlinValueObject<>(bindConstructor, kotlinConstructor); + return new KotlinValueObject<>(bindConstructor, kotlinConstructor, type); } - return DefaultValueObject.get(bindConstructor); + return DefaultValueObject.get(bindConstructor, type); } } @@ -174,21 +178,23 @@ class ValueObjectBinder implements DataObjectBinder { private final List constructorParameters; - private DefaultValueObject(Constructor constructor) { + private DefaultValueObject(Constructor constructor, ResolvableType type) { super(constructor); - this.constructorParameters = parseConstructorParameters(constructor); + this.constructorParameters = parseConstructorParameters(constructor, type); } - private static List parseConstructorParameters(Constructor constructor) { + private static List parseConstructorParameters(Constructor constructor, + ResolvableType type) { String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor); Assert.state(names != null, () -> "Failed to extract parameter names for " + constructor); Parameter[] parameters = constructor.getParameters(); List result = new ArrayList<>(parameters.length); for (int i = 0; i < parameters.length; i++) { String name = names[i]; - ResolvableType type = ResolvableType.forConstructorParameter(constructor, i); + ResolvableType parameterType = ResolvableType.forMethodParameter(new MethodParameter(constructor, i), + type); Annotation[] annotations = parameters[i].getDeclaredAnnotations(); - result.add(new ConstructorParameter(name, type, annotations)); + result.add(new ConstructorParameter(name, parameterType, annotations)); } return Collections.unmodifiableList(result); } @@ -199,8 +205,8 @@ class ValueObjectBinder implements DataObjectBinder { } @SuppressWarnings("unchecked") - static ValueObject get(Constructor bindConstructor) { - return new DefaultValueObject<>((Constructor) bindConstructor); + static ValueObject get(Constructor bindConstructor, ResolvableType type) { + return new DefaultValueObject<>((Constructor) bindConstructor, type); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index 14cf117522..f75b089fcc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.ResolvableType; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.util.Assert; @@ -233,6 +235,19 @@ class ValueObjectBinderTests { .satisfies(this::noConfigurationProperty); } + @Test + void bindToClassShouldBindWithGenerics() { + // gh-19156 + ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, String.class, String.class); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value.bar", "baz"); + this.sources.add(source); + GenericValue> bean = this.binder.bind("foo", Bindable + .>>of(ResolvableType.forClassWithGenerics(GenericValue.class, type))) + .get(); + assertThat(bean.getValue().get("bar")).isEqualTo("baz"); + } + private void noConfigurationProperty(BindException ex) { assertThat(ex.getProperty()).isNull(); } @@ -452,4 +467,18 @@ class ValueObjectBinderTests { } + static class GenericValue { + + private final T value; + + GenericValue(T value) { + this.value = value; + } + + T getValue() { + return this.value; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt index 1ae2667b0e..36dade5997 100644 --- a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt @@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.boot.context.properties.source.ConfigurationPropertyName import org.springframework.boot.context.properties.source.MockConfigurationPropertySource +import org.springframework.core.ResolvableType /** * Tests for `ConstructorParametersBinder`. @@ -173,6 +174,19 @@ class KotlinConstructorParametersBinderTests { assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR) } + @Test + fun `Bind to data class with generics`() { + val source = MockConfigurationPropertySource() + source.put("foo.value.bar", "baz") + val binder = Binder(source) + val type = ResolvableType.forClassWithGenerics(Map::class.java, String::class.java, + String::class.java) + val bean = binder.bind("foo", Bindable + .of>>(ResolvableType.forClassWithGenerics(GenericValue::class.java, type))) + .get() + assertThat(bean.value.get("bar")).isEqualTo("baz"); + } + class ExampleValueBean(val intValue: Int?, val longValue: Long?, val booleanValue: Boolean?, val stringValue: String?, val enumValue: ExampleEnum?) @@ -214,4 +228,8 @@ class KotlinConstructorParametersBinderTests { val stringValue: String = "my data", val enumValue: ExampleEnum = ExampleEnum.BAR_BAZ) + data class GenericValue( + val value: T + ) + }