SourceBuddyでJavaクラスを動的生成する

Javaバイトコードを操作するツールとしてはASMやJavassistがありますが、SourceBuddyはコンパイルバイトコード生成を行うツールです。
バイトコード操作を行いたい場合、既存バイトコードの変更を行う必要はなくて、実行時に新規クラスを生成できれば十分ということが多いように思います。SourceBuddyはそういう場合に使いやすいツールです。
ということで、Hello worldレベルで少し試してみました。
https://github.com/sourcebuddy/sourcebuddy

といっても、ソースコードを用意してコンパイルするだけなので、使い方はそんなに難しくありません。

まずはMavenなりGradleなりで依存を追加します。READMEには<version>2.2.0-SNAPSHOTを指定していますが、みつからなかったので2.1.0にしています。

<dependency>
    <groupId>com.javax0.sourcebuddy</groupId>
    <artifactId>SourceBuddy</artifactId>
    <version>2.1.0</version>
</dependency>

まず、これは普通のコードとして基になるインタフェースを用意します。

public interface Printer {
    void print();
}

そしてそのインタフェースをimplementsするクラスのソースコードを文字列で用意します。今回はこのコードをSourceBuddyでコンパイル、利用します。

var source = """
             package example;
             public class MyPrinter implements Main.Printer {
                @Override
                public void print() {
                    System.out.println("Hello!");
                }
             }
             """;

そしてコンパイルします。といっても、com.javax0.sourcebuddy.Compilerのstaticメソッドcompileソースコード文字列を与えるだけですね。

Class<?> clazz = Compiler.compile(source);

あとは通常のリフレクションのコードとして、コンストラクタを取得、インスタンス生成を行います。

Printer pt = (Printer) clazz.getConstructor().newInstance();

そうしてメソッドを呼び出すと、コンパイルしたコードが実行されることがわかります。

 pt.print();

ソースコード全体はこんな感じです。

package example;

import com.javax0.sourcebuddy.Compiler;
import java.lang.reflect.InvocationTargetException;

public class Main {

    public interface Printer {
        void print();
    }
    
    public static void main(String[] args) throws 
            ClassNotFoundException, // compileに必要
            Compiler.CompileException,
            NoSuchMethodException,  // getConstructorに必要
            InstantiationException, // newInstanceに必要
            IllegalAccessException, 
            IllegalArgumentException, 
            InvocationTargetException { 
        var start = System.currentTimeMillis();
        var source = """
                     package example;
                     public class MyPrinter implements Main.Printer {
                        @Override
                        public void print() {
                            System.out.println("Hello!");
                        }
                     }
                     """;
        Class<?> clazz = Compiler.compile(source);
        System.out.println(System.currentTimeMillis() - start);
        Printer pt = (Printer) clazz.getConstructor().newInstance();
        pt.print();
    }
}

SourceBuddy自体は検査例外は多く定義されてませんが、リフレクションで多くの検査例外の扱いが必要になります。 あと、compile()で結構時間がかかるので処理時間をとっています。手元の環境で450msかかっています。単発処理なのでなんどか実行すると初期化やJITの分で速くなる可能性はありますが、それでも450msというのは結構な時間がかかっています。

まあ、ソースコード生成はAPIの呼び出し方よりも生成するソースコードとか実行タイミングとかが難しいので、いろいろと試すのがいいんだと思います。