使用 LambdaMetafactory 创建 CallSite 时我自己 class 的 NoClassDefFoundError

NoClassDefFoundError for my own class when creating CallSite with LambdaMetafactory

我正在尝试创建一个小实用程序来代替我在整个项目中使用反射(主要是为了使用 LambdaMetafactory 的性能优势),但我在创建 CallSite 时遇到了麻烦。但是,问题似乎只在访问不属于我自己的 class 时发生。访问第 3 方库甚至 Java 自己的 classes(例如 java.lang.Object)将导致 NoClassDefFoundError 不是针对第 3 方 class,而是针对我的界面。

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;
        }
    }
}

我已经 运行 证明问题的单元测试可以在这里找到:

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);
    }
}

这将抛出以下内容:

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)

我花了太多时间绞尽脑汁寻找解决方案。第二双眼睛肯定会有所帮助。

我做错了什么?

您正在使用 Lookup 对象来表示 Method 对象的声明 class。因此,当目标方法是 Object.class.getDeclaredMethod("toString") 时,您正在为 java.lang.Object 创建一个查找对象,它由 bootstrap 加载器加载。

因此,您只能访问 bootstrap 加载程序已知的 classes,这排除了您自己的 ToString 接口。

通常,在组合任意接口和目标方法时,您必须找到一个 class 两者都知道的加载器。 OpenJDK 中的底层生成器工具不需要这样做,但 LambdaMetafactory 强制执行此操作。

同样,它强制查找对象必须 private 访问查找 class,即使 class 在其他方面无关紧要,例如仅访问 public 个工件时。这就是 MethodHandles.publicLookup() 不起作用的原因。

但是当 interface 和目标方法都可以从当前代码的 class 加载程序访问时,MethodHandles.lookup() 应该可以工作,而无需黑入内部。