mike-neckのブログ

Java or Groovy or Swift or Golang

Java のモジュールと ServiceLoader

GraalVM の native-image で、 javac のネイティブイメージを作成した際に、 JavacTool(JavaCompiler の実装クラス) というコアとなるクラスがネイティブ化されていませんでした。いろいろと振り返ってみたところ、 Java のモジュールシステムについてまったくといっていいほど理解が足りていないという結論にいたり、勉強がてら少しずつ実験してみることにしました。


時間がない方のための 1 行まとめ

  • ServiceLoader の javadoc 読んでください

ServiceLoader の仕組みと Java モジュール

JavacTool というのは javax.tools.ToolProvider というクラスから ServiceLoader を介して取得できるクラスです。 ServiceLoaderMETA-INF/services の下にある取得したいインターフェースと同じ名前(Fully Qualified Name)のファイル(provider-configuration ファイルと呼ばれているようです)に書かれた実装クラスのインスタンスを生成するという仕組みです。しかし、 JavaCompiler に関してはどこを探しても ServiceLoader が読み込むはずの META-INF/services/javax.tools.JavaCompiler というファイルが見当たりません。(以下、古い Java (graalvm-20.0.0 java-11)を使っているのは気にしないでください)

f:id:mike_neck:20210219071711p:plain
java.compiler モジュールの中身には META-INF/services ディレクトリーがないことがわかる

f:id:mike_neck:20210219073544p:plain
jdk.compiler の中には META-INF/services はあるものの、 JavaCompiler のものではない

ここで重要な役割を果たすのが、 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)での結果は以下のようになります。

f:id:mike_neck:20210219085848p:plain

しかし、これでは provider-configuration によって取得できたのかもしれないという疑念があるので、クラスローダーから provider-configuration ファイルの有無を確認してみます。

val loader = ClassLoader.getSystemClassLoader()
loader.getResource("META-INF/services/javax.tools.JavaCompiler")

f:id:mike_neck:20210219090935p:plain

見事、 null が返ってきましたね。つまり、 provider-configuration ファイルがなくても、インターフェースとその実装クラスを分離する手法が Java のモジュールによって提供されているということがわかったと思います。


さて、結論っぽいことまで書いておいて言いにくいのですが、私が調べたいと思っていることは Java モジュールで ServiceLoader のあれがあれできるよという話ではありません。 GraalVM の native-image で module-info が無視されることの対応方法を考えていくことです。

ということでここから本論に入っていきたいのですが、いかんせん文章が長くなってしまったので、ここで一旦終了して、私が満足する調べ物は次回以降に書きたいと思います。

最後までご視聴いただきありがとうございました。チャンネル登録といいねボタンをクリックしてくださると、私のやる気が出ます(ユーチューバー風)