From 88c2a25f126d2b2c212cdf564c0094977fac8e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 9 Aug 2023 10:26:25 +0200 Subject: [PATCH] Add support for Kotlin value classes in BeanUtils This commit adds support for Kotlin value classes annotated with @JvmInline to BeanUtils#findPrimaryConstructor. This is only the first step, more refinements are expected to be needed to achieve a comprehensive support of Kotlin values classes in Spring Framework. Closes gh-28638 --- .../org/springframework/beans/BeanUtils.java | 14 ++++- .../beans/BeanUtilsKotlinTests.kt | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 5fdc92ac395c..448b8bc3c369 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -31,8 +31,11 @@ import java.util.Set; import kotlin.jvm.JvmClassMappingKt; +import kotlin.jvm.JvmInline; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; +import kotlin.reflect.full.KAnnotatedElements; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; @@ -835,13 +838,22 @@ private static class KotlinDelegate { * @see * https://kotlinlang.org/docs/reference/classes.html#constructors */ + @SuppressWarnings("unchecked") @Nullable public static Constructor findPrimaryConstructor(Class clazz) { try { - KFunction primaryCtor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); + KClass kClass = JvmClassMappingKt.getKotlinClass(clazz); + KFunction primaryCtor = KClasses.getPrimaryConstructor(kClass); if (primaryCtor == null) { return null; } + if (kClass.isValue() && !KAnnotatedElements + .findAnnotations(kClass, JvmClassMappingKt.getKotlinClass(JvmInline.class)).isEmpty()) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + Assert.state(constructors.length == 1, + "Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor"); + return (Constructor) constructors[0]; + } Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryCtor); if (constructor == null) { throw new IllegalStateException( diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt index 40ba3cdbf5e0..ea901077c02c 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt @@ -90,6 +90,45 @@ class BeanUtilsKotlinTests { BeanUtils.instantiateClass(PrivateClass::class.java.getDeclaredConstructor()) } + @Test + fun `Instantiate value class`() { + val constructor = BeanUtils.findPrimaryConstructor(ValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = "Hello value class!" + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ValueClass(value)) + } + + @Test + fun `Instantiate value class with multiple constructors`() { + val constructor = BeanUtils.findPrimaryConstructor(ValueClassWithMultipleConstructors::class.java)!! + assertThat(constructor).isNotNull() + val value = "Hello value class!" + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(ValueClassWithMultipleConstructors(value)) + } + + @Test + fun `Instantiate class with value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(OneConstructorWithValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = ValueClass("Hello value class!") + val instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(OneConstructorWithValueClass(value)) + } + + @Test + fun `Instantiate class with nullable value class parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(OneConstructorWithNullableValueClass::class.java)!! + assertThat(constructor).isNotNull() + val value = ValueClass("Hello value class!") + var instance = BeanUtils.instantiateClass(constructor, value) + assertThat(instance).isEqualTo(OneConstructorWithNullableValueClass(value)) + instance = BeanUtils.instantiateClass(constructor, null) + assertThat(instance).isEqualTo(OneConstructorWithNullableValueClass(null)) + } + + class Foo(val param1: String, val param2: Int) class Bar(val param1: String, val param2: Int = 12) @@ -128,4 +167,17 @@ class BeanUtilsKotlinTests { private class PrivateClass + @JvmInline + value class ValueClass(private val value: String) + + @JvmInline + value class ValueClassWithMultipleConstructors(private val value: String) { + constructor() : this("Fail") + constructor(part1: String, part2: String) : this("Fail") + } + + data class OneConstructorWithValueClass(val value: ValueClass) + + data class OneConstructorWithNullableValueClass(val value: ValueClass?) + }