Java 24 で正式公開される クラスファイルAPI(JEP 484: Class-File API)

openjdk.org


はじめに

Class-File API は以下のプレビューを経て JDK24 で正式公開される予定です。

JDK には、クラス ファイル解析/生成ライブラリである ASM がコピーされて含まれています。 ASMは外部ライブラリである故に、6ヶ月毎のリリースサイクルの中で、クラス ファイル形式の変化に追従することが難しいため、標準APIとしてクラスファイルAPIが整備されることになりました。

クラスファイルAPIは、ラムダ、レコード、シール クラス、パターン マッチングなどの近代的なJava プログラミング言語機能を前提に再設計されています。


クラスファイルの読み込み

Class-File API は、java.lang.classfile パッケージ配下に定義されます。

クラスファイルの読み込みは ClassFile を通して ClassModel を取得することで開始します。

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

ClassModel はイミュータブルで、実際の解析は必要になるまで遅延されます。

ClassModel からフィールドやメソッドを FieldModel MethodModel として取得することができます。

for (FieldModel fm : cm.fields())
    System.out.printf("Field %s%n", fm.fieldName().stringValue());
for (MethodModel mm : cm.methods())
    System.out.printf("Method %s%n", mm.methodName().stringValue());

ClassModel 自体は Iterable<ClassElement> を実装するため、以下のようにそれぞれの要素をトラバースすることもできます。

ClassModel cm = ClassFile.of().parse(bytes);
for (ClassElement ce : cm) {
    switch (ce) {
        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 -> { }
    }
}

クラスの中で、メソッドやフィールド呼び出しが行われているクラスを収集する場合は以下のようにすることができます。

ClassModel cm = ClassFile.of().parse(bytes);
Set<ClassDesc> dependencies = new HashSet<>();

for (ClassElement ce : cm) {
    if (ce instanceof MethodModel mm) {
        for (MethodElement me : mm) {
            if (me instanceof CodeModel xm) {
                for (CodeElement e : xm) {
                    switch (e) {
                        case InvokeInstruction i -> dependencies.add(i.owner().asSymbol());
                        case FieldInstruction i -> dependencies.add(i.owner().asSymbol());
                        default -> { }
                    }
                }
            }
        }
    }
}

MethodModel が、MethodElement を持ち、CodeModelCodeElement を持つといったように、Model が Element を束ねるような構造になっています。

InvokeInstruction がメソッド呼び出し命令、FieldInstruction がフィールド呼び出し命令になり、これをパターンマッチで抽出しています。

クラスファイルの構造は以下のようになっており、黄色の Code 部分が CodeElement に該当します。

先のコードは、elementStream() を使い、以下のように書くこともできます。

ClassModel cm = ClassFile.of().parse(bytes);
Set<ClassDesc> dependencies =
      cm.elementStream()
        .flatMap(ce -> ce instanceof MethodModel mm ? mm.elementStream() : Stream.empty())
        .flatMap(me -> me instanceof CodeModel com ? com.elementStream() : Stream.empty())
        .<ClassDesc>mapMulti((xe, c) -> {
            switch (xe) {
                case InvokeInstruction i -> c.accept(i.owner().asSymbol());
                case FieldInstruction i -> c.accept(i.owner().asSymbol());
                default -> { }
            }
        })
        .collect(toSet());


クラスファイルの書き込み

クラスファイルの生成は、クラスを構築する ClassBuilder 、メソッドを構築する MethodBuilder、コードを構築するCodeBuilder で行います。

ビルダーは、ラムダの引数として提供されるものを使います。以下は「hello world 」プログラムを生成する例です。

