はじめに
Class-File API は以下のプレビューを経て JDK24 で正式公開される予定です。
- JDK22 JEP 457: Class-File API (Preview)
- JDK23 JEP 466: Class-File API (Second Preview)
- JDK24 JEP 484: Class-File API
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
を持ち、CodeModel
が CodeElement
を持つといったように、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
)。
ClassBuilder
の withXX
メソッドにより、クラスの要素を構築します。メソッドの作成もクラスと同じで、提供される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
パッケージ - クラスファイルは、
ClassModel
やMethodModel
のようなモデルと、その要素であるClassElement
やMethodElement
によるツリー構造で構造化され、各要素は、イミュータブル - 各要素は、ビルダーを介して生成する
- 変換は、
ClassModel
に対応するClassTransform
のような Transform 抽象を使う