深入探讨Java 22:Class-File API | 作者:Ben Weidig - Medium

作者:API传播员 · 2025-11-07 · 阅读时间:4分钟

一个元素描述类文件的某个部分。它是不可变的,可以小到一条指令,也可以大到整个类文件,以及介于两者之间的所有内容,如属性、字段或方法。

元素可以进一步包含它们自己的元素,使它们成为复合元素,如方法或类。

对于每种类型的复合元素,都有一个匹配的生成器,它配备了特定的构建方法,例如 ClassBuilder::withMethod。构建器还充当相关元素类型的消费者。

一个transform 是一个接受元素和构建器的函数,指导如何或是否将该元素转换为其他元素。


使用模式解析类文件

ASM 使用访问者模式来创建类文件视图,而新的 Class-File API 则采用了不同的方式,它依赖于 Java 17 中引入并在 Java 21 中最终确定的特性:模式匹配。

要创建这样的模型,我们可以将字节转换为实例:

ClassModel classModel = ClassFile.of().parse(bytes);

我们可以用循环结构遍历特定元素:

ClassModel cm = ClassFile.of().parse(bytes);

for (FieldModel fm : cm.fields()) {
    System.out.printf("Field %s%n", fm.fieldName().stringValue());
}

模型是一系列 Class 元素,我们也可以利用模式匹配来更有组织地处理:

ClassModel cm = ClassFile.of().parse(bytes);

for (ClassElement classElement : cm) {
    switch (cm) {
        case MethodModel mm -> System.out.printf("Method %s%n",
            mm.methodName().stringValue());
        case FieldModel fm -> System.out.printf("Field %s%n",
            fm.fieldName().stringValue());
        default -> { /* NO-OP */ }
    }
}

这种方式虽然功能强大,但代码层级较深,阅读起来较为困难。为了解决这一问题,可以通过 Stream 管道将不同的步骤重构为方法,从而进一步简化代码。


生成类文件

解析类文件只是操作的一部分,另一部分是生成新的类文件。

假设我们想生成一个包含简单方法的类。传统的 ASM 使用基于访问者的方式来实现,而新的 Class-File API 则采用了 lambda 接受构建器的方法:

构建器没有通过 visitVarInsn 传递指令,而是直接内置了许多方便的方法,如 aloadiload。这种方式更加直接和明确。

此外,Class-File API 还提供了更高级别的块范围和局部变量索引计算功能。由于 API 自动处理了块范围,我们不再需要手动创建标签或分支指令。这不仅简化了代码,还提升了代码的可维护性。


转换类文件

类文件库通常用于将读写步骤组合成转换操作:读取一个类并应用本地化更改,但其中大部分不会改变。为此,每个构建器都提供了 with 方法,使元素可以直接通过而无需任何转换。

例如,以下代码展示了如何从名称以 "debug" 开头的类中删除所有方法:

ClassModel classModel = ClassFile.of().parse(bytes);

byte[] newBytes = ClassFile.of().build(classModel.thisClass().asSymbol(),
    classBuilder -> {
        for (ClassElement ce : classModel) { 
            if (ce instanceof MethodModel mm
                && mm.methodName().stringValue().startsWith("debug")) {
                continue;
            }
            classBuilder.with(ce);
        }
    });

对于简单的转换,这种方式已经足够。然而,在类树中导航并检查每个元素可能会导致大量重复的样板代码,尤其是在嵌套代码较深的情况下。

以下代码展示了如何将类 "Foo" 上的方法调用替换为另一个名为 "Bar" 的类:

CodeTransform codeTransform = (codeBuilder, e) -> {
    switch (e) {
        case InvokeInstruction i when i.owner().asInternalName().equals("Foo") ->
            codeBuilder.invokeInstruction(i.opcode(),
                ClassDesc.of("Bar"),
                i.name().stringValue(),
                i.typeSymbol(),
                i.isInterface());
        default -> codeBuilder.accept(e);
    }
};

通过基于 lambda 的 transforms 方法,可以显著减少嵌套代码和样板代码:

MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform);
ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform);

最终,代码变得更加简洁:

ClassFile cc = ClassFile.of();
byte[] newBytes = cc.transform(cc.parse(bytes),
    ClassTransform.transformingMethods(
        MethodTransform.transformingCode(
            firstTransform.andThen(secondTransform))));

通过这种方式,我们可以为常见用例构建一系列简单的转换,并根据需要进行组合。这种方法不仅提升了代码的可读性,还增强了其灵活性和可维护性。

原文链接: https://medium.com/@benweidig/looking-at-java-22-class-file-api-a4cb241ff785