From f4201529bcb9aa8a1cef1d1ce637a5d7ecb9445e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 30 Jan 2026 10:32:13 +0100 Subject: [PATCH] Adapt composed property paths to Kotlin 2.0. Kotlin 2 now no longer implements serializable lambdas for SAM bridging through Java's writeReplace mechanism producing SerializedLambda but rather through a SAM class that captures the function object we're looking for. Also, skip intrinsics calls when introspecting Kotlin classfiles. Also, use ReflectJvmMapping instead of internal KPropertyImpl types as these will be removed with Kotlin 2.3. Closes #3451 --- .../data/core/SerializableLambdaReader.java | 107 +++++++++++++++--- .../data/core/TypedPropertyPaths.java | 32 ++---- .../data/core/KPropertyReferenceUnitTests.kt | 3 - .../data/core/TypedPropertyPathKtUnitTests.kt | 15 +-- 4 files changed, 107 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/springframework/data/core/SerializableLambdaReader.java b/src/main/java/org/springframework/data/core/SerializableLambdaReader.java index 79a2f4947..880bd3e25 100644 --- a/src/main/java/org/springframework/data/core/SerializableLambdaReader.java +++ b/src/main/java/org/springframework/data/core/SerializableLambdaReader.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandleInfo; import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.ArrayList; @@ -38,6 +39,7 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.asm.ClassReader; import org.springframework.asm.ClassVisitor; import org.springframework.asm.Label; @@ -52,6 +54,7 @@ import org.springframework.data.core.MemberDescriptor.KPropertyPathDescriptor; import org.springframework.data.core.MemberDescriptor.KPropertyReferenceDescriptor; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -138,9 +141,16 @@ class SerializableLambdaReader { */ public MemberDescriptor read(Object lambdaObject) { + // Kotlin 2.0 + Object k2Lambda = KotlinDetectorUtils.detectKotlin2SamLambda(lambdaObject); + if (k2Lambda != null) { + return KotlinDelegate.read(k2Lambda, lambdaObject); + } + SerializedLambda lambda = serialize(lambdaObject); - if (isKotlinPropertyReference(lambda)) { + // Kotlin 1.x + if (KotlinDetectorUtils.isKotlinPropertyReference(lambda)) { return KotlinDelegate.read(lambda); } @@ -183,12 +193,14 @@ class SerializableLambdaReader { } } - private MemberDescriptor getMemberDescriptor(Object lambdaObject, SerializedLambda lambda) throws IOException { + private MemberDescriptor getMemberDescriptor(Object lambdaObject, SerializedLambda lambda) + throws IOException, ReflectiveOperationException { + ClassLoader classLoader = lambdaObject.getClass().getClassLoader(); String implClass = Type.getObjectType(lambda.getImplClass()).getClassName(); Type owningType = Type.getArgumentTypes(lambda.getImplMethodSignature())[0]; String classFileName = implClass.replace('.', '/') + ".class"; - InputStream classFile = ClassLoader.getSystemResourceAsStream(classFileName); + InputStream classFile = classLoader.getResourceAsStream(classFileName); if (classFile == null) { throw new IllegalStateException("Cannot find class file '%s' for lambda introspection".formatted(classFileName)); @@ -197,8 +209,8 @@ class SerializableLambdaReader { try (classFile) { ClassReader cr = new ClassReader(classFile); - LambdaReadingVisitor classVisitor = new LambdaReadingVisitor(lambdaObject.getClass().getClassLoader(), - lambda.getImplMethodName(), owningType); + LambdaReadingVisitor classVisitor = new LambdaReadingVisitor(classLoader, lambda.getImplMethodName(), owningType, + KotlinDetector.isKotlinType(ClassUtils.forName(implClass, classLoader))); cr.accept(classVisitor, ClassReader.SKIP_FRAMES); return classVisitor.getMemberReference(lambda); } @@ -216,12 +228,50 @@ class SerializableLambdaReader { } } - private static boolean isKotlinPropertyReference(SerializedLambda lambda) { + /** + * Kotlin Lambda detector utilities. + */ + static class KotlinDetectorUtils { + + /** + * Detect whether the given lambda object is a Kotlin 2 SAM wrapper around a property reference + * {@link kotlin.reflect.KProperty} usage with {@link PropertyReference} or {@link TypedPropertyPath}. + *

