倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

javax.tools パッケージを使って Java コードをダイナミックにコンパイル!

以前から javax.tools パッケージって気になってたんですが、それを使ったサンプルを見つけたのでちょっと試してみることに。

javax.tools パッケージのクラスを使うと、Java クラスをダイナミックに生成することができるそうです。 ここでいう“ダイナミックに”とは「内容が Java コードの java.lang.String オブジェクトから Java クラスファイルを生成できる」という意味です*1

ただ、API を使っていると、どことなく「$JAVA_HOME/bin/javac.exe」をラップした API って感じが漂ってくるので(実際にそういう実装なのかは知りませんが)、何か何処かぎこちない気もしないでもない(こともない*2) まぁ、ともかくサンプルを動かしてみましょう。 ちなみに JDK 6 必須です。

参考 URL

準備


まず準備として、java.lang.String オブジェクトをコンパイル対象の Java ソースコードとして扱ってくれるようにするためのクラスを用意する必要があります。 これには javax.tools.FileObject インターフェースを実装したクラスを作成します。 「Generating Java classes dynamically through Java compiler API」に載っているコードを拝借することにして、以下のような DynamicJavaSourceCodeObject クラスを作ります:

package org.sample.hello;

import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;

class DynamicJavaSourceCodeObject extends SimpleJavaFileObject {

    private final String code;

    protected DynamicJavaSourceCodeObject(String name, String code) {
        super(URI.create("string:///" + name.replaceAll(".", "/") + Kind.SOURCE.extension), Kind.SOURCE);
        this.code = code ;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors)throws IOException {
        return code;
    }
}

アクセッサメソッドは特に必要ないようなので省略。 また、Super コンストラクタの第1引数の URI は、たぶんコンパイルに失敗した場合などに場所を特定する URI なので適当に。

ダイナミックにコンパイル!


では、ダイナミックに(動的に)コンパイルするコードを見て・・・行きたいところですが、その前にサンプルのためのインターフェースと生成するクラスの説明を少々。

Hello インターフェース

このインターフェースは、完全にサンプルのためのものです:

package org.sample.hello;

public interface Hello {
    void say();
}

動的に生成する DynamicCompilationHello クラス

で、動的に生成するクラスは上記の Hello インターフェースを実装した、以下のようなクラスとします:

package org.sample.hello;
class DynamicCompilationHello implements Hello{
    @Override
    public void say(){
        System.out.println("Hello, dynamic compilation world! : "+getClass().getName());
    }
}
  • public クラスを作ろうとすると「public クラスはクラス名と同じ名前のファイルに定義しろ!」ってメッセージが出てコンパイル・エラーになるので要注意。 案外使い方を制限されそうな気もする。

すぐ後に見る、実行を開始する Main クラスでは、このクラスに関する以下のようなフィールドを定義しています:

Main クラス内での名前
出力フォルダ "build/classes/main" (Gradle 風) DEST_DIR
パッケージ名 "org.sample.hello" PACKAGE_NAME
クラス名 "DynamicCompilationHello" CLASS_NAME
完全修飾名 "org.sample.hello.DynamicCompilationHello" QUALIFIED_CLASS_NAME
ソースコード 上記参照 SOURCE

定義部分をコードで書くとこんな感じになってます:

package org.sample.hello;
...
public class Main {

    static final String DEST_DIR = "build/classes/main";
    static final String PACKAGE_NAME = Main.class.getPackage().getName();
    static final String CLASS_NAME = "DynamicCompilationHello";
    static final String QUALIFIED_CLASS_NAME = PACKAGE_NAME +"."+ CLASS_NAME;

    static final String SOURCE =
        "package "+ PACKAGE_NAME +";" +
        "class "+ CLASS_NAME +" implements Hello{" +
            "@Override public void say(){" +
                "System.out.println(\"Hello, dynamic compilation world! : \"+getClass().getName());" +
            "}" +
        "}";
    ...
}

これを踏まえて Main クラスを見ていきましょう。

Main クラス

実行を開始する Main クラスです。 動的なコンパイルを行っているのは dynamicalCompile() メソッド。 動的コンパイルの手順はこんな感じ:

  1. コンパイルに渡すソースコードやオプションを作成
  2. コンパイラ・オブジェクト取得 (ToolProvider.getSystemJavaCompiler())
  3. コンパイル実行 (JavaCompiler#getTask())
  4. コンパイル・エラーのチェック

コード見た方が分かりやすいかな?:

package org.sample.hello;

import javax.tools.*;
import java.io.IOException;
import java.util.List;
import java.util.Locale;

import static java.util.Arrays.asList;

public class Main {

    static final String DEST_DIR = "build/classes/main";
    static final String PACKAGE_NAME = Main.class.getPackage().getName();
    static final String CLASS_NAME = "DynamicCompilationHello";
    static final String QUALIFIED_CLASS_NAME = PACKAGE_NAME +"."+ CLASS_NAME;

    static final String SOURCE =
        "package "+ PACKAGE_NAME +";" +
        "class "+ CLASS_NAME +" implements Hello{" +
            "@Override public void say(){" +
                "System.out.println(\"Hello, dynamic compilation world! : \"+getClass().getName());" +
            "}" +
        "}";

    private static void dynamicalCompile(){
        // (1) コンパイルに渡すソースコードやオプションを作成
        // コンパイル後にエラーが無いか調べる(ステップ(4)参照)
        DiagnosticCollector<JavaFileObject> diags = new DiagnosticCollector<JavaFileObject>();
        // コンパイル・オプション
        List<String> options = asList("-d", DEST_DIR);
        // Java のソースコード
        List<? extends JavaFileObject> src = asList(
            new DynamicJavaSourceCodeObject(QUALIFIED_CLASS_NAME, SOURCE)
        );

        // (2) コンパイラ・オブジェクト取得
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        // (3) コンパイル実行
        JavaCompiler.CompilationTask compilerTask = compiler.getTask(null, null, diags, options, null, src);

        // (4) コンパイル・エラーのチェック
        if (!compilerTask.call()){
            for (Diagnostic diag : diags.getDiagnostics()){
                System.out.format("Error on line %d in %s", diag.getLineNumber(), diag);
            }
        }
    }

    public static void main(String... args)throws Exception{
        dynamicalCompile();
        Hello hello = (Hello)Class.forName(QUALIFIED_CLASS_NAME).newInstance();
        hello.say();
    }
  • コンパイルを実行する JavaCompiler#getTask() メソッドにはいろいろ引数が渡せるようですが、ここではなるべく最小限にしてます*3
  • コンパイル・オプションや Java ソースコードを作成する箇所にある asList() は java.util.Arrays#asList() メソッドです(念のため)。
  • コンパイル・オプション "-d" によってクラスファイルの出力フォルダを設定してますが、これを指定しないとベースフォルダ(プログラムの実行フォルダ)にクラスファイルが出力されます(javac コマンドがそうであるように)。
  • 出力されたクラスファイルがクラスパス上にないと、もちろんコード内からそのクラスを参照することはできません。

実行すると以下のように表示されます:

Hello, dynamic compilation world! : org.sample.hello.DynamicCompilationHello

めでたしめでたし。

独習Java 第4版

独習Java 第4版

*1:スクリプト言語だと「あって当然」の機能かもしれませんけどね。

*2:三重否定(否定姫の必殺技)。

*3:Generating Java classes dynamically through Java compiler API」には、javax.tools.JavaFileManager インターフェースの使い方も載ってます。