以前から 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() メソッド。 動的コンパイルの手順はこんな感じ:
- コンパイルに渡すソースコードやオプションを作成
- コンパイラ・オブジェクト取得 (ToolProvider.getSystemJavaCompiler())
- コンパイル実行 (JavaCompiler#getTask())
- コンパイル・エラーのチェック
コード見た方が分かりやすいかな?:
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
めでたしめでたし。
- 作者: ジョゼフ・オニール,武藤健志,トップスタジオ
- 出版社/メーカー: 翔泳社
- 発売日: 2008/05/29
- メディア: 大型本
- 購入: 1人 クリック: 55回
- この商品を含むブログ (18件) を見る
*1:スクリプト言語だと「あって当然」の機能かもしれませんけどね。
*2:三重否定(否定姫の必殺技)。
*3:「Generating Java classes dynamically through Java compiler API」には、javax.tools.JavaFileManager インターフェースの使い方も載ってます。