+ * Kotlin 1 lambdas use {@link SerializedLambda} directly and provide the function object through + * {@link SerializedLambda#getCapturedArg(int) argument capture}. + * + * @param lambdaObject the lambda object to introspect. + * @return the function object or {@code null} if not detected. + */ + public static @Nullable Object detectKotlin2SamLambda(Object lambdaObject) { + + Class cls = lambdaObject.getClass(); + if (!KotlinDetector.isKotlinType(cls)) { + return null; + } + + Field field = ReflectionUtils.findField(lambdaObject.getClass(), "function"); + if (field == null) { + return null; + } + + ReflectionUtils.makeAccessible(field); + Object function = ReflectionUtils.getField(field, lambdaObject); + return isKotlinPropertyReference(function) ? function : null; + } + + public static boolean isKotlinPropertyReference(SerializedLambda lambda) { + + return KotlinDetector.isKotlinReflectPresent() // + && lambda.getCapturedArgCount() == 1 // + && lambda.getCapturedArg(0) != null // + && isKotlinPropertyReference(lambda.getCapturedArg(0)); + } + + private static boolean isKotlinPropertyReference(@Nullable Object capturedObject) { + return capturedObject != null && KotlinDetector.isKotlinType(capturedObject.getClass()); + } - return KotlinDetector.isKotlinReflectPresent() // - && lambda.getCapturedArgCount() == 1 // - && lambda.getCapturedArg(0) != null // - && KotlinDetector.isKotlinType(lambda.getCapturedArg(0).getClass()); } /** @@ -232,8 +282,10 @@ class SerializableLambdaReader { static class KotlinDelegate { public static MemberDescriptor read(SerializedLambda lambda) { + return read(lambda.getCapturedArg(0), lambda); + } - Object captured = lambda.getCapturedArg(0); + public static MemberDescriptor read(Object captured, Object lambda) { if (captured instanceof PropertyReference propRef // && propRef.getOwner() instanceof KClass owner // @@ -255,10 +307,10 @@ class SerializableLambdaReader { private final String implMethodName; private final LambdaMethodVisitor methodVisitor; - public LambdaReadingVisitor(ClassLoader classLoader, String implMethodName, Type owningType) { + public LambdaReadingVisitor(ClassLoader classLoader, String implMethodName, Type owningType, boolean kotlin) { super(SpringAsmInfo.ASM_VERSION); this.implMethodName = implMethodName; - this.methodVisitor = new LambdaMethodVisitor(classLoader, owningType); + this.methodVisitor = new LambdaMethodVisitor(classLoader, owningType, kotlin); } public MemberDescriptor getMemberReference(SerializedLambda lambda) { @@ -283,17 +335,20 @@ class SerializableLambdaReader { Type.getInternalName(Boolean.class)); private static final String BOXING_METHOD = "valueOf"; + private static final String KOTLIN_INTRINSICS_CLASS = "kotlin/jvm/internal/Intrinsics"; private final ClassLoader classLoader; private final Type owningType; + private final boolean kotlinCode; private int line; private final List memberDescriptors = new ArrayList<>(); private final Set errors = new LinkedHashSet<>(); - public LambdaMethodVisitor(ClassLoader classLoader, Type owningType) { + public LambdaMethodVisitor(ClassLoader classLoader, Type owningType, boolean kotlinCode) { super(SpringAsmInfo.ASM_VERSION); this.classLoader = classLoader; this.owningType = owningType; + this.kotlinCode = kotlinCode; } @Override @@ -324,6 +379,11 @@ class SerializableLambdaReader { @Override public void visitLdcInsn(Object value) { + + if (kotlinCode) { + return; + } + errors.add(new ReadingError(line, "Code loads a constant. Only method calls to getters, record components, or field access allowed.", null)); } @@ -365,6 +425,10 @@ class SerializableLambdaReader { return; } + if (owner.equals(KOTLIN_INTRINSICS_CLASS)) { + return; + } + errors.add(new ReadingError(line, "Method references must invoke no-arg methods only")); return; } @@ -487,17 +551,24 @@ class SerializableLambdaReader { @Nullable Function syntheticSupplier) { int filterIndex = findEntryPoint(stackTrace); + int offset = syntheticSupplier == null ? 0 : 1; if (filterIndex != -1) { - int offset = syntheticSupplier == null ? 0 : 1; + StackTraceElement synthetic; + if (syntheticSupplier != null) { + synthetic = syntheticSupplier.apply(stackTrace[filterIndex + 1]); + if (synthetic.getLineNumber() == 0) { + return stackTrace; + } + } else { + synthetic = null; + } StackTraceElement[] copy = new StackTraceElement[(stackTrace.length - filterIndex) + offset]; System.arraycopy(stackTrace, filterIndex, copy, offset, stackTrace.length - filterIndex); - if (syntheticSupplier != null) { - StackTraceElement userCode = copy[1]; - StackTraceElement synthetic = syntheticSupplier.apply(userCode); + if (synthetic != null) { copy[0] = synthetic; } return copy; diff --git a/src/main/java/org/springframework/data/core/TypedPropertyPaths.java b/src/main/java/org/springframework/data/core/TypedPropertyPaths.java index f9a595487..db6280f54 100644 --- a/src/main/java/org/springframework/data/core/TypedPropertyPaths.java +++ b/src/main/java/org/springframework/data/core/TypedPropertyPaths.java @@ -17,14 +17,16 @@ package org.springframework.data.core; import kotlin.reflect.KProperty; import kotlin.reflect.KProperty1; -import kotlin.reflect.jvm.internal.KProperty1Impl; -import kotlin.reflect.jvm.internal.KPropertyImpl; +import kotlin.reflect.jvm.ReflectJvmMapping; import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.WeakHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -228,29 +230,19 @@ class TypedPropertyPaths { public static TypedPropertyPath of(Object property) { if (property instanceof KPropertyPath paths) { - - TypedPropertyPath parent = of(paths.getProperty()); - TypedPropertyPath child = of(paths.getLeaf()); - - return TypedPropertyPaths.compose(parent, child); - } - - if (property instanceof KPropertyImpl impl) { - - Class owner = impl.getJavaField() != null ? impl.getJavaField().getDeclaringClass() - : impl.getGetter().getCaller().getMember().getDeclaringClass(); - KPropertyPathMetadata metadata = TypedPropertyPaths.KPropertyPathMetadata - .of(MemberDescriptor.KPropertyReferenceDescriptor.create(owner, (KProperty1) impl)); - return new TypedPropertyPaths.ResolvedKPropertyPath(metadata); + return TypedPropertyPaths.compose(of(paths.getProperty()), of(paths.getLeaf())); } if (property instanceof KProperty1 kProperty) { - if (kProperty.getGetter().getProperty() instanceof KProperty1Impl impl) { - return of(impl); - } + Field javaField = ReflectJvmMapping.getJavaField(kProperty); + Method getter = ReflectJvmMapping.getJavaGetter(kProperty); - throw new IllegalArgumentException("Property " + kProperty.getName() + " is not a KProperty"); + Class owner = javaField != null ? javaField.getDeclaringClass() + : Objects.requireNonNull(getter).getDeclaringClass(); + KPropertyPathMetadata metadata = TypedPropertyPaths.KPropertyPathMetadata + .of(MemberDescriptor.KPropertyReferenceDescriptor.create(owner, kProperty)); + return new TypedPropertyPaths.ResolvedKPropertyPath(metadata); } throw new IllegalArgumentException("Property " + property + " is not a KProperty"); diff --git a/src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt b/src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt index cbcdd64fd..e63cbb20f 100644 --- a/src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt @@ -17,7 +17,6 @@ package org.springframework.data.core import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatIllegalArgumentException -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test /** @@ -36,7 +35,6 @@ class KPropertyReferenceUnitTests { } @Test // GH-3400 - @Disabled("https://github.com/spring-projects/spring-data-commons/issues/3451") fun shouldComposePropertyPath() { val path = KPropertyReference.of(Person::address).then(Address::city) @@ -45,7 +43,6 @@ class KPropertyReferenceUnitTests { } @Test // GH-3400 - @Disabled("https://github.com/spring-projects/spring-data-commons/issues/3451") fun shouldComposeManyPropertyPath() { val path = KPropertyReference.of(Person::addresses).then(Address::city) diff --git a/src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt b/src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt index c828333c0..2d2861aa8 100644 --- a/src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt @@ -16,7 +16,6 @@ package org.springframework.data.core import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -50,20 +49,20 @@ class TypedPropertyPathKtUnitTests { ), Arguments.argumentSet( "Person.address.country", - TypedPropertyPath.path(Person::address) + TypedPropertyPath.path(Person::address) .then(Address::country), PropertyPath.from("address.country", Person::class.java) ), Arguments.argumentSet( "Person.address.country.name", - TypedPropertyPath.path(Person::address) - .then(Address::country).then(Country::name), + TypedPropertyPath.path(Person::address) + .then(Address::country).then(Country::name), PropertyPath.from("address.country.name", Person::class.java) ), Arguments.argumentSet( "Person.emergencyContact.address.country.name", - TypedPropertyPath.path(Person::emergencyContact) - .then

(Person::address).then(Address::country) + TypedPropertyPath.path(Person::emergencyContact) + .then(Person::address).then(Address::country) .then(Country::name), PropertyPath.from( "emergencyContact.address.country.name", @@ -85,13 +84,12 @@ class TypedPropertyPathKtUnitTests { @Test // GH-3400 fun shouldSupportComposedPropertyReference() { - val path = TypedPropertyPath.path(Person::address) + val path = TypedPropertyPath.path(Person::address) .then(Address::city); assertThat(path.toDotPath()).isEqualTo("address.city") } @Test // GH-3400 - @Disabled("https://github.com/spring-projects/spring-data-commons/issues/3451") fun shouldSupportPropertyLambda() { assertThat(TypedPropertyPath.path { it.address } .toDotPath()).isEqualTo("address") @@ -100,7 +98,6 @@ class TypedPropertyPathKtUnitTests { } @Test // GH-3400 - @Disabled() fun shouldSupportComposedPropertyLambda() { val path = TypedPropertyPath.path { it.address };