モジュール java.base

パッケージjava.lang.classfile.components


パッケージjava.lang.classfile.components
java.lang.classfile.componentsは、JavaプラットフォームのプレビューAPIです。
プレビュー機能が有効な場合のみ、プログラムでjava.lang.classfile.componentsを使用できます。
プレビュー機能は、今後のリリースで削除するか、Javaプラットフォームの永続機能にアップグレードすることができます。

java.lang.classfilePREVIEWライブラリ上に構築された特定のコンポーネント、変換およびツールを提供します。

java.lang.classfile.componentsパッケージには、最小限の労力で非常に複雑なタスクを構成するのに役立つ特定の変換コンポーネントおよびユーティリティ・クラスが含まれています。

ClassPrinterPREVIEW

ClassPrinterPREVIEWは、ClassModelPREVIEWFieldModelPREVIEWMethodModelPREVIEWまたはCodeModelPREVIEWを、JSON、XMLまたはYAML形式の人間が読める構造化テキストに、またはトラバース可能なノードと印刷可能なノードのツリーにシームレスにエクスポートするヘルパー・クラスです。

ClassPrinterPREVIEWの主な目的は、デバッグ、例外処理およびロギングの目的で人間が読めるクラス情報を提供することです。 また、印刷クラスは、自動オフライン処理をサポートするために標準形式に準拠しています。

最も頻繁なユース・ケースは、単にクラスを出力することです。

ClassPrinter.toJson(classModel, ClassPrinter.Verbosity.TRACE_ALL, System.out::print);

ClassPrinterPREVIEWを使用すると、単純な印刷可能ノードのツリーを横断してカスタム・プリンタをフックできます:

void customPrint(ClassModel classModel) {
    print(ClassPrinter.toTree(classModel, ClassPrinter.Verbosity.TRACE_ALL));
}

void print(ClassPrinter.Node node) {
    switch (node) {
        case ClassPrinter.MapNode mn -> {
            // print map header
            mn.values().forEach(this::print);
        }
        case ClassPrinter.ListNode ln -> {
            // print list header
            ln.forEach(this::print);
        }
        case ClassPrinter.LeafNode n -> {
            // print leaf node
        }
    }
}

ClassPrinterPREVIEWのもう1つのユースケースは、自動テストの記述を簡略化することです:

@Test
void printNodesInTest(ClassModel classModel) {
    var classNode = ClassPrinter.toTree(classModel, ClassPrinter.Verbosity.TRACE_ALL);
    assertContains(classNode, "method name", "myFooMethod");
    assertContains(classNode, "field name", "myBarField");
    assertContains(classNode, "inner class", "MyInnerFooClass");
}

void assertContains(ClassPrinter.Node node, ConstantDesc key, ConstantDesc value) {
    if (!node.walk().anyMatch(n -> n instanceof ClassPrinter.LeafNode ln
                           && ln.name().equals(key)
                           && ln.value().equals(value))) {
        node.toYaml(System.out::print);
        throw new AssertionError("expected %s: %s".formatted(key, value));
    }
}

ClassRemapperPREVIEW

ClassRemapperは、指定されたマップまたはマップ関数に従って、ClassTransformPREVIEWFieldTransformPREVIEWMethodTransformPREVIEWおよびCodeTransformPREVIEWですべてのクラス参照を任意の形式で深く再マッピングします。

再マッピングは、スーパークラス、インタフェース、あらゆる種類の記述子およびシグネチャ、クラスを参照するすべての属性((すべてのタイプの注釈を含む))およびクラスを参照するすべての命令に適用されます。

プリミティブ型および配列は、マッピングの対象ではなく、マッピングのターゲットとして許可されません。

参照型の配列は、常に分解され、ベース参照型としてマップされ、配列に構成されます。

単一クラスの再マッピングの例:

var classRemapper = ClassRemapper.of(
        Map.of(CD_Foo, CD_Bar));
var cc = ClassFile.of();
for (var classModel : allMyClasses) {
    byte[] newBytes = classRemapper.remapClass(cc, classModel);

}

特定のパッケージのすべてのクラスの再マッピング:

var classRemapper = ClassRemapper.of(cd ->
        ClassDesc.ofDescriptor(cd.descriptorString().replace("Lcom/oldpackage/", "Lcom/newpackage/")));
var cc = ClassFile.of();
for (var classModel : allMyClasses) {
    byte[] newBytes = classRemapper.remapClass(cc, classModel);

}

CodeLocalsShifterPREVIEW

CodeLocalsShifterPREVIEWは、コード・インジェクション中の競合を回避するために、ローカルを新しく割り当てられた位置に移動するCodeTransformPREVIEWです。 受信側またはメソッド引数スロットを指すローカルは、決してシフトされません。 メソッド引数以外を指すすべてのローカル・オブジェクトは、出現順に再索引付けされます。

すべてのメソッドのすべてのロケールをシフトするコード変換のサンプル:

byte[] newBytes = ClassFile.of().transform(
        classModel,
        (classBuilder, classElement) -> {
            if (classElement instanceof MethodModel method)
                classBuilder.transformMethod(method,
                        MethodTransform.transformingCode(
                                CodeLocalsShifter.of(method.flags(), method.methodTypeSymbol())));
            else
                classBuilder.accept(classElement);
        });

