JVM 使用 Groovy 的 Hello World

作者: Paul King
发布时间: 2022-12-22 02:24PM


对于那些还没有看到它的人,JVM Advent 团队 在几天前发布了一篇很棒的Groovy 和数据科学博客文章,作为 2022 JVM Advent 系列的一部分。如果您对数据科学感兴趣,我们建议您在继续本文之前先查看一下。

今天的文章 在 JVM Advent 系列中,我们正在探索 JVM 上的字节码库世界。让我们看看如何使用 GroovyProGuardCOREASMByte Buddy 库创建与那篇文章中相同的 Hello World 示例。

首先,我们强烈建议您先阅读之前提到的 JVM Advent 文章 以了解更多背景信息。毕竟,直接创建简单的 Hello World 类文件示例很容易,比如作为 Java 源文件(如该文章所示)或作为 Groovy 源文件,如下所示

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 博客文章中示例的“Groovy 化”版本。我们使用库的 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 细节!

更多信息