NoClassDefFoundError for my own class when creating CallSite with LambdaMetafactory

649 Views Asked by At

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?

1

There are 1 best solutions below

2
On BEST ANSWER

You are using a Lookup object representing the declaring class of the Method object. Therefore, when the target method is Object.class.getDeclaredMethod("toString"), you are creating a lookup object for java.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 only public artifacts. That’s why MethodHandles.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.