CodeRelabelerPREVIEW

CodeRelabelerPREVIEWは、変換されたコード内のすべてのLabelPREVIEWを新しいインスタンスで置き換えるCodeTransformPREVIEWです。 すべてのLabelTargetPREVIEW命令が適宜調整されます。 ラベルが変更されたコード・グラフは元のものと同じです。

CodeRelabelerPREVIEWの主な目的は、同じコード・ブロックを繰り返しインジェクションすることです。 同じコード・ブロックの繰返しインジェクションにラベルを付け直す必要があるため、LabelPREVIEWの各インスタンスはターゲット・バイトコードに1回のみバインドされます。

すべてのメソッドのラベルを変更するサンプル変換:

byte[] newBytes = ClassFile.of().transform(
        classModel,
        ClassTransform.transformingMethodBodies(
                CodeTransform.ofStateful(CodeRelabeler::of)));

クラス計測のサンプル

次のスニペットは、完全に機能するクラス・インストゥルメント変換へのClassRemapperPREVIEWCodeLocalsShifterPREVIEWおよびCodeRelabelerPREVIEWのサンプル構成です:
byte[] classInstrumentation(ClassModel target, ClassModel instrumentor, Predicate<MethodModel> instrumentedMethodsFilter) {
    var instrumentorCodeMap = instrumentor.methods().stream()
                                          .filter(instrumentedMethodsFilter)
                                          .collect(Collectors.toMap(mm -> mm.methodName().stringValue() + mm.methodType().stringValue(), mm -> mm.code().orElseThrow()));
    var targetFieldNames = target.fields().stream().map(f -> f.fieldName().stringValue()).collect(Collectors.toSet());
    var targetMethods = target.methods().stream().map(m -> m.methodName().stringValue() + m.methodType().stringValue()).collect(Collectors.toSet());
    var instrumentorClassRemapper = ClassRemapper.of(Map.of(instrumentor.thisClass().asSymbol(), target.thisClass().asSymbol()));
    return ClassFile.of().transform(target,
            ClassTransform.transformingMethods(
                    instrumentedMethodsFilter,
                    (mb, me) -> {
                        if (me instanceof CodeModel targetCodeModel) {
                            var mm = targetCodeModel.parent().get();
                            //instrumented methods code is taken from instrumentor
                            mb.transformCode(instrumentorCodeMap.get(mm.methodName().stringValue() + mm.methodType().stringValue()),
                                    //all references to the instrumentor class are remapped to target class
                                    instrumentorClassRemapper.asCodeTransform()
                                    .andThen((codeBuilder, instrumentorCodeElement) -> {
                                        //all invocations of target methods from instrumentor are inlined
                                        if (instrumentorCodeElement instanceof InvokeInstruction inv
                                            && target.thisClass().asInternalName().equals(inv.owner().asInternalName())
                                            && mm.methodName().stringValue().equals(inv.name().stringValue())
                                            && mm.methodType().stringValue().equals(inv.type().stringValue())) {

                                            //store stacked method parameters into locals
                                            var storeStack = new ArrayDeque<StoreInstruction>();
                                            int slot = 0;
                                            if (!mm.flags().has(AccessFlag.STATIC))
                                                storeStack.push(StoreInstruction.of(TypeKind.ReferenceType, slot++));
                                            for (var pt : mm.methodTypeSymbol().parameterList()) {
                                                var tk = TypeKind.from(pt);
                                                storeStack.push(StoreInstruction.of(tk, slot));
                                                slot += tk.slotSize();
                                            }
                                            storeStack.forEach(codeBuilder::with);

                                            //inlined target locals must be shifted based on the actual instrumentor locals
                                            codeBuilder.block(inlinedBlockBuilder -> inlinedBlockBuilder
                                                    .transform(targetCodeModel, CodeLocalsShifter.of(mm.flags(), mm.methodTypeSymbol())
                                                    .andThen(CodeRelabeler.of())
                                                    .andThen((innerBuilder, shiftedTargetCode) -> {
                                                        //returns must be replaced with jump to the end of the inlined method
                                                        if (shiftedTargetCode instanceof ReturnInstruction)
                                                            innerBuilder.goto_(inlinedBlockBuilder.breakLabel());
                                                        else
                                                            innerBuilder.with(shiftedTargetCode);
                                                    })));
                                        } else
                                            codeBuilder.with(instrumentorCodeElement);
                                    }));
                        } else
                            mb.with(me);
                    })
            .andThen(ClassTransform.endHandler(clb ->
                //remaining instrumentor fields and methods are injected at the end
                clb.transform(instrumentor,
                        ClassTransform.dropping(cle ->
                                !(cle instanceof FieldModel fm
                                        && !targetFieldNames.contains(fm.fieldName().stringValue()))
                                && !(cle instanceof MethodModel mm
                                        && !ConstantDescs.INIT_NAME.equals(mm.methodName().stringValue())
                                        && !targetMethods.contains(mm.methodName().stringValue() + mm.methodType().stringValue())))
                        //and instrumentor class references remapped to target class
                        .andThen(instrumentorClassRemapper)))));
}
導入されたバージョン:
22