Use generic type when binding constructor parameters
Fixes gh-19156
This commit is contained in:
parent
2e763be9d1
commit
f4db8c89d4
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue