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 };