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