From 1e73439955544f3ee7b224f5bc7332f2ab266e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 11 Aug 2023 16:52:44 +0200 Subject: [PATCH] Support Kotlin value class properties in SpEL This commit adds support for inlined Kotlin value class properties in SpEL by leveraging Kotlin reflection instead of Java reflection broken by the mangled method name. Closes gh-30468 --- spring-expression/spring-expression.gradle | 1 + .../support/ReflectivePropertyAccessor.java | 39 ++++++++++++++++++- .../expression/spel/SpelReproKotlinTests.kt | 21 ++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/spring-expression/spring-expression.gradle b/spring-expression/spring-expression.gradle index 920f7c7bc0..4b31d5b6f5 100644 --- a/spring-expression/spring-expression.gradle +++ b/spring-expression/spring-expression.gradle @@ -4,6 +4,7 @@ apply plugin: "kotlin" dependencies { api(project(":spring-core")) + optional("org.jetbrains.kotlin:kotlin-reflect") testImplementation(testFixtures(project(":spring-core"))) testImplementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index fa2cd34fbc..d9e6dcb373 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -27,7 +27,15 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KClass; +import kotlin.reflect.KMutableProperty; +import kotlin.reflect.KProperty; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; + import org.springframework.asm.MethodVisitor; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.convert.Property; import org.springframework.core.convert.TypeDescriptor; @@ -55,6 +63,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @author Phillip Webb * @author Sam Brannen + * @author Sebastien Deleuze * @since 3.0 * @see StandardEvaluationContext * @see SimpleEvaluationContext @@ -401,7 +410,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { Method[] methods = getSortedMethods(clazz); for (String methodSuffix : methodSuffixes) { for (Method method : methods) { - if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) && + if (isCandidateForProperty(method, clazz) && + (method.getName().equals(prefix + methodSuffix) || isKotlinProperty(method, methodSuffix)) && method.getParameterCount() == numberOfParams && (!mustBeStatic || Modifier.isStatic(method.getModifiers())) && (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) { @@ -557,6 +567,13 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { return this; } + private static boolean isKotlinProperty(Method method, String methodSuffix) { + Class clazz = method.getDeclaringClass(); + return KotlinDetector.isKotlinReflectPresent() && + KotlinDetector.isKotlinType(clazz) && + KotlinDelegate.isKotlinProperty(method, methodSuffix); + } + /** * Captures the member (method/field) to call reflectively to access a property value @@ -755,4 +772,24 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } } + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + public static boolean isKotlinProperty(Method method, String methodSuffix) { + KClass kClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()); + for (KProperty property : KClasses.getMemberProperties(kClass)) { + if (methodSuffix.equalsIgnoreCase(property.getName()) && + (method.equals(ReflectJvmMapping.getJavaGetter(property)) || + property instanceof KMutableProperty mutableProperty && + method.equals(ReflectJvmMapping.getJavaSetter(mutableProperty)))) { + return true; + } + } + return false; + } + + } + } diff --git a/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt b/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt index 65260a1ef4..24fc744adf 100644 --- a/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt +++ b/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt @@ -55,6 +55,20 @@ class SpelReproKotlinTests { assertThat(expr.getValue(context, Boolean::class.java)).isFalse() } + @Test + fun `gh-30468 Unmangle Kotlin inlined class getter`() { + context.setVariable("something", Something(UUID(123), "name")) + val expr = parser.parseExpression("#something.id") + assertThat(expr.getValue(context, Int::class.java)).isEqualTo(123) + } + + @Test + fun `gh-30468 Unmangle Kotlin inlined class setter`() { + context.setVariable("something", Something(UUID(123), "name")) + val expr = parser.parseExpression("#something.id = 456") + assertThat(expr.getValue(context, Int::class.java)).isEqualTo(456) + } + @Suppress("UNUSED_PARAMETER") class Config { @@ -71,4 +85,11 @@ class SpelReproKotlinTests { } } + + @JvmInline value class UUID(val value: Int) + + data class Something( + var id: UUID, + var name: String, + ) }