byte[] bytes = ClassFile.of().build(CD_Hello,
    clb -> clb.withFlags(ClassFile.ACC_PUBLIC)
      .withMethod(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void,
          ClassFile.ACC_PUBLIC,
          mb -> mb.withCode(
              cob -> cob.aload(0)
                    .invokespecial(ConstantDescs.CD_Object,
                                   ConstantDescs.INIT_NAME, ConstantDescs.MTD_void)
                    .return_()))
      .withMethod("main", MTD_void_StringArray, ClassFile.ACC_PUBLIC + ClassFile.ACC_STATIC,
          mb -> mb.withCode(
              cob -> cob.getstatic(CD_System, "out", CD_PrintStream)
                    .ldc("Hello World")
                    .invokevirtual(CD_PrintStream, "println", MTD_void_String)
                    .return_())));

ClassFile#build(ClassDesc thisClass, Consumer<? super ClassBuilder> handler)ClassBuilder が提供されます(上記コード例では clb)。 ClassBuilderwithXX メソッドにより、クラスの要素を構築します。メソッドの作成もクラスと同じで、提供されるMethodBuilder を介してメソッドを定義します(上記コード例では mb)。

ClassBuilder#withMethod(
  Utf8Entry name,
  Utf8Entry descriptor,
  int methodFlags,
  Consumer<? super MethodBuilder> handler)

withMethodBody() を使い、 MethodBuilder を介さずに、CodeBuilder でコードを構築することもできます。

byte[] bytes = ClassFile.of().build(CD_Hello,
    clb -> clb.withFlags(ClassFile.ACC_PUBLIC)
      .withMethodBody(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void,
                      ClassFile.ACC_PUBLIC,
                      cob -> cob.aload(0)
                                .invokespecial(ConstantDescs.CD_Object,
                                               ConstantDescs.INIT_NAME, ConstantDescs.MTD_void)
                                .return_())
      .withMethodBody("main", MTD_void_StringArray, ClassFile.ACC_PUBLIC + ClassFile.ACC_STATIC,
                      cob -> cob.getstatic(CD_System, "out", CD_PrintStream)
                                .ldc("Hello World")
                                .invokevirtual(CD_PrintStream, "println", MTD_void_String)
                                .return_()));

クラスファイルの変換

クラスファイルの変換は、ClassModel を取得し、ビルダーを介して行います。 各ビルダーには with(element) メソッドがあり、変更のない要素は、このメソッドにより透過させることができます。

以下の例は、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"))) {
                classBuilder.with(ce);
            }
        }
    });

クラスの変換は最も良く行われるため、ClassModel に対応する ClassTransform のように、各モデルに対応する XxxTransform 型が準備されています。 加えて、各ビルダー型は、子モデルを変換する transformYyy メソッドを持ちます。

これらを使えば、前述の debug で始まるメソッドを取り除く例は以下のように書くことができます。

ClassTransform ct = (builder, element) -> {
    if (!(element instanceof MethodModel mm && mm.methodName().stringValue().startsWith("debug")))
        builder.with(element);
};
var cc = ClassFile.of();
byte[] newBytes = cc.transformClass(cc.parse(bytes), ct);

ClassTransform.drop というコンビニエントメソッドを使えば、以下のようにさらに簡単に書くこともできます。

ClassTransform ct = ClassTransform.dropping(
            element -> element instanceof MethodModel mm
                    && mm.methodName().stringValue().startsWith("debug"));

トランスフォームを使った例はわずかに短くなるが、この方法でトランスフォームを表現する利点は、トランスフォーム操作をより簡単に組み合わせられることだ。例えば、Foo上の静的メソッドの呼び出しをBar上の対応するメソッドにリダイレクトしたいとします。これをCodeElementPREVIEWのトランスフォームとして表現することができます:


まとめ

  • JDK24 にて、クラス ファイル解析/生成を行う Class-File API が提供される
  • Class-File API は、java.lang.classfile パッケージ
  • クラスファイルは、ClassModelMethodModel のようなモデルと、その要素である ClassElementMethodElement によるツリー構造で構造化され、各要素は、イミュータブル
  • 各要素は、ビルダーを介して生成する
  • 変換は、ClassModel に対応する ClassTransform のような Transform 抽象を使う