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 920f7c7bc07..4b31d5b6f5b 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 fa2cd34fbc4..d9e6dcb373b 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 65260a1ef41..24fc744adf8 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, + ) }