
こんにちは。インテグレーションサービス本部の横山です。
Java はコンパイル言語なので、アプリケーションの実行中にソースコードをコンパイルすることは、通常ありません。が、 javax.tools パッケージを使うと
実行時にコンパイルを実現することができます。
今回、この機能を使って、 Java のソースコードをコンパイルしてクラスを生成する仕組みを作成しました。
クラス構成

- StandardRuntimeCompilerEngine …… 今回の機能の中核となるクラスです。javax.tools.JavaCompilerを使用して、ソースコードをコンパイルします。
- StringJavaFileObject …… コンパイラに渡すソースコードを格納するクラスです。
- CompileErrorListener …… コンパイラからコンパイルエラーを受け取るクラスです。コンパイルエラーが発生したとき、内容をコンソールに出力します。
- RuntimeClassFileManager …… コンパイラに渡すファイルマネージャ。コンパイル後のクラスを取得します。
- RuntimeClassObject …… コンパイル済みのクラスを格納するためのjavax.tools.JavaFileObjectの実装です。
コンパイルの流れ
中心となるのは、完全修飾クラス名とソースコードを渡すと、コンパイルを実行してクラスを返すメソッドです。
このメソッドが返したクラスをインスタンス化して、アプリケーションの中で使用することができます。
処理の流れは、コード中のコメントを参考にしてください。
public Class compile(String className, String source) {
Class ret = null;
// ファイルオブジェクトを生成する。
JavaFileObject fileObject =
new StringJavaFileObject(className, source);
// コンパイラを取得する。
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// コンパイルエラー用リスナを取得する。
DiagnosticListener listener = new CompileErrorListener();
// 標準ファイルマネージャーを取得する。
JavaFileManager fileManager =
new RuntimeClassFileManager(compiler, new CompileErrorListener());
try {
// コンパイルするソースを取得する。
List fileObjects = new ArrayList();
fileObjects.add(fileObject);
// コンパイルオプションを指定する。
List options = new ArrayList();
options.add("-verbose");
// 参照するクラスのjarファイルを、コンパイルオプションに指定する。
StringBuilder buf = new StringBuilder();
for (String s : this.getClassLoactions(source)) { …… ★1
if (buf.length() > 0) {
buf.append(PATH_SEPARATOR);
}
buf.append(s);
}
if (buf.length() > 0) {
options.add("-classpath");
options.add(buf.toString());
}
// コンパイルタスクを取得する。
CompilationTask task =
compiler.getTask(null, fileManager, listener, options, null,
fileObjects);
// コンパイルを実行する。
if (!task.call()) {
throw new AplInternalException("compile failed.");
}
// コンパイルしたクラスを取得する。
try {
ret = (Class)fileManager.getClassLoader(null).loadClass(className);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
finally {
// ファイルマージャーを閉じる。
try {
fileManager.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
return ret;
}
★1のメソッド「getClassLoactions」では、システムプロパティ「java.class.path」に指定されたクラスパスに加えて、ソースコードからimportで指定されたクラスを抽出しています。従って、ワイルドカード「*」を使用したimportがあると、正常に動作しません。
StringJavaFileObject クラス
コンパイルする Java ソースコードを保持するクラスです。
package compiler;
import java.io.IOException;
import java.net.URI;
import javax.tools.SimpleJavaFileObject;
public class StringJavaFileObject extends SimpleJavaFileObject {
/** コンパイルするソースコード */
private String content;
public StringJavaFileObject(String className, String source) {
super(URI.create("string:///" + className.replace('.', '/') +
Kind.SOURCE.extension),
Kind.SOURCE);
this.content = source;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return this.content;
}
}
CompileErrorListener クラス
コンパイルエラーが発生したとき、メッセージをコンソールに出力します。
package compiler;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
public class CompileErrorListener implements DiagnosticListener {
@Override
public void report(Diagnostic extends JavaFileObject> diagnostic) {
System.out.println("<< " + diagnostic.getKind() + " >> " +
diagnostic.getMessage(null));
}
}
RuntimeClassFileManager クラス
一度コンパイルしたクラスは Map にキャッシュします。
package compiler;
import java.io.IOException;
import java.security.SecureClassLoader;
import java.util.HashMap;
import java.util.Map;
import javax.tools.DiagnosticListener;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
public class RuntimeClassFileManager
extends ForwardingJavaFileManager {
/** クラス名とコンパイル済みクラスのマップ */
private Map objects =
new HashMap();
/** クラスローダ */
private ClassLoader classLoader = null;
/** インスタンスを生成する。 */
public RuntimeClassFileManager(JavaCompiler compiler,
DiagnosticListener super JavaFileObject> listener) {
super(compiler.getStandardFileManager(listener, null, null));
}
/** 出力用ファイルオブジェクトを取得する。 */
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, Kind kind,
FileObject sibling)
throws IOException {
RuntimeClassObject ret = new RuntimeClassObject(className, kind);
this.objects.put(className, ret);
return ret;
}
/** クラスローダーを取得する。 */
@Override
public ClassLoader getClassLoader(Location location) {
if (this.classLoader == null) {
this.classLoader = new InnerClassLoader(this.getClass().getClassLoader());
}
return this.classLoader;
}
/** クラスローダ(内部クラス) */
public class InnerClassLoader extends SecureClassLoader {
/** インスタンスを生成する。 */
public InnerClassLoader(ClassLoader parent) {
super(parent);
}
/** クラスを検索する。 */
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class> ret = null;
RuntimeClassObject obj = RuntimeClassFileManager.this.objects.get(name);
if (obj == null) {
ret = this.getClass().getClassLoader().loadClass(name);
}
else {
ret = obj.getDefinedClass();
if (ret == null) {
byte[] arr = obj.getBytes();
ret = super.defineClass(name, arr, 0, arr.length);
obj.setDefinedClass(ret);
}
}
return ret;
}
}
}
RuntimeClassObject クラス
コンパイル済みのクラスを保持します。
package compiler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import javax.tools.SimpleJavaFileObject;
/** コンパイルしたクラスを格納するクラス。 */
public class RuntimeClassObject extends SimpleJavaFileObject {
/** コンパイルしたクラスのバイト配列 */
private final ByteArrayOutputStream value = new ByteArrayOutputStream();
/** コンパイルしたクラス */
private Class> definedClass = null;
public RuntimeClassObject(String className, Kind kind) {
super(URI.create("string:///" + className.replace('.', '/') +
kind.extension), kind);
}
@Override
public OutputStream openOutputStream() throws IOException {
return this.value;
}
public byte[] getBytes() {
return this.value.toByteArray();
}
public Class> getDefinedClass() {
return this.definedClass;
}
public void setDefinedClass(Class> definedClass) {
this.definedClass = definedClass;
}
}
動的にコンパイルしたクラスのメンバーには、当然のことですが他のクラスからアクセスすることができません。適切に定義されたインターフェースを通じてアクセスする必要があります。