Lambda 表达式和匿名 类 在加载为隐藏 类 时不起作用

Lambda expressions and anonymous classes don't work when loaded as hidden classes

我正在尝试在运行时编译和加载动态生成的 Java 代码。由于 ClassLoader::defineClass 和 Unsafe::defineAnonymousClass 在这种情况下都有严重的缺点,我尝试使用 hidden classes via Lookup::defineHiddenClass 代替。这适用于我尝试加载的所有 classes,除了调用 lambda 表达式或包含匿名 classes 的那些 classes。

调用 lambda 表达式会引发以下异常:

Exception in thread "main" java.lang.NoClassDefFoundError: tests/HiddenClassLambdaTest$LambdaRunner/0x0000000800c04400
    at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:22)
Caused by: java.lang.ClassNotFoundException: tests.HiddenClassLambdaTest$LambdaRunner.0x0000000800c04400
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:636)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:182)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:519)
    ... 1 more

执行实例化匿名的代码 class 会引发以下错误:

Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400.run()V @5: invokespecial
  Reason:
    Type 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' (current frame, stack[2]) is not assignable to 'tests/HiddenClassLambdaTest$LambdaRunner'
  Current Frame:
    bci: @5
    flags: { }
    locals: { 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
    stack: { uninitialized 0, uninitialized 0, 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
  Bytecode:
    0000000: bb00 1159 2ab7 0013 4cb1               

    at java.base/java.lang.ClassLoader.defineClass0(Native Method)
    at java.base/java.lang.System.defineClass(System.java:2193)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2446)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClassAsLookup(MethodHandles.java:2427)
    at java.base/java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2133)
    at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:25)

这是一个重现问题的简短示例:

import java.lang.invoke.MethodHandles;

public class HiddenClassLambdaTest {
    /** This class is to be loaded and executed as hidden class */
    public static final class LambdaRunner implements Runnable {
        @Override public void run() {
            Runnable runnable = () -> System.out.println("Success");
            runnable.run();
        }
    }
    
    public static void main(String[] args) throws Throwable {
        // Path to the class file of the nested class defined above
        String nestedClassPath = HiddenClassLambdaTest.class.getTypeName().replace('.','/') + "$LambdaRunner.class";
        // Class file content of the LambdaRunner class
        byte[] classFileContents = HiddenClassLambdaTest.class.getClassLoader().getResourceAsStream(nestedClassPath).readAllBytes();
        Class<?> lambdaRunnerClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true).lookupClass();
        Runnable lambdaRunnerInstance = (Runnable) lambdaRunnerClass.getConstructor().newInstance();
        lambdaRunnerInstance.run();
    }
}

我已经尝试用不同的 JDK 编译和 运行 代码,使用不同的方式创建隐藏 class 的新实例,在 https://bugs.openjdk.java.net/ 搜索错误,搞砸了与字节码本身和其他一些东西。我不是 Java 内部专家,所以我不确定我是否没有正确理解引入隐藏 classes 的 JEP。

我是不是做错了什么,这是不可能的还是一个错误?

编辑:JEP 状态

Migration should take the following into account: To invoke private nestmate instance methods from code in a hidden class, use invokevirtual or invokeinterface instead of invokespecial. Generated bytecode that uses invokespecial to invoke a private nestmate instance method will fail verification. invokespecial should only be used to invoke private nestmate constructors.

这可能是匿名 class 的问题。有没有一种编译代码的方法可以避免在字节码中使用 invokespecial?

你不能把任意 classes 变成隐藏的 classes.

documentation of defineHiddenClass包含句子

  • On any attempt to resolve the entry in the run-time constant pool indicated by this_class, the symbolic reference is considered to be resolved to C and resolution always succeeds immediately.

它没有明确说明的是,这是唯一一个类型解析最终出现在隐藏的地方 class。

但在bug report JDK-8222730中已经明确表示:

For a hidden class, its specified hidden name should only be accessible through the hidden class's 'this_class' constant pool entry.

The class should not be accessible by specifying its original name in, for example, a method or field signature even within the hidden class.

