I’m trying to create a small utility to replace my use of reflection across my entire project (mostly for performance benefits of using LambdaMetafactory) but I’m stumbling at the creation of a CallSite. However, the issue only appears to happen when accessing classes that are not my own. Accessing 3rd party libraries or even Java’s own classes (java.lang.Object for instance) will result in a NoClassDefFoundError not for the 3rd party class, but for my interface.
public final class Accessor {
private static Constructor<MethodHandles.Lookup> lookupConstructor;
static {
newLookupConstructor();
}
protected static void newLookupConstructor() {
try {
lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class);
lookupConstructor.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("MethodHandles.Lookup class constructor (Class) not found! Check java version.");
}
}
private Accessor() { }
public static <T> T to(Class<T> interfaze, Class<?> clazz, String method, Class<?>... params) {
try {
return to(interfaze, clazz.getDeclaredMethod(method, params));
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
public static <T> T to(Class<T> interfaze, Method method) {
try {
MethodHandles.Lookup caller = lookupConstructor.newInstance(method.getDeclaringClass());
MethodHandle implMethod = caller.unreflect(method);
CallSite site = LambdaMetafactory.metafactory(caller, method.getName(), MethodType.methodType(interfaze), implMethod.type(), implMethod, implMethod.type());
// ^ java.lang.NoClassDefFoundError for the passed interfaze class
return (T) site.getTarget().invoke();
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
}
Unit tests I’ve run demonstrating the issue can be found here:
final class AccessorTest {
@Test // SUCCESS
@DisplayName("Verify MethodHandles.Lookup constructor")
void lookupConstructorAvailabilityTest() {
Assertions.assertDoesNotThrow(() -> Accessor.newLookupConstructor());
}
@Test // SUCCESS
@DisplayName("Verify available matching instance method is called")
void findMatchingMethodAndCallTest() {
ObjectAccessor accessor = Accessor.to(ObjectAccessor.class, TestObject.class, "instanceMethod");
Assertions.assertNotNull(accessor);
Assertions.assertTrue(accessor.instanceMethod(new TestObject()));
}
@Test // SUCCESS
@DisplayName("Verify available matching static method is called")
void findMatchingStaticMethodAndCallTest() {
ObjectAccessor accessor = Accessor.to(ObjectAccessor.class, TestObject.class, "staticMethod");
Assertions.assertNotNull(accessor);
Assertions.assertTrue(accessor.staticMethod());
}
@Test // FAILURE
@DisplayName("Verify java.lang.Object#toString works")
void testDynamicToStringInvokation() {
ToString accessor = Accessor.to(ToString.class, Object.class, "toString");
// ^ java.lang.NoClassDefFoundError: com/gmail/justisroot/autoecon/data/AccessorTest$ToString
Assertions.assertNotNull(accessor);
Assertions.assertEquals(accessor.toString(Integer.valueOf(42)), "42");
}
public interface ObjectAccessor {
public boolean instanceMethod(TestObject o);
public boolean staticMethod();
}
public interface ToString {
public String toString(Object o);
}
}
This will throw the following:
java.lang.NoClassDefFoundError: com/gmail/justisroot/autoecon/data/AccessorTest$ToString at java.base/jdk.internal.misc.Unsafe.defineAnonymousClass0(Native Method) at java.base/jdk.internal.misc.Unsafe.defineAnonymousClass(Unsafe.java:1223) at java.base/java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:320) at java.base/java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:188) at java.base/java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:317) at com.gmail.justisroot.autoecon.data.Accessor.to(Accessor.java:43)
I’ve been spending way too many hours wracking my brain for solutions. A second pair of eyes would certainly be helpful.
What am I doing wrong?
You are using a
Lookup
object representing the declaring class of theMethod
object. Therefore, when the target method isObject.class.getDeclaredMethod("toString")
, you are creating a lookup object forjava.lang.Object
, which is loaded by the bootstrap loader.As a consequence, you can only access classes known to the bootstrap loader, which precludes your own
ToString
interface.Generally, when combining arbitrary interfaces and target methods, you must find a class loader which knows both. This wouldn’t be required by the underlying generator facility in OpenJDK, but the
LambdaMetafactory
enforces this.Likewise, it enforces that the lookup object must have
private
access to the lookup class, even when the class is otherwise irrelevant, e.g. when accessing onlypublic
artifacts. That’s whyMethodHandles.publicLookup()
does not work.But when both, the
interface
and the target method, are accessible from the class loader of your current code,MethodHandles.lookup()
should work, without the need to hack into the internals.