我试图在运行时编译和加载动态生成的Java代码.由于ClassLoader::defineClass和Unsafe::DefineAnyMousClass在这种情况下都有严重的缺点,所以我try 使用hidden classes via Lookup::defineHiddenClass.这适用于我try 加载的所有类,除了那些调用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();
    }
}

我已经try 过用不同的JDK编译和运行代码,使用不同的方法创建隐藏类的新实例,搜索https://bugs.openjdk.java.net/个错误,搞乱字节码本身和其他一些事情.我不是Java内部构件方面的专家,所以我不确定自己是否理解了正确引入隐藏类的JEP.

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

编辑:JEP个州

迁移应考虑以下因素: 若要从隐藏类中的代码调用私有NestMate实例方法,请使用InvokeVirtual或InvokeInterface而不是invokSpecial.使用invokSpecial调用私有NestMate实例方法的生成的字节码将无法通过验证.invokSpecial只能用于调用私有的嵌套构造函数.

这可能是匿名类的问题.有没有一种编译代码的方法可以避免字节码中的invokespecial?

推荐答案

您不能将任意类转换为隐藏类.

documentation of defineHiddenClass包含这样的句子

  • 在try 解析运行时常量池中由this_class指示的条目时,符号引用被认为解析为C,解析总是立即成功.

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

但在bug report JDK-8222730年里,它被明确地说过:

对于隐藏类,其指定的隐藏名称只能通过隐藏类的"this_class"常量池条目访问.

即使在隐藏类中,也不应该通过在方法或字段签名中指定其原始名称来访问该类.

我们可以查一下.即使是像这样一个简单的 case

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() {
        }
    }
}

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

同样的道理也适用于

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

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

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

一个不太直观的例子是

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

这将根据编译器和选项而失败,因为当使用StringConcatFactory时,行为类似于调用一个方法,该方法将所有非常数部分作为参数并返回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项声明的类.因此它被重定向到文档中描述的隐藏类.

但作为MethodReference的一部分构造MethodType并不像类文字那样使用这些信息来加载Class.相反,它试图通过定义类加载器加载隐藏的类,但在您发布的NoClassDefFoundError中失败.

这似乎与JDK-8130087有关,这表明普通的方法解析与MethodType的工作方式不同,在只调用方法就可以工作的情况下,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

这表明所有的障碍都被绕过了,但是当涉及到从生成的Runnable到保存lambda主体的方法的实际调用时,它将失败,因为目标类是hidden.一个急于解析符号引用的JVM可能会更早失败,也就是说,该示例可能不会打印got runnable.

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


底线是,正如一开始所说,不能将任意类转换为隐藏类.Lambda表达式并不是唯一不使用隐藏类的功能.试着感到惊讶不是个好主意.隐藏类只能与字节码生成器结合使用,必须小心地使用已知有效的功能.

Java相关问答推荐

JLS中形式参数列表后面的任何括号对用于确定方法结果中的精确数组类型的具体含义是什么?

我的scala文件失败了Scala.g4 ANTLR语法

使用标记时,场景大纲不在多个线程上运行

如何配置ActiveMQ Artemis以使用AMQP 1.0和其他协议与Java

Java记录的不同序列化/反序列化

JavaFX Maven Assembly插件一直打包到错误的JDK版本

使用Testcontainers与OpenLiberty Server进行集成测试会抛出SocketException

Java 21 struct 化连接货币,需要可预知的子任务异常排序

只需最少的代码更改即可将版本号标记添加到日志(log)

Hibernate EmptyInterceptor可以工作,但不能拦截器

基于调车场算法的科学计算器

暂停计时器

Jakarta CDI强制bean构造/注册遗留事件侦听器

Sack()步骤中的合并运算符未按预期工作

无法播放音频:从资源加载库GStreamer-Lite失败

在Spring Boot应用程序中,server.port=0的默认端口范围是多少?

Android Studio模拟器没有互联网

如何在不作为类出现的表上执行原生查询?

在具有Quarkus Panache的PostgreSQL中将JSON数据存储为JSONB时,会将其存储为转义字符串

如何使用Java ZoneID的区域设置?