我们可以检查一下。即使是像

这样简单的案例
public class HiddenClassLambdaTest {

    public static void main(String[] args) throws Throwable {
        byte[] classFileContents = HiddenClassLambdaTest.class
            .getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class")
            .readAllBytes();
        var hidden = MethodHandles.lookup()
            .defineHiddenClass(classFileContents, true, ClassOption.NESTMATE);
        Runnable lambdaRunnerInstance = (Runnable)hidden.findConstructor(
            hidden.lookupClass(), MethodType.methodType(void.class)).invoke();
        lambdaRunnerInstance.run();
    }

    static class LambdaRunner implements Runnable {
        LambdaRunner field = this;

        @Override
        public void run() {
        }
    }
}

已经失败了。请注意,在隐藏的 class 中解析原始 class 名称 LambdaRunner 的尝试不会失败,这是一种特殊情况,因为您使用了现有的 class 作为模板。因此,由于隐藏的 class 和现有的 LambdaRunner class 之间的不匹配,您会得到 IncompatibleClassChangeErrorVerifierError。当您不使用现有 class 的 class 定义时,您会得到 NoClassDefFoundError.

同样适用于

    static class LambdaRunner implements Runnable {
        static void method(LambdaRunner arg) {
        }

        @Override
        public void run() {
            method(this);
        }
    }

正如引用的错误报告所说,字段和方法都不能引用其签名中隐藏的 class。

一个不太直观的例子是

    static class LambdaRunner implements Runnable {
        @Override
        public void run() {
            System.out.println("" + this);
        }
    }

根据编译器和选项的不同,这将失败,因为当使用 StringConcatFactory 时,行为就像调用一个将所有 non-constant 部分作为参数并返回 String。所以这是在方法签名中隐藏 class 的另一种情况。


Lambda 表达式很特殊,class 就像

    static class LambdaRunner implements Runnable {
        @Override
        public void run() {
            Runnable runnable = () -> System.out.println("Success");
            runnable.run();
        }
    }

编译类似于

    static class LambdaRunner implements Runnable {
        @Override
        public void run() {
            Runnable runnable = LambdaRunner::lambdaBody;
            runnable.run();
        }
        private static void lambdaBody() {
            System.out.println("Success");
        }
    }

它在方法签名中没有隐藏的 class,但必须引用包含 lambda 表达式主体的方法作为 MethodReference。在常量池中,该方法的描述是指它使用 this_class 条目声明 class。因此它被重定向到隐藏的 class,如文档中所述。

但是作为 MethodReference 一部分的 MethodType 的构造不会像 class 文字那样使用此信息来加载 Class。相反,它会尝试通过定义的 class 加载程序加载隐藏的 class,该加载程序因您发布的 NoClassDefFoundError 而失败。

这似乎与 JDK-8130087 有关,这表明普通方法解析与 MethodType 工作方式不同,这可能会使 MethodType 在仅调用该方法的情况下失败.

但有可能证明即使解决这个问题也不能解决一般问题:

    static class LambdaRunner implements Runnable {
        @Override
        public void run() {
            var lookup = MethodHandles.lookup();
            var noArgVoid = MethodType.methodType(void.class);
            try {
                MethodHandle mh = LambdaMetafactory.metafactory(lookup, "run",
                    MethodType.methodType(Runnable.class), noArgVoid,
                    lookup.findStatic(LambdaRunner.class, "lambdaBody", noArgVoid),
                    noArgVoid).getTarget();
                System.out.println("got factory");
                Runnable runnable = (Runnable)mh.invokeExact();
                System.out.println("got runnable");
                runnable.run();
            }
            catch(RuntimeException|Error e) {
                throw e;
            }
            catch(Throwable e) {
                throw new AssertionError(e);
            }
        }
        private static void lambdaBody() {
            System.out.println("Success");
        }
    }

这会绕过上述问题并手动调用 LambdaMetafactory。当被重新定义为隐藏时class,它会打印:

got factory
got runnable
Exception in thread "main" java.lang.NoClassDefFoundError: test/HiddenClassLambdaTest$LambdaRunner/0x0000000800c01400
    at test/test.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:15)
Caused by: java.lang.ClassNotFoundException: test.HiddenClassLambdaTest$LambdaRunner.0x0000000800c01400
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    ... 1 more

这说明绕过了所有的障碍,但是当真正从生成的Runnable调用到持有lambda体的方法时,会失败,因为目标class 是 隐藏的 。急切解析符号引用的 JVM 可能会更早失败,即示例可能不会打印 got runnable 然后。

不像旧的JVM匿名classes,没有办法link到一个隐藏的class,甚至不能从另一个隐藏的class.


最重要的是,正如开头所说,您不能将任意 classes 变成隐藏的 classes。 Lambda 表达式并不是唯一不能使用隐藏 classes 的特性。尝试并感到惊讶不是一个好主意。隐藏的 classes 只能与字节码生成器结合使用,谨慎使用已知可用的功能。

正如 Holger 指出的那样,您不能使用 MethodHandles.Lookup.defineHiddenClass.

加载任意 classes

但是您可以对 class 文件进行一些转换以符合隐藏 class.

的限制

一个这样的转换是使用 MethodHandles::invokeExact 作为实现方法 - 因此 lambda 将捕获 MethodHandle.

以您的代码为基础,我想到了这个 - gist with imports and w/o comments:

public class HiddenClassLambdaTest {
    /** This class is to be loaded and executed as hidden class */
    public static final class LambdaRunner implements Runnable {
        @Override public void run() {
            Runnable runnable = () -> System.out.println("Success");
            runnable.run();
        }
    }
    
    public static void main(String[] args) throws Throwable {
        // Path to the class file of the nested class defined above
        byte[] classFileContents = HiddenClassLambdaTest.class
                .getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class").readAllBytes();
        
        classFileContents = processLambdas(classFileContents);
        MethodHandles.Lookup hiddenClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true);
        Runnable lambdaRunnerInstance = (Runnable) (
                hiddenClass.findConstructor(hiddenClass.lookupClass(), methodType(void.class))
                ).asType(methodType(Runnable.class)).invokeExact();
        lambdaRunnerInstance.run();
    }

我们开始喜欢您的示例代码 - 除了我们在将 classFileContents 传递给 MethodHandles.Lookup.defineHiddenClass 之前使用 ASM 对其进行处理。

    public static CallSite metafactory(MethodHandles.Lookup l, String name, MethodType mt,
            MethodType interfaceType, MethodHandle mh, MethodType dynamicMethodType) throws Throwable {
        MethodHandle invoker = MethodHandles.exactInvoker(mh.type());
        if (mt.parameterCount() == 0) {
            // Non-capturing lambda
            mt = mt.appendParameterTypes(MethodHandle.class);
            CallSite cs = LambdaMetafactory.metafactory(l, name, mt, interfaceType, invoker, dynamicMethodType);
            Object instance = cs.dynamicInvoker().asType(methodType(Object.class, MethodHandle.class)).invokeExact(mh);
            return new ConstantCallSite(MethodHandles.constant(mt.returnType(), instance));
        } else {
            // capturing
            MethodType lambdaMt = mt.insertParameterTypes(0, MethodHandle.class);
            CallSite cs = LambdaMetafactory.metafactory(l, name, lambdaMt, interfaceType, invoker, dynamicMethodType);
            return new ConstantCallSite(cs.dynamicInvoker().bindTo(mh));
        }
    }

作为技巧的一部分,我们将所有指向 LambdaMetafactory.metafactory 的 invokedynamic 指令替换为 HiddenClassLambdaTest.metafactory
在那里我们用 MethodHandles.invokeExact 替换实现方法,然后添加原始 MethodHandle 作为捕获参数的一部分。

您可能希望将该方法移动到不同的 class。

    public static CallSite altMetafactory(MethodHandles.Lookup l, String name, MethodType mt, Object... args) {
        throw new UnsupportedOperationException("Not Implemented");
    }

LambdaMetafactory.altMetafactory 相同。除了我没有实施它。 ¯\(ツ)

    private static byte[] processLambdas(byte[] bytes) {
        ClassReader cr = new ClassReader(bytes);
        ClassWriter cw = new ClassWriter(cr, 0);
        
        ClassVisitor cv = cw;
        cv = new LambdaTransformer(cv);
        
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

只是 ASM 转换的常用样板文件。
因为我预计我必须,所以我就这样写了。

    private static class LambdaTransformer extends ClassVisitor {
        LambdaTransformer(ClassVisitor parent) {
            super(ASM9, parent);
        }
        
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor,
                String signature, String[] exceptions) {
            return new LambdaMethodTransformer(super.visitMethod(access, name, descriptor, signature, exceptions));
        }
        
        private static class LambdaMethodTransformer extends MethodVisitor {
            public LambdaMethodTransformer(MethodVisitor parent) {
                super(ASM9, parent);
            }
            
            private static final ClassDesc CD_LambdaMetafactory = LambdaMetafactory.class.describeConstable().orElseThrow();
            private static final ClassDesc CD_HiddenLambdaTest = HiddenClassLambdaTest.class.describeConstable().orElseThrow();
            
            private static final DirectMethodHandleDesc LMF_FACTORY = ofCallsiteBootstrap(
                    CD_LambdaMetafactory, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType);
            private static final DirectMethodHandleDesc LMF_ALTFACTORY = ofCallsiteBootstrap(
                    CD_LambdaMetafactory, "altMetafactory", CD_Object.arrayType());
            
            private static final Handle MY_FACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType));
            private static final Handle MY_ALTFACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "altMetafactory", CD_CallSite, CD_Object.arrayType()));
            
            @Override
            public void visitInvokeDynamicInsn(String name, String descriptor,
                    Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
                MethodHandleDesc h = fromASM(bootstrapMethodHandle);
                if (h.equals(LMF_FACTORY)) {
                    super.visitInvokeDynamicInsn(name, descriptor,
                            MY_FACTORY, bootstrapMethodArguments);
                } else if (h.equals(LMF_ALTFACTORY)) {
                    super.visitInvokeDynamicInsn(name, descriptor, MY_ALTFACTORY, bootstrapMethodArguments);
                } else {
                    super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
                }
            }

这是重要的部分 - 检查 invokedynamic 指令的 bootstrap 方法是否为 LambdaMetafactory.metafactoryLambdaMetafactory.altMetafactory 并将其替换为 HiddenClassLambdaTest.metafactoryHiddenClassLambdaTest.altMetafactory分别。

            private static MethodHandleDesc fromASM(Handle h) {
                return MethodHandleDesc.of(Kind.valueOf(h.getTag(), h.isInterface()),
                        ClassDesc.ofDescriptor("L" + h.getOwner() + ";"),
                        h.getName(), h.getDesc());
            }
            
            private static Handle toASM(DirectMethodHandleDesc desc) {
                return new Handle(desc.refKind(), toInternal(desc.owner()), desc.methodName(), desc.lookupDescriptor(), desc.isOwnerInterface());
            }
            
            private static String toInternal(TypeDescriptor.OfField<?> desc) {
                String d = desc.descriptorString();
                if (d.charAt(0) != 'L') {
                    throw new IllegalArgumentException("Not a valid internal type: " + d);
                }
                return d.substring(1, d.length() - 1); // Strip "L" + ";"
            }
        }
    }
}

最后一些辅助方法让我可以轻松地在 ASM 类型和 java.lang.constant.* 类型之间进行转换。
我更喜欢使用 java.lang.constant.* API - 这些辅助方法通常是从实用程序 class.

静态导入的

但这只是故事的一半。
有关更多可能出错的事情,请参阅 Holger 的回答。
其中一些有一个“简单”的解决方案,其他的可能更复杂。

例如,要删除引用 class 的字段,请将字段类型替换为超级 class(例如 java.lang.Object)并注入 checkcast 在这些字段的每个 getfieldgetstatic 之后的指令。

要将它们从方法签名中移除 - 您还必须更改调用站点的签名。