Use generic type when binding constructor parameters

Fixes gh-19156
This commit is contained in:
Madhura Bhave 2019-12-03 09:25:48 -08:00
parent 2e763be9d1
commit f4db8c89d4
3 changed files with 70 additions and 17 deletions

View File

@ -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<T>) bindConstructor);
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType());
}
return DefaultValueObject.get(bindConstructor);
return DefaultValueObject.get(bindConstructor, bindable.getType());
}
}
@ -132,19 +133,22 @@ class ValueObjectBinder implements DataObjectBinder {
private final List<ConstructorParameter> constructorParameters;
private KotlinValueObject(Constructor<T> primaryConstructor, KFunction<T> kotlinConstructor) {
private KotlinValueObject(Constructor<T> primaryConstructor, KFunction<T> kotlinConstructor,
ResolvableType type) {
super(primaryConstructor);
this.constructorParameters = parseConstructorParameters(kotlinConstructor);
this.constructorParameters = parseConstructorParameters(kotlinConstructor, type);
}
private List<ConstructorParameter> parseConstructorParameters(KFunction<T> kotlinConstructor) {
private List<ConstructorParameter> parseConstructorParameters(KFunction<T> kotlinConstructor,
ResolvableType type) {
List<KParameter> parameters = kotlinConstructor.getParameters();
List<ConstructorParameter> 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 <T> ValueObject<T> get(Constructor<T> bindConstructor) {
static <T> ValueObject<T> get(Constructor<T> bindConstructor, ResolvableType type) {
KFunction<T> 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<ConstructorParameter> constructorParameters;
private DefaultValueObject(Constructor<T> constructor) {
private DefaultValueObject(Constructor<T> constructor, ResolvableType type) {
super(constructor);
this.constructorParameters = parseConstructorParameters(constructor);
this.constructorParameters = parseConstructorParameters(constructor, type);
}
private static List<ConstructorParameter> parseConstructorParameters(Constructor<?> constructor) {
private static List<ConstructorParameter> 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<ConstructorParameter> 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 <T> ValueObject<T> get(Constructor<?> bindConstructor) {
return new DefaultValueObject<>((Constructor<T>) bindConstructor);
static <T> ValueObject<T> get(Constructor<?> bindConstructor, ResolvableType type) {
return new DefaultValueObject<>((Constructor<T>) bindConstructor, type);
}
}

View File

@ -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<Map<String, String>> bean = this.binder.bind("foo", Bindable
.<GenericValue<Map<String, String>>>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<T> {
private final T value;
GenericValue(T value) {
this.value = value;
}
T getValue() {
return this.value;
}
}
}

View File

@ -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<GenericValue<Map<String, String>>>(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<T>(
val value: T
)
}