Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 7851

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

$
0
0

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からフィールドやメソッドを FieldModelMethodModelとして取得することができます。

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 抽象を使う




Viewing all articles
Browse latest Browse all 7851

Trending Articles