Simplify DefaultMethodInvokingMethodInterceptor.

As our baseline is now Java 17, we can remove all indirections to produce a Lookup previously needed to support Java 8 and 9.

Fixes #2971.
This commit is contained in:
Oliver Drotbohm
2023-11-08 08:06:33 +01:00
parent 96f8ee25fe
commit 38b3099ec5
2 changed files with 29 additions and 164 deletions

View File

@@ -19,7 +19,6 @@ import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
@@ -28,7 +27,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
@@ -44,7 +42,7 @@ import org.springframework.util.ReflectionUtils;
*/
public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor {
private final MethodHandleLookup methodHandleLookup = MethodHandleLookup.getMethodHandleLookup();
private static final Lookup LOOKUP = MethodHandles.lookup();
private final Map<Method, MethodHandle> methodHandleCache = new ConcurrentReferenceHashMap<>(10, ReferenceType.WEAK);
/**
@@ -64,7 +62,7 @@ public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor
@Nullable
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
@@ -84,7 +82,7 @@ public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor
if (handle == null) {
handle = methodHandleLookup.lookup(method);
handle = lookup(method);
methodHandleCache.put(method, handle);
}
@@ -92,153 +90,20 @@ public class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor
}
/**
* Strategies for {@link MethodHandle} lookup.
* Lookup a {@link MethodHandle} given {@link Method} to look up.
*
* @since 2.0
* @param method must not be {@literal null}.
* @return the method handle.
* @throws ReflectiveOperationException
*/
enum MethodHandleLookup {
private static MethodHandle lookup(Method method) throws ReflectiveOperationException {
/**
* Encapsulated {@link MethodHandle} lookup working on Java 9.
*/
ENCAPSULATED {
Lookup lookup = MethodHandles.privateLookupIn(method.getDeclaringClass(), LOOKUP);
MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
Class<?> declaringClass = method.getDeclaringClass();
private final @Nullable Method privateLookupIn = ReflectionUtils.findMethod(MethodHandles.class,
"privateLookupIn", Class.class, Lookup.class);
@Override
MethodHandle lookup(Method method) throws ReflectiveOperationException {
if (privateLookupIn == null) {
throw new IllegalStateException("Could not obtain MethodHandles.privateLookupIn");
}
return doLookup(method, getLookup(method.getDeclaringClass(), privateLookupIn));
}
@Override
boolean isAvailable() {
return privateLookupIn != null;
}
private Lookup getLookup(Class<?> declaringClass, Method privateLookupIn) {
Lookup lookup = MethodHandles.lookup();
try {
return (Lookup) privateLookupIn.invoke(MethodHandles.class, declaringClass, lookup);
} catch (ReflectiveOperationException e) {
return lookup;
}
}
},
/**
* Open (via reflection construction of {@link MethodHandles.Lookup}) method handle lookup. Works with Java 8 and
* with Java 9 permitting illegal access.
*/
OPEN {
private final Lazy<Constructor<Lookup>> constructor = Lazy.of(MethodHandleLookup::getLookupConstructor);
@Override
MethodHandle lookup(Method method) throws ReflectiveOperationException {
if (!isAvailable()) {
throw new IllegalStateException("Could not obtain MethodHandles.lookup constructor");
}
Constructor<Lookup> constructor = this.constructor.get();
return constructor.newInstance(method.getDeclaringClass()).unreflectSpecial(method, method.getDeclaringClass());
}
@Override
boolean isAvailable() {
return constructor.orElse(null) != null;
}
},
/**
* Fallback {@link MethodHandle} lookup using {@link MethodHandles#lookup() public lookup}.
*
* @since 2.1
*/
FALLBACK {
@Override
MethodHandle lookup(Method method) throws ReflectiveOperationException {
return doLookup(method, MethodHandles.lookup());
}
@Override
boolean isAvailable() {
return true;
}
};
private static MethodHandle doLookup(Method method, Lookup lookup)
throws NoSuchMethodException, IllegalAccessException {
MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
if (Modifier.isStatic(method.getModifiers())) {
return lookup.findStatic(method.getDeclaringClass(), method.getName(), methodType);
}
return lookup.findSpecial(method.getDeclaringClass(), method.getName(), methodType, method.getDeclaringClass());
}
/**
* Lookup a {@link MethodHandle} given {@link Method} to look up.
*
* @param method must not be {@literal null}.
* @return the method handle.
* @throws ReflectiveOperationException
*/
abstract MethodHandle lookup(Method method) throws ReflectiveOperationException;
/**
* @return {@literal true} if the lookup is available.
*/
abstract boolean isAvailable();
/**
* Obtain the first available {@link MethodHandleLookup}.
*
* @return the {@link MethodHandleLookup}
* @throws IllegalStateException if no {@link MethodHandleLookup} is available.
*/
public static MethodHandleLookup getMethodHandleLookup() {
for (MethodHandleLookup it : MethodHandleLookup.values()) {
if (it.isAvailable()) {
return it;
}
}
throw new IllegalStateException("No MethodHandleLookup available");
}
@Nullable
private static Constructor<Lookup> getLookupConstructor() {
try {
Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
ReflectionUtils.makeAccessible(constructor);
return constructor;
} catch (Exception ex) {
// this is the signal that we are on Java 9 (encapsulated) and can't use the accessible constructor approach.
if (ex.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
return null;
}
throw new IllegalStateException(ex);
}
}
return Modifier.isStatic(method.getModifiers())
? lookup.findStatic(declaringClass, method.getName(), methodType)
: lookup.findSpecial(declaringClass, method.getName(), methodType, declaringClass);
}
}

View File

@@ -16,11 +16,9 @@
package org.springframework.data.projection;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.MethodHandleLookup;
import org.springframework.data.util.Version;
import org.springframework.aop.framework.ProxyFactory;
/**
* Unit tests for {@link DefaultMethodInvokingMethodInterceptor}.
@@ -30,22 +28,24 @@ import org.springframework.data.util.Version;
*/
class DefaultMethodInvokingMethodInterceptorUnitTests {
@Test // DATACMNS-1376
void shouldApplyEncapsulatedLookupOnJava9AndHigher() {
@Test // GH-2971
void invokesDefaultMethodOnProxy() {
assumeThat(Version.javaVersion()).isGreaterThanOrEqualTo(Version.parse("9.0"));
ProxyFactory factory = new ProxyFactory();
factory.setInterfaces(Sample.class);
factory.addAdvice(new DefaultMethodInvokingMethodInterceptor());
assertThat(MethodHandleLookup.getMethodHandleLookup()).isEqualTo(MethodHandleLookup.ENCAPSULATED);
assertThat(MethodHandleLookup.ENCAPSULATED.isAvailable()).isTrue();
Object proxy = factory.getProxy();
assertThat(proxy).isInstanceOfSatisfying(Sample.class, it -> {
assertThat(it.sample()).isEqualTo("sample");
});
}
@Test // DATACMNS-1376
void shouldApplyOpenLookupOnJava8() {
interface Sample {
assumeThat(Version.javaVersion()).isLessThan(Version.parse("1.8.9999"));
assertThat(MethodHandleLookup.getMethodHandleLookup()).isEqualTo(MethodHandleLookup.OPEN);
assertThat(MethodHandleLookup.OPEN.isAvailable()).isTrue();
assertThat(MethodHandleLookup.ENCAPSULATED.isAvailable()).isFalse();
default String sample() {
return "sample";
}
}
}