Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.2.0-GH-3521-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down
55 changes: 53 additions & 2 deletions src/main/java/org/springframework/data/core/PropertyPathUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
import java.util.Objects;

import org.jspecify.annotations.Nullable;

import org.springframework.asm.Type;
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

/**
* Utility class for {@link PropertyPath} and {@link PropertyReference} implementations.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 4.1
*/
public class PropertyPathUtil {
Expand All @@ -48,10 +51,48 @@ public static PropertyPath resolve(Object obj) {
return TypedPropertyPaths.of(new SerializableWrapper((Serializable) obj));
}

private record SerializableWrapper(Serializable serializable) implements PropertyReference<Object, @Nullable Object> {
private static final class SerializableWrapper
implements PropertyReference<Object, @Nullable Object>, SerializableLambdaReader.LambdaWrapper {

private final Serializable serializable;

private final Lazy<Method> bridgeMethod = Lazy.of(this::resolveBridge);

SerializableWrapper(Serializable serializable) {
this.serializable = serializable;
}

@Override
public Object unwrap() {
return serializable;
}

@Override
public @Nullable Object get(Object obj) {

Method bridgeMethod = this.bridgeMethod.getNullable();
if (bridgeMethod == null) {
return null;
}
return ReflectionUtils.invokeMethod(bridgeMethod, serializable, obj);
}

private @Nullable Method resolveBridge() {

SerializedLambda lambda = writeReplace();

Class<?> functionalInterface = ClassUtils.resolveClassName(lambda.getFunctionalInterfaceClass().replace('/', '.'),
serializable.getClass().getClassLoader());

// TODO: actual arg count vs. hardcoded value '1'
int argumentCount = Type.getArgumentCount(lambda.getFunctionalInterfaceMethodSignature());

for (Method method : functionalInterface.getDeclaredMethods()) {
if (method.getName().equals(lambda.getFunctionalInterfaceMethodName())
&& method.getParameterCount() == argumentCount) {
return method;
}
}
return null;
}

Expand All @@ -69,6 +110,16 @@ private record SerializableWrapper(Serializable serializable) implements Propert
return (SerializedLambda) ReflectionUtils.invokeMethod(method, serializable);
}

@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof SerializableWrapper that && serializable.equals(that.serializable);
}

@Override
public int hashCode() {
return serializable.hashCode();
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
*/
class PropertyReferences {

private static final Map<ClassLoader, Map<PropertyReference<?, ?>, ResolvedPropertyReference<?, ?>>> resolved = new WeakHashMap<>();
private static final Map<ClassLoader, Map<Object, ResolvedPropertyReference<?, ?>>> resolved = new WeakHashMap<>();

private static final SerializableLambdaReader reader = new SerializableLambdaReader(PropertyReference.class,
TypedPropertyPath.class, TypedPropertyPaths.class, PropertyReferences.class);
Expand All @@ -55,13 +55,15 @@ public static <P, T> PropertyReference<T, P> of(PropertyReference<T, P> lambda)
return lambda;
}

Map<PropertyReference<?, ?>, ResolvedPropertyReference<?, ?>> cache;
Map<Object, ResolvedPropertyReference<?, ?>> cache;
synchronized (resolved) {
cache = resolved.computeIfAbsent(lambda.getClass().getClassLoader(), k -> new ConcurrentReferenceHashMap<>());
}

return (PropertyReference<T, P>) cache.computeIfAbsent(lambda,
o -> new ResolvedPropertyReference(o, read(lambda)));
Object key = reader.getCacheKey(lambda);

return (PropertyReference<T, P>) cache.computeIfAbsent(key,
o -> new ResolvedPropertyReference(lambda, read(lambda)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import kotlin.jvm.JvmClassMappingKt;
import kotlin.jvm.internal.PropertyReference;
import kotlin.reflect.KClass;
import kotlin.reflect.KProperty;
import kotlin.reflect.KProperty1;

import java.io.IOException;
Expand Down Expand Up @@ -98,6 +99,7 @@
* </ul>
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 4.1
*/
class SerializableLambdaReader {
Expand Down Expand Up @@ -140,10 +142,9 @@ private static boolean isEnabled(String property, boolean defaultValue) {
*/
public MemberDescriptor read(Object lambdaObject) {

// Kotlin 2.0
Object k2Lambda = KotlinDetectorUtils.detectKotlin2SamLambda(lambdaObject);
if (k2Lambda != null) {
return KotlinDelegate.read(k2Lambda, lambdaObject);
Object property = KotlinDetectorUtils.getCapturedProperty(lambdaObject);
if (property != null) {
return KotlinDelegate.read(property, lambdaObject);
}

SerializedLambda lambda = serialize(lambdaObject);
Expand Down Expand Up @@ -177,6 +178,14 @@ public MemberDescriptor read(Object lambdaObject) {
+ ". The given value is not a lambda or method reference.");
}

Object getCacheKey(Object lambdaObject) {

Object target = lambdaObject instanceof LambdaWrapper wrapper ? wrapper.unwrap() : lambdaObject;
Object property = KotlinDetectorUtils.getCapturedProperty(target);

return property != null ? property : lambdaObject;
}

private void assertNotConstructor(SerializedLambda lambda) {

if (lambda.getImplMethodKind() == MethodHandleInfo.REF_newInvokeSpecial
Expand Down Expand Up @@ -233,30 +242,34 @@ private static SerializedLambda serialize(Object lambda) {
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.
* Detect whether the given lambda object is a Kotlin 2 SAM wrapper around a property reference.
*/
public static @Nullable Object detectKotlin2SamLambda(Object lambdaObject) {
static @Nullable Object getCapturedProperty(Object lambdaObject) {

if (!KotlinDetector.isKotlinReflectPresent()) {
return null;
}

Class<?> cls = lambdaObject.getClass();
if (!KotlinDetector.isKotlinType(cls)) {
Field field;

if (KotlinDetector.isKotlinType(cls)) {
field = ReflectionUtils.findField(cls, "function");
} else if (cls.isHidden() && ClassUtils.isLambdaClass(cls)) {
Field[] fields = new Field[1];
ReflectionUtils.doWithFields(cls, it -> fields[0] = it);
field = fields[0];
} else {
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;
Object value = ReflectionUtils.getField(field, lambdaObject);
return value != null && KotlinDelegate.isKProperty(value) ? value : null;
}

public static boolean isKotlinPropertyReference(SerializedLambda lambda) {
Expand All @@ -280,6 +293,10 @@ private static boolean isKotlinPropertyReference(@Nullable Object capturedObject
*/
static class KotlinDelegate {

static boolean isKProperty(Object candidate) {
return candidate instanceof KProperty<?>;
}

public static MemberDescriptor read(SerializedLambda lambda) {
return read(lambda.getCapturedArg(0), lambda);
}
Expand Down Expand Up @@ -605,6 +622,15 @@ private boolean matchesEntrypoint(String className) {
return false;
}


/**
* Marker interface to distinguish between a wrapped and an actual lambda.
*/
interface LambdaWrapper {

Object unwrap();
}

/**
* Value object for reading errors.
*
Expand Down
Loading
Loading