S-JIS[2024-03-23] 変更履歴

Class-File API(Java22)

Java22のClass-File API(プレビュー版)について。


概要

2024/3/19にリリースされたJava22で、プレビュー版としてClass-File APIが追加された。

classファイルをパース(解析)したり加工したバイト列を作り出したりすることが出来る。


Class-File APIはJava22ではプレビュー版の機能なので、この機能を使いたい場合はコンパイル時にjavacコマンドに--enable-previewを付ける必要があり、
また、実行時にjavaコマンドに--enable-previewを付ける必要がある。
JShellで試す場合もjshellコマンドに--enable-previewを付ける。

> javac --enable-preview --release 22 Example.java
> java --enable-preview Example
> java --enable-preview --source 22 Example.java

Class-File APIで読み込む例

Class-File APIを使ってclassファイルを読み込む例。

import java.io.IOException;
import java.lang.classfile.ClassElement;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;
import java.nio.file.Path;

public class ClassFileExample {

    public static void main(String[] args) throws IOException {
        var path = Path.of("/tmp/example/java22/Example.class");
        ClassModel cm = ClassFile.of().parse(path);
//      ClassModel cm = ClassFile.of().parse(Files.readAllBytes(path));

        System.out.println("cm=" + cm);
        System.out.println("cm.name=" + cm.thisClass().name().stringValue());
        System.out.println("cm.majorVersion=" + cm.majorVersion());
        System.out.println("cm.minorVersion=" + cm.minorVersion());

        for (ClassElement ce : cm) {
            switch (ce) {
            case FieldModel fm:
                System.out.printf("Field %s%n", fm.fieldName().stringValue());
                break;
            case MethodModel mm:
                System.out.printf("Method %s%n", mm.methodName().stringValue());
                break;
            default:
                break;
            }
        }
    }
}

ClassFileを用いて、classファイルのバイト列からClassModelを作る。
そこからClassElement(フィールドやメソッドの共通インターフェース)を取得できる。

ClassModel#thisClass().name()FieldModel#fieldName()MethodModel#methodName()はUtf8Entryを返すので、そこからStringで取得するにはstringValue()を呼び出す。


ClassModelからフィールド一覧やメソッド一覧を取得することも出来る。

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

検索する例

printStackTraceメソッドの呼び出しとSystem.out・errを使っているクラスを検索する例。

import java.io.IOException;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.CompoundElement;
import java.lang.classfile.MethodModel;
import java.lang.classfile.instruction.FieldInstruction;
import java.lang.classfile.instruction.InvokeInstruction;
import java.lang.classfile.instruction.LineNumber;
import java.nio.file.Files;
import java.nio.file.Path;
public class ClassFileSearchExample {

    public static void main(String[] args) throws IOException {
        var dir = Path.of("/tmp/example/java22/");
        System.out.println(dir);

        try (var stream = Files.walk(dir)) {
            stream.filter(p -> p.toString().endsWith(".class")).forEach(p -> search(p));
        }
    }
    static void search(Path path) {
        System.out.println(path);

        ClassModel classModel;
        try {
            classModel = ClassFile.of().parse(path);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        search(classModel, null);
    }
    static void search(CompoundElement<?> compoundElement, String methodName) {
        if (compoundElement instanceof MethodModel mm) {
            methodName = mm.methodName().stringValue();
        }

        int lineNumber = -1;
        for (var element : compoundElement) {
//          System.out.println("element=" + element);

            if (element instanceof LineNumber ln) {
                lineNumber = ln.line();
                continue;
            }
            if (element instanceof InvokeInstruction i) {
                if (i.name().stringValue().equals("printStackTrace")) {
                    System.out.printf("%d (%s) %s%n", lineNumber, methodName, i);
                }
                continue;
            }
            if (element instanceof FieldInstruction i) {
                String name = i.name().stringValue();
                if (name.equals("out") || name.equals("err")) {
                    if (i.owner().name().stringValue().equals("java/lang/System")) {
                        System.out.printf("%d (%s) %s%n", lineNumber, methodName, i);
                    }
                }
                continue;
            }
            if (element instanceof CompoundElement ce) {
                search(ce, methodName);
            }
        }
    }
}

ClassModelやMethodModel・FieldModelはCompoundElementを継承しており、要素(ClassFileElement)の一覧を持っている。
(Iterableで要素を順番に取得できる他、elementList()やelementStream()といった一覧取得メソッドを持っている)

要素は、ClassModelだと以下のような物が並んでいる。

MethodModelの要素は以下のような感じ。

CodeModelの中に命令を表すオブジェクトが並んでいるのは不思議ではないが、ソースコードの行番号もオブジェクトとして独立して入っているようだ。


それと、printStackTrace呼び出しのownerが例外クラスかどうかを判別したいところだが、
ownerのクラス名は取れるものの、それがThrowableを継承しているかどうかの判定は出来なさそう。


Class-File APIで加工する例

classファイルを読み込んでClass-File APIを使って加工する例。

以下のようなソースをコンパイルしたExample.classを読み込み、debugメソッド呼び出しを削除してみる。

public class Example {

