9

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

调用 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

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

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$2.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 编译和运行代码,使用不同的方式创建隐藏类的新实例,在https://bugs.openjdk.java.net/上搜索错误,弄乱字节码本身和几个其他事情。我不是 Java 内部的专家,所以我不确定我是否没有理解正确引入隐藏类的 JEP。

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

编辑:JEP状态

迁移应考虑以下因素: 要从隐藏类中的代码调用私有的 nestmate 实例方法,请使用 invokevirtual 或 invokeinterface 而不是 invokespecial。使用invokespecial 调用私有nestmate 实例方法的生成字节码将无法验证。invokespecial 应该只用于调用私有的 nestmate 构造函数。

这可能是匿名类的问题。有没有办法编译代码,以便在字节码中避免调用特殊?

4

2 回答 2

8

您不能将任意类变成隐藏类。

文档defineHiddenClass包含句子

  • 在任何尝试解析由 指示的运行时常量池中的条目时this_class,符号引用被认为已解析为C并且解析总是立即成功。

它没有明确说明的是,这是类型解析最终在隐藏类中结束的唯一地方。

但是在错误报告 JDK-8222730中已经明确表示:

对于隐藏类,其指定的隐藏名称只能通过隐藏类的“this_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() {
        }
    }
}

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

这同样适用于

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

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

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

一个不太直观的例子是

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

取决于编译器和选项,这将失败,当StringConcatFactory使用 时,行为就像调用具有所有非常量部分作为参数并返回 a 的方法String。所以这是在方法签名中包含隐藏类的另一种情况。


Lambda 表达式很特殊,就像一个类

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

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

但是MethodType作为部分的构造MethodReference不使用这些信息来加载一个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手动调用。当被重新定义为隐藏类时,它将打印:

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

这表明所有的障碍都被绕过了,但是当涉及到从 generateRunnable到持有 lambda body 的方法的实际调用时,由于目标类是隐藏的,它会失败。具有急切解决符号引用的 JVM 可能会更早地失败,即示例可能不会在got runnable那时打印。

与旧的 JVM 匿名类不同,无法链接到隐藏类,甚至无法链接到另一个隐藏类。


底线是,正如开头所说,你不能将任意类变成隐藏类。Lambda 表达式并不是唯一不能用于隐藏类的功能。尝试感到惊讶并不是一个好主意。隐藏类只能与字节码生成器一起使用,谨慎使用已知的功能。

于 2022-02-23T18:19:50.310 回答
1

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

但是您可以对类文件进行一些转换以符合隐藏类的限制。

一种这样的转换是MethodHandles::invokeExact用作实现方法 - 所以 lambda 将捕获一个MethodHandle.

使用您的代码作为基础,我想出了这个 -带有导入和无注释的要点

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将 ASM 传递给MethodHandles.Lookup.defineHiddenClass.

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

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

您可能希望将该方法移至不同的类。

    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指令的引导方法是否是LambdaMetafactory.metafactoryor ,并分别用orLambdaMetafactory.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——这些辅助方法通常是从实用程序类中静态导入的。


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

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

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

于 2022-02-24T08:51:18.780 回答