GraalVM の native-image で javac のネイティブ化に失敗したことの原因が Java のモジュールシステムの理解不足にあることからモジュールシステムを一から勉強し直している、日本で 1,000,000 番目に Java のモジュールシステムに詳しいものです。
今回は自分でモジュールを書いて ServiceLoader
で実装クラスの名前を知らなくてもインスタンスを取得するという実験をやっていきたいと思います。
例によって時間のない人のための 1 行まとめ
ServiceLoader
の javadoc 、特に Deploying providers as module を読んでください- GraalVM の
native-image
はモジュールシステムに2021年2月現在は対応していないため、諦めてください
おまけで、まとめの行数を増やしておきました。
時間がないけど、 javadoc 読みたくない人のための 5 行まとめ
- 実装クラスを提供するモジュールにて provider configuration(
META-INF/services/foo.bar.baz.Interface
)ファイルの代わりにmodule-info.java
にprovide <インターフェース名> with <実装クラス名>
を書く - インターフェースを利用するモジュールにて
module-info.java
にuses <インターフェース名>
を記述する - Gradle でサブプロジェクトによって、モジュールを構成した場合は、
java.modularity.inferModulePath
にtrue
を設定する ServiceLoader
で 1. で指定したインターフェースを取得すると指定した実装クラスが返される- 詳しくは
ServiceLoader
の javadoc に書いてある - GraalVM の
native-image
はモジュールシステムに2021年2月現在は対応していないため、諦めてください
おまけで、まとめの行数を増やしておきました。
設計
モジュールシステムの実験をするにあたり、どのような構成が必要であるかを考えます。
ServiceLoader
で読み込む対象のインターフェース(api モジュールとします)- で作ったインターフェースを実装した
ServiceLoader
に実際に読み込まれるクラス(impl モジュールとします)
- で作ったインターフェースを実装した
- 上記のインターフェースをコンパイル時に参照できて、実行時に実装クラスを取得するメインアプリケーション(app モジュールとします)
そして、これらのモジュールと Gradle のサブプロジェクトを同一のものとして扱うようにします。
3 つのプロジェクトがあるマルチプロジェクトを上記参照関係で構成すれば、今回の実験を行えそうです。
今回の実験のレポジトリーはこちらです
api プロジェクト (インターフェース)の build.gradle
はこんな感じです(主要な部分だけ書いています)
plugins {
id 'java-library'
}
impl プロジェクト(実装クラス)の build.gradle
plugins { id 'java-library' } dependencies { implementation( project(':api')) } java { modularity .inferModulePath .set(true) //(1) }
(1) Gradle でモジュールを使う場合にコンシューマー側のプロジェクトで、modularity.inferModulePath
に対して true
を設定する必要があります。これを設定することにより、 Java モジュールパスが指定されるようになります。詳しくは Gradle のドキュメントを参照ください
docs.gradle.org
app プロジェクト(コンシューマー)の build.gradle
plugins { id 'java' id 'application' } dependencies { implementation( project(':api')) //(1) runtimeOnly( project(':impl')) //(2) } java { modularity .inferModulePath .set(true) } application { mainClass .set('com.example.app.App') mainModule .set('app') //(3) }
(1) コンパイル時に api プロジェクトのインターフェースが参照できる必要があるので、 implementation
で api プロジェクトを指定します
(2) 実行時に参照できればいいので runtimeOnly
で impl プロジェクトを指定します
(3) モジュールを利用したアプリケーションの場合に、 application.mainModule
に main
メソッドのあるモジュールの名前を指定します
これで、プロジェクトを構成できたので、後は適当なインターフェースとその実装と利用するアプリケーションを書くだけです
実装
アプリケーションを書くだけですなんて簡単そうなことを言っておいてから、改めて言うのもおかしな話ですが、 module-info.java
を正しく書かないと実験に失敗します。ただし、たいていは IDE が補完してくれるので、それほど難しい作業ではありません。
api プロジェクト (インターフェース)のコード
package com.example.api; public interface Messager { String message(); }
簡単にするため api プロジェクトでは、なんらかのメッセージを返してくれるインターフェースを定義することにしました。
そして、 api プロジェクトの module-info.java
は次のような定義になります。
module api { requires java.base; //(1) exports com.example.api; //(2) }
(1) Messager
インターフェースは String
を参照するので、 java.base
モジュールを requires
ディレクティブに設定します。しかし、 java.base
の場合は省略可能です。
(2) Messager
インターフェースを内包する com.exmple.api
パッケージはほかのモジュールに参照してもらいたいので、 exports
ディレクティブに com.exmple.api
パッケージを指定します。
impl プロジェクト(実装クラス)のコード
Messager
の実装クラスには特別な記述は必要ありません。また、 provider configuration ファイル(src/main/resources/META-INF/services/com.example.api.Messager
) も作成しません。
module-info.java
の記述は以下のようになります。
module impl {
requires api;
provides //(1)
com.example.api.Messager
with
com.example.impl.HelloMessager;
}
(1) provides
ディレクティブは、 ServiceLoader
でロードできるようにする実装クラスを記述します。ここでは、 HelloMessager
クラスが Messager
インターフェースの実装クラスとして提供されていることを示します。これが Java のモジュールシステムにおける provider configuration(META-INF/services/
以下のファイル)の代わりとなります。
app プロジェクト(コンシューマー) のコード
ServiceLoader
でインターフェースを読み込むアプリケーションのコードも特別なことはしていません。実行時にログがわかりやすくなるようにアプリケーション起動後の文字色を変えています。
package com.example.app; import com.example.api.Messager; import java.util.ServiceLoader; public class App { public static void main(String[] args) { System.out.println("\u001B[33mapp start\u001B[0m"); for (Messager messager : ServiceLoader.load(Messager.class)) { System.out.println(messager.message()); } } }
アプリケーションのプロジェクトの module-info.java
は次のようになっています。
module app {
requires api;
uses // (1)
com.example.api.Messager;
}
(1) ServiceLoader
で読み込むインターフェースの名前を uses
ディレクティブに記述します。
実行
Gradle でアプリケーションを起動します。(IDE の実行ボタンでもいいです)
というわけで、 provider configuration ファイルなしでも ServiceLoader
によって実装クラスをロードできました。でも、まあ、これは想定された結果です。
一旦 Gradle でモジュールを使ったアプリケーションの起動ができたところで、今回の目標は達成ですが、もう少しだけ進めたいと思います。
native-image-agent
僕の作っている org.mikeneck.graalvm-native-image
という Gradle のプラグインは、 GraalVM の native-image
コマンドのラッパープラグインです。そして、このプラグインのオプション機能として、 generateNativeImageConfig
というタスクにより native-image
の configuration.json ファイルを生成できます。この configuration.json ファイルにリフレクションや JNI 、リソースファイルを指定すると、 native-image
は生成するバイナリーにこれらのリソースやメソッド呼び出しをコンパイルしてくれます。 generateNativeImageConfig
は単純にアプリケーションを何度も実行して、 native-image-agent
によって生成された json をマージして生成しています。では、この generateNativeImageConfig
を実行してみましょう。
app プロジェクト(コンシューマー)の build.gradle
を次のように書き換えます
plugins { id 'java' id 'application' id 'org.mikeneck.graalvm-native-image' version 'v1.2.0' //(1) } dependencies { implementation( project(':api')) runtimeOnly( project(':impl')) } java { modularity .inferModulePath .set(true) } application { mainClass .set('com.example.app.App') mainModule .set('app') } nativeImage { graalVmHome = System.getProperty('java.home') //(2) mainClass = 'com.example.app.App' //(3) executableName = 'jmodl' arguments { add '--no-fallback' add '--enable-all-security-services' add '--report-unsupported-elements-at-runtime' } } generateNativeImageConfig { enabled = true byRunningApplicationWithoutArguments() }
これで、 generateNativeImageConfig
タスクを実行すると、 build/native-image-config
ディレクトリーに、各種 config.json が出力されます。
さて、 ServiceLoader
は仕様としてパラメーターなしのコンストラクターを起動することから、内部的にはリフレクションを使って実装クラスのインスタンスを生成します。したがって、生成される config.json のうちの reflection-config.json には必ず HelloMessager
クラスの <init>
メソッドが記載されているはずです。
では実行してみましょう。
一旦、 clean
タスクによって、 build
ディレクトリーを消した状態から、 generateNativeImageConfig
を実行します。
あれ? HelloMessager
の message
が出力されていないですね…まあ、これは理由はわかっていて、モジュールシステムによって ServiceLoader
を利用したため、 provider configuration ファイルがなくて、 Messager
の実装クラスとして HelloMessager
を認識できていないためです。最初に Gradle の java
コンベンションオブジェクトの設定で modularity.inferModulePath
を設定したのは、 jar で起動しないためです 。そして、これは僕の graalvm-native-image-plugin の機能不足で jar 起動のみに対応しており、モジュール起動に対応していないためです。
そして、当然ながら、生成された config.json も空っぽです。
というわけで、 graalvm-native-image-plugin の generateNativeImageConfig
についてはイシューを立てておきました(自分のレポジトリー)。
では、 graalvm-native-image-plugin の機能ではだめということで、通常の JavaExec
にモジュールを利用する設定を行った上で、 native-image-agent
を agentlib にして実行してみることにしましょう。
app プロジェクト(コンシューマー)の build.gradle
に次の記述を追加します
task runByModuleWithAgent(type: JavaExec, group: 'application') { modularity .inferModulePath .set(true) mainModule .set('app') classpath = sourceSets.main.output + configurations.runtimeClasspath // (1) jvmArgs("-agentlib:native-image-agent=config-output-dir=$buildDir/$name")//(2) }
(1) 依存ライブラリーとjavac でコンパイル・生成したアウトプットを classpath に登録します
(2) native-image-agent を使って、リフレクションの情報などの config.json を出力させます
これで、モジュールによって ServiceLoader
の読み込みから config.json を出力させられそうです(フラグ)!では試してみましょう。
アプリケーションの実行は問題なくできており、 ServiceLoader
によって読み込まれた HelloMessager
のメッセージが出力されています。
さあ、それでは reflect-config.json
を見てみましょう!
あれ? reflect-config.json
に何も出力されていませんね…何が良くないのかわかりませんね…
よくわからないので、GraalVM の Slack で質問してみることにしました。
どうやれば、モジュールによる ServiceLoader
で config.json 出力できる?
モジュールのサポートは仕掛中だよ。ワークアラウンドがあるけど、最新のビルドが必要
というわけで、対応してないものはどうしようもないので、とりあえず完成を待つことにしましょう
なお、最初から、 slack で質問しておけば、儂は Java のことが全然わかってないなどと落ち込むこともなかったかもしれません。なお、調べている最中に ServiceLoader
に新しい機能が追加されていたようなので、次回に紹介します。