使用 Groovy™ 实现 JVM Hello World

作者: Paul King

发布时间:2022-12-22 02:24PM


对于那些还没看过的人来说,JVM Advent 的同仁们几天前发布了一篇精彩的Groovy 与数据科学博客文章,作为 2022 年 JVM Advent 系列的一部分。如果您对数据科学感兴趣,我们建议您在继续阅读本文之前先查看该文章。

JVM Advent 系列中的今天这篇文章着眼于 JVM 上的字节码库世界。让我们看看如何使用Groovy以及 ProGuardCOREASMByte Buddy 库来创建该文章中的相同 hello world 示例。

首先,我们强烈建议您先阅读前面提到的JVM Advent 文章以获取更多背景信息。毕竟,直接作为 Java 源文件(如该文章所示)或作为 Groovy 源文件创建简单的 hello-world 类文件示例非常容易,就像这样

println 'Hello world'

本博客文章中展示的示例说明了如何使用允许您直接操作生成字节码的库创建等效的类文件。如果您想了解更多 JVM 内部原理,这有点深入,并且对于许多用例也很方便,例如构建工具或动态修改 Java 类。如果您想了解更多详细信息或额外的动机,我建议您阅读这些库的网站。

ProGuardCORE

ProGuardCORE 库允许您读取、分析、修改和写入 Java 类文件。以下是我们可以如何使用它来写入 hello-world 类文件

var name = 'HelloProGuardCORE'
var superclass = 'java/lang/Object'
var classBuilder = new ClassBuilder(CLASS_VERSION_1_8, PUBLIC, name, superclass).tap {
    addMethod(PUBLIC | STATIC, 'main', '([Ljava/lang/String;)V', 100, builder ->
        builder
            .getstatic('java/lang/System', 'out', 'Ljava/io/PrintStream;')
            .ldc("Hello from $name")
            .invokevirtual('java/io/PrintStream', 'println', '(Ljava/lang/String;)V')
            .return_()
    )
}

new File("${name}.class").withDataOutputStream { dos ->
    classBuilder.programClass.accept(new ProgramClassWriter(dos))
}

这本质上是 JVM Advent 博客文章中示例的“Groovified”版本。我们正在使用库的 `ClassBuilder` 类,添加一个方法,然后添加四个字节码语句作为方法体。如果您以前没有见过方法和类型描述符语法,某些部分可能看起来有点奇怪,但您可能不会感到惊讶,它似乎正在引用一个 `System.out.println` 调用并向其传递一个常量字符串。

当我们运行此脚本时,会生成一个 `HelloProGuardCORE` 类文件。我们可以以正常方式调用该类文件

$ java HelloProGuardCORE
Hello from HelloProGuardCORE

如果您想了解更多详细信息,我们鼓励您阅读 JVM Advent 文章或库文档。

ASM

ASM 是一个通用 Java 字节码操作和分析框架。事实上,它是 Groovy 在其解析器和一些工具中使用的框架。以下是如何使用它生成与前一个示例或多或少相同的类

var name = 'HelloASM'
var superclass = 'java/lang/Object'
var cw = new ClassWriter(0)
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, name, null, superclass, null)
cw.visitMethod(ACC_PUBLIC + ACC_STATIC, 'main', '([Ljava/lang/String;)V', null, null).with {
    visitCode()
    visitFieldInsn(GETSTATIC, 'java/lang/System', 'out', 'Ljava/io/PrintStream;')
    visitLdcInsn('Hello from ' + name)
    visitMethodInsn(INVOKEVIRTUAL, 'java/io/PrintStream', 'println', '(Ljava/lang/String;)V', false)
    visitInsn(RETURN)
    visitMaxs(3, 3)
    visitEnd()
}
cw.visitEnd()

new File("${name}.class").withDataOutputStream { dos ->
    dos.write(cw.toByteArray())
}

运行此脚本后,将生成一个 `HelloASM` 类文件,以下是运行该类文件时的输出

$ java HelloASM
Hello from HelloASM

代码的某些部分应该与前面的示例相似。

Byte Buddy

Byte Buddy 是一个用于创建和修改 Java 类的代码生成和操作库。它的优势在于能够动态创建和修改类。因此,对于我们简单的示例来说,可能不需要它的强大功能。然而,这个库的一个优点是它通过其流畅的 API 隐藏了一些低级细节,例如类型和方法描述符。这是我们的示例

var name = 'HelloByteBuddy'
new ByteBuddy()
    .subclass(Object)
    .name(name)
    .defineMethod('main', Void.TYPE, PUBLIC | STATIC)
    .withParameter(String[])
    .intercept(MethodCall.invoke(
        PrintStream.getMethod('println', String))
        .onField(System.getField('out'))
        .with('Hello from ' + name))
    .make()
    .saveIn('.' as File)

与其他脚本一样,这也生成了一个类文件,我们可以按此处所示调用它

$ java HelloByteBuddy
Hello from HelloByteBuddy

以上就是我们使用这三个库的示例,但我们还有一个有趣的替代方案要介绍!

使用 Groovy ASTs

Groovy 是一种高度可扩展的语言。它提供了,除其他功能外,一种编译时元编程机制,称为 AST 转换(抽象语法树转换)。此机制使用注解来指示编译器在编译期间需要特殊处理。一个现在有些过时的 AST 转换,`@Bytecode`,尝试允许您直接在 Groovy 代码中编写字节码指令。让我们看看在这里如何使用该 AST 转换

@CompileStatic @POJO
class HelloAST {
    @Bytecode
    static void main(args) {
        getstatic 'java/lang/System.out', 'Ljava/io/PrintStream;'
        ldc 'Hello from HelloAST'
        invokevirtual 'java/io/PrintStream.println', '(Ljava/lang/String;)V'
        return
    }
}

我们直接编写 Java 或 Groovy 编译器(启用静态编译时)将生成的指令。对于此示例,我们不运行脚本以生成类文件,我们只是使用 Groovy 编译器编译它。

我们绝对不建议在任何生产代码中依赖 `@Bytecode` AST 转换,但玩玩它可能会很有趣。我们还使用了 `@CompileStatic` 和 `@POJO` AST 转换来告诉编译器我们没有使用任何 Groovy 动态特性,以便它尽可能编写类似 Java 的代码并避免调用 Groovy 运行时。

我们可以使用 javap 检查字节码,它确实有与使用其他库生成的字节码相似的字节码

public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #23                 // String Hello from HelloAST
         5: invokevirtual #29                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   Ljava/lang/Object;

因为代码没有调用 Groovy 运行时,所以我们可以在没有 Groovy jar 的情况下直接调用它

$ java HelloAST
Hello from HelloAST

我们的字节码库之旅到此结束。希望您学到了一些额外的 JVM 细节!

更多信息