Browse Source

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
main
Mark Paluch 5 days ago
parent
commit
f4201529bc
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 107
      src/main/java/org/springframework/data/core/SerializableLambdaReader.java
  2. 32
      src/main/java/org/springframework/data/core/TypedPropertyPaths.java
  3. 3
      src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt
  4. 15
      src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt

107
src/main/java/org/springframework/data/core/SerializableLambdaReader.java

@ -24,6 +24,7 @@ import java.io.IOException; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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}.
* <p>
* 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 { @@ -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 { @@ -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 { @@ -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<MemberDescriptor> memberDescriptors = new ArrayList<>();
private final Set<ReadingError> 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 { @@ -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 { @@ -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 { @@ -487,17 +551,24 @@ class SerializableLambdaReader {
@Nullable Function<StackTraceElement, StackTraceElement> 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;

32
src/main/java/org/springframework/data/core/TypedPropertyPaths.java

@ -17,14 +17,16 @@ package org.springframework.data.core; @@ -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 { @@ -228,29 +230,19 @@ class TypedPropertyPaths {
public static <T, P> TypedPropertyPath<T, P> 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");

3
src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt

@ -17,7 +17,6 @@ package org.springframework.data.core @@ -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 { @@ -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 { @@ -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)

15
src/test/kotlin/org/springframework/data/core/TypedPropertyPathKtUnitTests.kt

@ -16,7 +16,6 @@ @@ -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 { @@ -50,20 +49,20 @@ class TypedPropertyPathKtUnitTests {
),
Arguments.argumentSet(
"Person.address.country",
TypedPropertyPath.path<Person, Address>(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>(Person::address)
.then<Country>(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, Person>(Person::emergencyContact)
.then<Address>(Person::address).then<Country>(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 { @@ -85,13 +84,12 @@ class TypedPropertyPathKtUnitTests {
@Test // GH-3400
fun shouldSupportComposedPropertyReference() {
val path = TypedPropertyPath.path<Person, Address>(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<Person, Address> { it.address }
.toDotPath()).isEqualTo("address")
@ -100,7 +98,6 @@ class TypedPropertyPathKtUnitTests { @@ -100,7 +98,6 @@ class TypedPropertyPathKtUnitTests {
}
@Test // GH-3400
@Disabled()
fun shouldSupportComposedPropertyLambda() {
val path = TypedPropertyPath.path<Person, Address> { it.address };

Loading…
Cancel
Save