JDK22新特性Class-File API尝鲜

前言

到目前为止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文件,也就是读取它的类信息,既然是读取类,我们就写一个类之后再编译。

public class Test {

    public String name = "Enaium";

    public void render() {
        System.out.println(name);
    }
}

之后我们在IntelliJ IDEA中编译一下,然后我们就可以读取这个class文件了。

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数组。

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源码中,获取它有哪些实现类,ClassEntryFieldRefEntryMethodRefEntry等等。

读取字段信息

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方法获取字段的类型的符号。

读取方法信息

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

创建类信息

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文件。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class Enaium {
}

添加字段信息

classBuilder.withField("name", ClassDesc.ofDescriptor("Ljava/lang/String;"), ClassFile.ACC_PUBLIC);

这里使用withField方法添加一个字段,这个方法传入字段的名字,字段的类型,字段的修饰符。

添加方法信息

classBuilder.withMethod("<init>", MethodTypeDesc.ofDescriptor("()V"), ClassFile.ACC_PUBLIC, methodBuilder -> {
});

这里使用withMethod方法添加一个方法,这个方法传入方法的名字,方法的类型,方法的修饰符,一个MethodBuilder的消费者,我们这里只是创建一个空的方法。

添加代码信息

这里我们为刚才创建好的字段添加一个值。

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方法加载thisinvokespecial方法调用父类的构造方法,putfield方法设置字段的值,return_方法返回。

现在我们可以创建一个方法用来获取刚才创建好的字段。

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方法加载thisgetfield方法获取字段,areturn方法返回,这里返回的是一个对象,所以和刚才的return_不一样。

之后我也可以添加一个方法来设置刚才创建好的字段。

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_();
    });
});

测试

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的文章。