Java Class-File API 指南

1、简介

Java Class-File(类文件) API 是在 JEP-484 中引入的,是 Java 24 的一部分。它旨在创建一个接口,允许在不依赖于 ASM 库的传统 JDK 内部复制实现的情况下进行类文件处理。

本文将带你了解如何从头开始构建类文件,以及如何使用类文件 API 将一个类文件转换为另一个类文件。

2、 Class-File 核心的 API

Class-File 有三个核心元素:

  • 元素 - 代表代码的一部分,如变量、指令、方法或类。此外,一个元素可能包含其他元素。例如,一个类元素可能包含方法元素,而方法元素又包括变量或指令元素。
  • 构建器 -(如方法构建器和代码构建器)用于创建每种类型的元素。
  • 转换函数 - 可用于使用构建器将元素转换为其他元素。

3、生成类文件

使用 MethodBuilderCodeBuilder 类生成类文件。

3.1、示例方法

先来看一个简单的代码段,它根据员工的角色和基本工资计算员工的工资:

public double calculateAnnualBonus(double baseSalary, String role) {
    if (role.equals("sales")) {
        return baseSalary * 0.35;
    }

    if (role.equals("engineer")) {
        return baseSalary * 0.25;
    }

    return baseSalary * 0.15;
}

3.2、使用 MethodBuilder 和 CodeBuilder

我们可以使用 MethodBuilderCodeBuilder 类来生成与 calculateAnnualBonus() 功能相同的方法

首先,用 Consumer<MethodBuilder> 来定义 generate() 方法,该方法将用于构造方法:

public static void generate() throws IOException {
    Consumer<MethodBuilder> calculateAnnualBonusBuilder = methodBuilder -> methodBuilder.withCode(codeBuilder -> {
        Label notSales = codeBuilder.newLabel();
        Label notEngineer = codeBuilder.newLabel();
        ClassDesc stringClass = ClassDesc.of("java.lang.String");

        // ...
    });
}

如上,我们定义了两个 Label 对象,稍后将用于在条件语句之间跳转。此外,还定义了一个 ClassDesc 常量,用于表示 String 类文件,供以后使用。

然后,在 calculateAnnualBonusBuilder 的 lambda 表达式中添加逻辑的第一部分:

codeBuilder.aload(3)
  .ldc("sales")
  .invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
  .ifeq(notSales)
  .dload(1)
  .ldc(0.35)
  .dmul()
  .dreturn()

上述逻辑的每一行解释如下:

  • 首先使用 aload(3)role 方法参数加载到引用中。需要注意的是,aload() 的参数是方法参数中变量的槽号(slot),其中 longdouble 占用两个槽。因此,第一个 baseSalary 参数位于槽 1,而 role 位于槽 3
  • 然后,使用 ldc()"sales" 常量存储在操作数栈中,以备后续操作之用。
  • 然后,调用 invokevirtual() 来处理栈中的最后一个操作数,即常量 "sales"role 参数。此外,还调用了存储在 stringClass 变量中的字符串类的 equals() 方法来比较操作数。ClassDesc.of(Z) 部分表示,我们期望该方法调用的返回类型是布尔型。
  • 然后,通过 notSales 标签变量调用 ifeq()。这意味着,只有 invokevirtual() 的布尔结果返回 true 时,生成器的后续指令才会运行。否则,程序将跳转到我们稍后定义的 notSales 绑定处。
  • 最后,如果 ifReq() 的条件返回 true,我们将使用 dload(1) 加载 baseSalary 参数。然后,将常数 0.35 加载到操作数栈,并使用 dmul() 对存储的操作数进行乘法运算。最后,使用 dreturn() 返回值

第一部分涵盖了我们要生成的方法的第一个 if 语句。要生成第二个 if 语句,我们可以在 codeBuilder()dreturn() 调用后添加更多的方法调用:

  .labelBinding(notSales)
  .aload(3)
  .ldc("engineer")
  .invokevirtual(stringClass, "equals", MethodTypeDesc.of(ClassDesc.of("Z"), stringClass))
  .ifeq(notEngineer)
  .dload(1)
  .ldc(0.25)
  .dmul()
  .dreturn()

如果 ifeq(notSales) 表达式返回 false,则运行 labelBinding(notSales)。其他操作与之前处理第一个 if 语句的操作类似。

最后,添加最后的一部分,以涵盖默认值返回:

  .labelBinding(notEngineer)
  .dload(1)
  .ldc(0.15)
  .dmul()
  .dreturn();

同样的情况也会发生在标记分支上,但现在是针对 notEngineer 标签。如果 ifeq(notEngineer) 返回 false,则运行最后一部分。

最后,为了完成 generate() 方法,我们需要定义 ClassFile 对象并将其写入 .class 文件:

var classBuilder = ClassFile.of()
  .build(ClassDesc.of("EmployeeSalaryCalculator"),
    cb - > cb.withMethod("calculateAnnualBonus", MethodTypeDesc.of(CD_double, CD_double, CD_String), 
      AccessFlag.PUBLIC.mask(), 
      calculateAnnualBonusBuilder));

Files.write(Path.of("EmployeeSalaryCalculator.class"), classBuilder);

如上,使用 ClassFile.of().build() 实例化了一个类文件生成器,并向其传递了两个参数。第一个是封装在 ClassDesc.of() 调用中的类名。第二个参数是一个 ClassBuilder consumer,用于生成带有所需方法的类。为此,我们使用 withMethod() 传递方法名称、方法签名、访问标志和之前定义的方法代码生成器。

要注意的时,我们将方法签名定义为 MethodTypeDesc.of(CD_double,CD_double,CD_String),这意味着生成的方法返回由第一个参数定义的 double,并接收一个 double 和一个 String 参数。

然后,使用 Files writer 将存储在 classBuilder 变量中的字节数组写入文件。

4、类文件转换

现在,假设我们想将一个类文件的所有内容复制到另一个类文件中。我们可以通过使用不同的转换来实现这一目的:

public static void transform() throws IOException {
    var basePath = Files.readAllBytes(Path.of("EmployeeSalaryCalculator.class"));

    CodeTransform codeTransform = ClassFileBuilder::accept;

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

    ClassFile classFile = ClassFile.of();
    byte[] transformedClass = classFile.transformClass(classFile.parse(basePath), classTransform);
    Files.write(Path.of("TransformedEmployeeSalaryCalculator.class"), transformedClass);
}

如上,首先读取上一节创建的类文件 EmployeeSalaryCalculator

然后,定义一个 CodeTransform,它接受原始类中定义的所有 CodeElements。此外,我们使用 codeTransform 创建 MethodTransform,使用 methodTransform 创建 ClassTransform。通过这种组合,我们可以轻松地将 transformer 通用化并重复用于不同的目的。

可以使用更明确的 lambda 表达式定义更多自定义代码和方法转换。例如,我们可以使用 lambda 表达式定义自定义的 MethodTransform,该表达式只接受具有特定名称的方法:

MethodTransform methodTransform = (methodBuilder, methodElement) - > {
    if (methodElement.header().name().stringValue().equals("calculateAnnualBonus")) {
        methodBuilder.withCode(codeBuilder - > {
            for (var codeElement: methodElement.code()) {
                codeBuilder.accept(codeElement);
            }
        });
    }
};

如上,首先使用 header()name() 方法检查方法名是否等于字面量 calculateAnnualBonus。如果是,就使用 methodBuilder 创建一个与原始类的 methodElement 完全相同的方法。

5、总结

本文介绍了使用 Class File API 从头开始创建类以及将内容从一个类复制到另一个类的细节。


Ref:https://www.baeldung.com/java-class-file-api