Skip to content

Commit 1e73439

Browse files
committed
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
1 parent 566621f commit 1e73439

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

spring-expression/spring-expression.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ apply plugin: "kotlin"
44

55
dependencies {
66
api(project(":spring-core"))
7+
optional("org.jetbrains.kotlin:kotlin-reflect")
78
testImplementation(testFixtures(project(":spring-core")))
89
testImplementation("org.jetbrains.kotlin:kotlin-reflect")
910
testImplementation("org.jetbrains.kotlin:kotlin-stdlib")

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@
2727
import java.util.Set;
2828
import java.util.concurrent.ConcurrentHashMap;
2929

30+
import kotlin.jvm.JvmClassMappingKt;
31+
import kotlin.reflect.KClass;
32+
import kotlin.reflect.KMutableProperty;
33+
import kotlin.reflect.KProperty;
34+
import kotlin.reflect.full.KClasses;
35+
import kotlin.reflect.jvm.ReflectJvmMapping;
36+
3037
import org.springframework.asm.MethodVisitor;
38+
import org.springframework.core.KotlinDetector;
3139
import org.springframework.core.MethodParameter;
3240
import org.springframework.core.convert.Property;
3341
import org.springframework.core.convert.TypeDescriptor;
@@ -55,6 +63,7 @@
5563
* @author Juergen Hoeller
5664
* @author Phillip Webb
5765
* @author Sam Brannen
66+
* @author Sebastien Deleuze
5867
* @since 3.0
5968
* @see StandardEvaluationContext
6069
* @see SimpleEvaluationContext
@@ -401,7 +410,8 @@ private Method findMethodForProperty(String[] methodSuffixes, String prefix, Cla
401410
Method[] methods = getSortedMethods(clazz);
402411
for (String methodSuffix : methodSuffixes) {
403412
for (Method method : methods) {
404-
if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) &&
413+
if (isCandidateForProperty(method, clazz) &&
414+
(method.getName().equals(prefix + methodSuffix) || isKotlinProperty(method, methodSuffix)) &&
405415
method.getParameterCount() == numberOfParams &&
406416
(!mustBeStatic || Modifier.isStatic(method.getModifiers())) &&
407417
(requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) {
@@ -557,6 +567,13 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab
557567
return this;
558568
}
559569

570+
private static boolean isKotlinProperty(Method method, String methodSuffix) {
571+
Class<?> clazz = method.getDeclaringClass();
572+
return KotlinDetector.isKotlinReflectPresent() &&
573+
KotlinDetector.isKotlinType(clazz) &&
574+
KotlinDelegate.isKotlinProperty(method, methodSuffix);
575+
}
576+
560577

561578
/**
562579
* Captures the member (method/field) to call reflectively to access a property value
@@ -755,4 +772,24 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
755772
}
756773
}
757774

775+
/**
776+
* Inner class to avoid a hard dependency on Kotlin at runtime.
777+
*/
778+
private static class KotlinDelegate {
779+
780+
public static boolean isKotlinProperty(Method method, String methodSuffix) {
781+
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass());
782+
for (KProperty<?> property : KClasses.getMemberProperties(kClass)) {
783+
if (methodSuffix.equalsIgnoreCase(property.getName()) &&
784+
(method.equals(ReflectJvmMapping.getJavaGetter(property)) ||
785+
property instanceof KMutableProperty<?> mutableProperty &&
786+
method.equals(ReflectJvmMapping.getJavaSetter(mutableProperty)))) {
787+
return true;
788+
}
789+
}
790+
return false;
791+
}
792+
793+
}
794+
758795
}

spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ class SpelReproKotlinTests {
5555
assertThat(expr.getValue(context, Boolean::class.java)).isFalse()
5656
}
5757

58+
@Test
59+
fun `gh-30468 Unmangle Kotlin inlined class getter`() {
60+
context.setVariable("something", Something(UUID(123), "name"))
61+
val expr = parser.parseExpression("#something.id")
62+
assertThat(expr.getValue(context, Int::class.java)).isEqualTo(123)
63+
}
64+
65+
@Test
66+
fun `gh-30468 Unmangle Kotlin inlined class setter`() {
67+
context.setVariable("something", Something(UUID(123), "name"))
68+
val expr = parser.parseExpression("#something.id = 456")
69+
assertThat(expr.getValue(context, Int::class.java)).isEqualTo(456)
70+
}
71+
5872
@Suppress("UNUSED_PARAMETER")
5973
class Config {
6074

@@ -71,4 +85,11 @@ class SpelReproKotlinTests {
7185
}
7286

7387
}
88+
89+
@JvmInline value class UUID(val value: Int)
90+
91+
data class Something(
92+
var id: UUID,
93+
var name: String,
94+
)
7495
}

0 commit comments

Comments
 (0)