首发于Enaium的个人博客
前言
到目前为止JDK22已经Final Release Candidate了,不出意外的话,这个就是最终General Availability版本了,在本次更新有一个新的的特性也就是,Class-File API,不过还是在预览版中,不过我们可以尝鲜一下,也就是在未来的版本中可能会被删除或者修改,大家在之前可能使用过ASM等第三方库,但现在JDK是每6个月就会发布一个新的版本,第三方库可能会更新不及时,所以JDK内置了一个Class-File API,这样就可以更好的支持Java的新特性。
安装
我们先需要在jdk.java.net下载JDK22,之后再IntelliJ IDEA中开启22(Preview),之后就可以使用Class-File API了。
使用
读取类信息
我们首先是读取一个class文件,也就是读取它的类信息,既然是读取类,我们就写一个类之后再编译。
java
public class Test {
public String name = "Enaium";
public void render() {
System.out.println(name);
}
}
之后我们在IntelliJ IDEA中编译一下,然后我们就可以读取这个class文件了。
java
void main() throws IOException {
ClassFile.of().parse(Path.of("out/production/untitled1/Test.class"));
ClassFile.of().parse(Files.readAllBytes(Path.of("out/production/untitled1/Test.class")));
}
我们可以看到ClassFile有一个of方法,这个方法返回一个ClassFile对象,然后我们可以调用parse方法解析class文件,这里可以使用两种方法,一种是传入Path对象,一种是传入byte数组。
java
void main() throws IOException {
final ClassModel parse = ClassFile.of().parse(Path.of("out/production/untitled1/Test.class"));
System.out.println(parse.majorVersion());
System.out.println(parse.superclass().get().name());
for (PoolEntry poolEntry : parse.constantPool()) {
System.out.println(STR." \{poolEntry.toString()}");
}
}
我们可以看到ClassFile有一个parse方法,这个方法返回一个ClassModel对象,然后我们可以调用majorVersion方法获取class文件的版本,superclass方法获取父类,constantPool方法获取常量池。
其中PoolEntry比较特殊,它是一个接口,所以我直接调用toString方法,这个方法返回一个String对象,这个对象就是常量池的内容,我们进入到JDK源码中,获取它有哪些实现类,ClassEntry、FieldRefEntry、MethodRefEntry等等。
读取字段信息
java
void main() throws IOException {
final ClassModel parse = ClassFile.of().parse(Path.of("out/production/untitled1/Test.class"));
for (FieldModel field : parse.fields()) {
System.out.println(STR."\{field.flags().flags()} \{field.fieldName()}: \{field.fieldType()} | \{field.fieldTypeSymbol().packageName()}.\{field.fieldTypeSymbol().displayName()}");
}
}
我们调用fields可以获取这个类中的所有字段,然后我们可以调用flags方法获取字段的修饰符,fieldName方法获取字段的名字,fieldType方法获取字段的类型,fieldTypeSymbol方法获取字段的类型的符号。
读取方法信息
java
void main() throws IOException {
final ClassModel parse = ClassFile.of().parse(Path.of("out/production/untitled1/Test.class"));
for (MethodModel method : parse.methods()) {
System.out.println(STR."\{method.flags().flags()} \{method.methodName()}\{method.methodType()}");
for (CodeElement codeElement : method.code().get()) {
System.out.println(STR." \{codeElement}");
}
}
}
和读取字段不同的是,可以使用code方法获取方法的指令,类似于ASM的中的Instruction。
创建类信息
java
void main() throws IOException {
final String name = "Enaium";
final byte[] build = ClassFile.of().build(ClassDesc.of(name), classBuilder -> {
});
Files.write(Path.of(STR."\{name}.class"), build);
}
这里使用ClassFile中的build方法构建一个class文件,这个方法传入一个类信息,一个ClassBuilder的消费者,我们这里只是创建一个空的class文件,之后我们将返回的byte数组写入到文件中,之后我们使用IntelliJ IDEA打开这个class文件,我们可以看到这个class文件是一个空的class文件。
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class Enaium {
}
添加字段信息
java
classBuilder.withField("name", ClassDesc.ofDescriptor("Ljava/lang/String;"), ClassFile.ACC_PUBLIC);
这里使用withField方法添加一个字段,这个方法传入字段的名字,字段的类型,字段的修饰符。
添加方法信息
java
classBuilder.withMethod("<init>", MethodTypeDesc.ofDescriptor("()V"), ClassFile.ACC_PUBLIC, methodBuilder -> {
});
这里使用withMethod方法添加一个方法,这个方法传入方法的名字,方法的类型,方法的修饰符,一个MethodBuilder的消费者,我们这里只是创建一个空的方法。
添加代码信息
这里我们为刚才创建好的字段添加一个值。
java
methodBuilder.withCode(codeBuilder -> {
codeBuilder.aload(codeBuilder.receiverSlot());
codeBuilder.invokespecial(ClassDesc.ofDescriptor("Ljava/lang/Object;"), "<init>", MethodTypeDesc.ofDescriptor("()V"));
codeBuilder.aload(codeBuilder.receiverSlot());
codeBuilder.ldc("Enaium");
codeBuilder.putfield(ClassDesc.of(name), "name", ClassDesc.ofDescriptor("Ljava/lang/String;"));
codeBuilder.return_();
});
这里使用withCode方法添加代码,这个方法传入一个CodeBuilder的消费者,这里我们使用aload方法加载this,invokespecial方法调用父类的构造方法,putfield方法设置字段的值,return_方法返回。
现在我们可以创建一个方法用来获取刚才创建好的字段。
java
classBuilder.withMethod("getName", MethodTypeDesc.ofDescriptor("()Ljava/lang/String;"), ClassFile.ACC_PUBLIC, methodBuilder -> {
methodBuilder.withCode(codeBuilder -> {
codeBuilder.aload(codeBuilder.receiverSlot());
codeBuilder.getfield(ClassDesc.of(name), "name", ClassDesc.ofDescriptor("Ljava/lang/String;"));
codeBuilder.areturn();
});
});
这里使用withCode方法添加代码,这个方法传入一个CodeBuilder的消费者,这里我们使用aload方法加载this,getfield方法获取字段,areturn方法返回,这里返回的是一个对象,所以和刚才的return_不一样。
之后我也可以添加一个方法来设置刚才创建好的字段。
java
classBuilder.withMethod("setName", MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V"), ClassFile.ACC_PUBLIC, methodBuilder -> {
methodBuilder.withCode(codeBuilder -> {
codeBuilder.aload(codeBuilder.receiverSlot());
codeBuilder.aload(codeBuilder.parameterSlot(0));
codeBuilder.putfield(ClassDesc.of(name), "name", ClassDesc.ofDescriptor("Ljava/lang/String;"));
codeBuilder.return_();
});
});
测试
java
final Object o = URLClassLoader.newInstance(Collections.singleton(Path.of(".").toUri().toURL()).toArray(URL[]::new)).loadClass(name).getConstructor().newInstance();
final Method getName = o.getClass().getMethod("getName");
System.out.println(getName.invoke(o));
final Method setName = o.getClass().getMethod("setName", String.class);
setName.invoke(o, "This is enaium's class file");
System.out.println(getName.invoke(o));
这里我们使用URLClassLoader加载刚才创建好的class文件,之后我们就可以使用反射调用里面的方法了。
总结
本篇文章简单的使用了Class-File API,之后我会继续深入的了解这个新特性,也会写一些关于Class-File API的文章。