ENGINEER BLOG

ENGINEER BLOG

Java で実行時にソースコードをコンパイルする

こんにちは。インテグレーションサービス本部の横山です。

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

クラス構成


0.class(2)

  1. StandardRuntimeCompilerEngine …… 今回の機能の中核となるクラスです。javax.tools.JavaCompilerを使用して、ソースコードをコンパイルします。
  2. StringJavaFileObject …… コンパイラに渡すソースコードを格納するクラスです。
  3. CompileErrorListener …… コンパイラからコンパイルエラーを受け取るクラスです。コンパイルエラーが発生したとき、内容をコンソールに出力します。
  4. RuntimeClassFileManager …… コンパイラに渡すファイルマネージャ。コンパイル後のクラスを取得します。
  5. 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 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 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;
  }

}

動的にコンパイルしたクラスのメンバーには、当然のことですが他のクラスからアクセスすることができません。適切に定義されたインターフェースを通じてアクセスする必要があります。