今月はJavaのプログラムの中から,Javaファイルをコンパイルするために使用されるCompiler APIを取りあげます。

実をいうと,Compiler APIを使わずとも,プログラムの中でJavaコンパイラを使用することができます。これはJava SE 6以前のJavaでも可能です。

例えば,AntやMavenは,コンパイラタスクを実行してJavaファイルをコンパイルすることができます。また,JSPははじめてアクセスされた時にサーブレットに変換され,コンパイルした後に実行されます。これらのアプリケーションはプログラムの中でJavaコンパイラを使用しているのです。

javacコマンドの実体はJavaで記述されており,そのメインクラスはcom.sun.tools.javac.Mainクラスです。このMainクラスのmainメソッドをコールすれば,Javaファイルをコンパイルすることができます。

例えば,Hello.javaをコンパイルするには,次のように記述します。

import com.sun.tools.javac.Main;
 
public class CompileSample {
    public CompileSample() {
        try {
            String[] args 
                = new String[] {"-verbose", "Hello.java"};
            Main.main(args);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 
    public static void main(String[] args) {
        new CompileSample();
    }
}

Mainクラスのmainメソッドの引数はjavacコマンドの引数に対応します。したがって,mainメソッドの引数にコンパイルオプションやコンパイルするソースファイルを指定します。

コンパイルするソースファイルは前述したようにHello.javaです。ここでは,コンパイルの状況がわかりやすいように,-verboseオプションを付加してコンパイルするようにしてみました。

では,このサンプルをコンパイルして,実行してみましょう。

com.sun.tools.javac.Mainクラスは標準のライブラリが格納されているrt.jarファイルではなく,JDKをインストールしたディレクトリの直下のlibディレクトリにあるtools.jarに含まれています。そのため,コンパイルや実行する時には,tools.jarをクラスパスに含めるようにします。

ここでは,JDKはC:\Program Files\Java\jdk1.6.0_10にインストールされているとします。

C:\compiler>javac -cp "c:\Program Files\Java\jdk1.6.0_10\lib\tools.jar";. CompileSample.java
    
C:\compiler>java -cp "c:\Program Files\Java\jdk1.6.0_10\lib\tools.jar";. CompileSample   
[Hello.java を構文解析開始]
[23ms で構文解析完了]
[ソースファイルの検索パス: c:\Program Files\Java\jdk1.6.0_10\lib\tools.jar,.]
[クラスファイルの検索パス: C:\Program Files\Java\jdk1.6.0_10\jre\lib\resources.jar,
C:\Program Files\Java\jdk1.6.0_10\jre\lib\rt.jar,C:\Program Files\Java\jdk1.6.0_10\
jre\lib\sunrsasign.jar,C:\Program Files\Java\jdk1.6.0_10\jre\lib\jsse.jar,C:\Program Files
\Java\jdk1.6.0_10\jre\lib\jce.jar,C:\Program Files\Java\jdk1.6.0_10\jre\lib\charsets.jar,
C:\Program Files\Java\jdk1.6.0_10\jre\classes,C:\Program Files\Java\jdk1.6.0_10\jre\lib\ext
\dnsns.jar,
C:\Program Files\Java\jdk1.6.0_10\jre\lib\ext\localedata.jar,C:\Program Files\Java\jdk1.6.0_10
\jre\lib\ext\sunjce_provider.jar,C:\Program Files\Java\jdk1.6.0_10\jre\lib\ext\sunmscapi.jar,
C:\Program Files\Java\jdk1.6.0_10\jre\lib\ext\sunpkcs11.jar,c:\Program Files\Java\jdk1.6.0_10
\lib\tools.jar,.]
[java\lang\Object.class(java\lang:Object.class) を読み込み中]
[java\lang\String.class(java\lang:String.class) を読み込み中]
[Hello を確認中]
[java\lang\System.class(java\lang:System.class) を読み込み中]
[java\io\PrintStream.class(java\io:PrintStream.class) を読み込み中]
[java\io\FilterOutputStream.class(java\io:FilterOutputStream.class) を読み込み中]
[java\io\OutputStream.class(java\io:OutputStream.class) を読み込み中]
[Hello.class を書き込み完了]
[合計 273ms]

このサンプルを実行すると,Hello.classが生成されることが確かめられます。

このようにプログラムの中で,Javaファイルをコンパイルすることができます。では,なぜCompiler APIが必要なのでしょうか。

理由はいくつかあります。

1つめの理由はjavacのメインクラスがcom.sun.tools.java.Mainクラスとは限らないということです。

com.sunで始まることから,このMainクラスはSun Microsystems製のJDKに限定されます。したがって,Eclipseに付属するJavaコンパイラのような,他のJavaコンパイラを使用したくても,使うことはできません。

これに対して,Compiler APIではコンパイラを呼び出すインタフェースを統一化し,様々なJavaコンパイラを使えるようにしています。

2つめの理由は,com.sun.tools.java.Mainクラスを直接呼び出す場合,コンパイルエラーをプログラム中で扱いにくいという点があります。

コンパイルエラーは標準エラー出力に出力されるので,文字列としてコンパイルエラーを取得することはできます。しかし,文字列ではちょっと扱いにくいですね。

そこで,Compiler APIでは,コンパイルエラーの情報をオブジェクトとして扱えるようになっています。

最後の理由はファイルです。

com.sun.tools.java.Mainクラスはコンパイルの対象はあくまでもファイルです。このため,動的にソースを生成して,コンパイルしたい場合は,生成したソースをファイルに出力し,その後にそのファイルを再び読み込んでコンパイルするという手順になります。

しかし,ファイルに出力したものを,再び読み込むのですから,効率がよくありません。できれば,オンメモリーでコンパイルも行ってしまいたいところです。

そこで,Compiler APIでは,仮想的なファイルマネージャが提供されています。このファイルマネージャを使用することで,オンメモリーでJavaのソースをコンパイルすることができるのです。

このような特徴を持つCompiler APIをさっそく使ってみましょう。