简体   繁体   中英

NoClassDefFoundError for my own class when creating CallSite with LambdaMetafactory

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 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, eg 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM