多分、日本で1,000,000番目に Java のモジュールシステムに詳しい人間になれた気がする
— 引きこもり持田 (@mike_neck) 2021年2月18日
GraalVM の native-image で、 javac のネイティブイメージを作成した際に、 JavacTool
(JavaCompiler
の実装クラス) というコアとなるクラスがネイティブ化されていませんでした。いろいろと振り返ってみたところ、 Java のモジュールシステムについてまったくといっていいほど理解が足りていないという結論にいたり、勉強がてら少しずつ実験してみることにしました。
時間がない方のための 1 行まとめ
- ServiceLoader の javadoc 読んでください
ServiceLoader
の仕組みと Java モジュール
JavacTool
というのは javax.tools.ToolProvider
というクラスから ServiceLoader
を介して取得できるクラスです。 ServiceLoader
は META-INF/services
の下にある取得したいインターフェースと同じ名前(Fully Qualified Name)のファイル(provider-configuration ファイルと呼ばれているようです)に書かれた実装クラスのインスタンスを生成するという仕組みです。しかし、 JavaCompiler
に関してはどこを探しても ServiceLoader
が読み込むはずの META-INF/services/javax.tools.JavaCompiler
というファイルが見当たりません。(以下、古い Java (graalvm-20.0.0 java-11)を使っているのは気にしないでください)
ここで重要な役割を果たすのが、 module-info.java
です。このクラスはご存知の方もいると思いますが、 Java9 で登場した、パブリッククラスの無秩序な利用を制御するための仕組みです。もともとは Java7 で導入される予定だったものの、登場したのが遅かったこともあり使っている人は殆どいないのではないかと思います。
jdk.compiler の module-info.java
を見てみますと、次のように書かれています(一部省略しています)。
module jdk.compiler { requires transitive java.compiler; provides java.util.spi.ToolProvider with com.sun.tools.javac.main.JavacToolProvider; provides com.sun.tools.javac.platform.PlatformProvider with com.sun.tools.javac.platform.JDKPlatformProvider; provides javax.tools.JavaCompiler with com.sun.tools.javac.api.JavacTool; provides javax.tools.Tool with com.sun.tools.javac.api.JavacTool; }
module-info.java
に使われている個々のキーワードについては以下のオラクルから発行されている Java Magazine を参照ください。
ここで重要なのは、 provides <インターフェース名> with <実装クラス名>
の部分で、インターフェースに対する実装をしていることです。そして、この指定によって provider-configuration ファイルが存在しなくても、 ServiceLoader
によりインスタンスを取得できるようになっています(このことは javadoc に書いてあるので、ちゃんと読んでいなかった私が悪いのですが…)。ここでの記述内容からは、 javax.tools.Tool
インターフェースは com.sun.tools.javac.api.JavacTool
が、 javax.tools.JavaCompiler
インターフェースも同じく com.sun.tools.javac.api.JavacTool
が提供しているということがわかります。
実験
とはいえ、机上の空論をするよりも実際に動かしてみることが大事なので、 jshell で確認してみましょう。
次のようなコードを入力してみます。
import javax.tools.JavaCompiler import java.util.ServiceLoader var serviceLoader = ServiceLoader.load(JavaCompiler.class) serviceLoader.forEach(compiler -> { System.out.println(compiler.getClass()); })
Java11(GraalVM21.0.0)での結果は以下のようになります。
しかし、これでは provider-configuration によって取得できたのかもしれないという疑念があるので、クラスローダーから provider-configuration ファイルの有無を確認してみます。
val loader = ClassLoader.getSystemClassLoader() loader.getResource("META-INF/services/javax.tools.JavaCompiler")
見事、 null
が返ってきましたね。つまり、 provider-configuration ファイルがなくても、インターフェースとその実装クラスを分離する手法が Java のモジュールによって提供されているということがわかったと思います。
さて、結論っぽいことまで書いておいて言いにくいのですが、私が調べたいと思っていることは Java モジュールで ServiceLoader
のあれがあれできるよという話ではありません。 GraalVM の native-image で module-info
が無視されることの対応方法を考えていくことです。
ということでここから本論に入っていきたいのですが、いかんせん文章が長くなってしまったので、ここで一旦終了して、私が満足する調べ物は次回以降に書きたいと思います。
最後までご視聴いただきありがとうございました。チャンネル登録といいねボタンをクリックしてくださると、私のやる気が出ます(ユーチューバー風)