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, + ) }