    public static void main(String... args) {
        System.out.println("example");
        debug("zzz"); // これを削除してみる
    }

    private static void debug(String message) {
        System.out.println("debug: " + message);
    }
}

import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.ClassTransform;
import java.lang.classfile.CodeTransform;
import java.lang.classfile.MethodTransform;
import java.lang.classfile.Opcode;
import java.lang.classfile.instruction.InvokeInstruction;
import java.nio.file.Files;
import java.nio.file.Path;
public class ClassFileTransformExample {

    public static void main(String[] args) throws Exception {
        var path = Path.of("/tmp/example/java22/Example.class");
        System.out.println(path);

        byte[] bytes = Files.readAllBytes(path);
        loadAndExecuteExample(bytes); // 普通にクラスをロードして実行すると、debugメッセージが出力される

        // 加工元となるClassModel
        ClassModel cm = ClassFile.of().parse(bytes);

        CodeTransform removeDebugInvoke = (builder, ce) -> {
            if (ce instanceof InvokeInstruction i) {
                if (i.name().stringValue().equals("debug") //
                        && i.owner().name().stringValue().equals("Example") //
                        && i.opcode() == Opcode.INVOKESTATIC) {
                    System.out.println("remove: " + i);
                    return; // builderにwithしないことによって、メソッド呼び出しを削除する
                }
            }
            builder.with(ce);
        };

//      MethodTransform mt = MethodTransform.transformingCode(removeDebugInvoke);
//      ClassTransform ct = ClassTransform.transformingMethods(mt);
        ClassTransform ct = ClassTransform.transformingMethodBodies(removeDebugInvoke);

        byte[] newBytes = ClassFile.of().transform(cm, ct);
        loadAndExecuteExample(newBytes); // 実行するとdebugメッセージは出力されない
    }

まず、CodeTransformを用意し、CodeElementを加工する処理を記述する。(上記のremoveDebugInvoke

そして、ClassTransformを生成する。
メソッド本体の加工用はClassTransform.transformingMethodBodies()で生成できる。
(内部でMethodTransform.transformingCode()ClassTransform.transformingMethods()の呼び出しを行っている)

最後に、ClassFile#transform()に「加工元のClassModel」とClassTransformを渡すと、加工されたバイト列が生成される。


生成されたバイト列をクラスとして読み込んで実行するには、Class-File APIではなく、従来通りClassLoaderを使用する。

    static void loadAndExecuteExample(byte[] bytes) throws Exception {
        var loader = new MyClassLoader(ClassFileTransformExample.class.getClassLoader());
        loader.addClass(bytes);

        var clazz = loader.loadClass("Example");
        System.out.println(clazz);

        var mainMethod = clazz.getMethod("main", String[].class);
        System.out.println(mainMethod);

        System.out.println("----invoke start");
        mainMethod.invoke(null, new Object[] { new String[0] });
        System.out.println("----invoke end");
    }
}

public class MyClassLoader extends ClassLoader {

    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }

    public void addClass(byte[] bytes) {
        super.defineClass(null, bytes, 0, bytes.length);
    }
}

ClassLoaderのdefineClassメソッドはprotectedなので、自分でサブクラスを作らないと呼び出せない。


プレビュー機能へ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま