Java Class-File API 指南
1、简介
Java Class-File(类文件) API 是在 JEP-484 中引入的,是 Java 24 的一部分。它旨在创建一个接口,允许在不依赖于 ASM 库的传统 JDK 内部复制实现的情况下进行类文件处理。
本文将带你了解如何从头开始构建类文件,以及如何使用类文件 API 将一个类文件转换为另一个类文件。
2、 Class-File 核心的 API
Class-File 有三个核心元素:
- 元素 - 代表代码的一部分,如变量、指令、方法或类。此外,一个元素可能包含其他元素。例如,一个类元素可能包含方法元素,而方法元素又包括变量或指令元素。
- 构建器 -(如方法构建器和代码构建器)用于创建每种类型的元素。
- 转换函数 - 可用于使用构建器将元素转换为其他元素。
3、生成类文件
使用 MethodBuilder
和 CodeBuilder
类生成类文件。
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
我们可以使用 MethodBuilder
和 CodeBuilder
类来生成与 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),其中long
和double
占用两个槽。因此,第一个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