GraalVM の native-image はモジュールには未対応ということで、モジュールによるアプリケーションのネイティブ化には一旦諦めましたが、モジュールの仕組みがまだよくわかってないので、もう少し勉強を続けることにした日本で 1,000,000 番目に Java のモジュールシステムに詳しいものです。
今回は Java9 で追加されていた ServiceLoader のファクトリーメソッドによるオブジェクト生成について紹介します。
時間のない人のための 1 行まとめ
- Java9 のモジュールシステムによって、
ServiceLoaderは生成するオブジェクトを従来の引数なしコンストラクターだけでなく、provide ... with ...ディレクティブに指定されたクラスのprovidersという static ファクトリーメソッドによって生成できるようになっていた
で、いつもどおりですが、詳しくは ServiceLoader の javadoc を読むのがおすすめです。
従来の ServiceLoader
ServiceLoader は provider configuration と呼ばれるファイルを作成して、中身に引数なしのコンストラクターを持つ実装クラスを指定しておくと、 ServiceLoader が指定されたクラスを生成してくれる仕組みです。
例えば、インターフェース com.example.api.Message に対して com.example.impl.Hello という実装クラスを作成して、 META-INF/services/com.example.api.Message というファイルの中に com.example.impl.Hello と記入しておくと、 ServiceLoader.load(Message.class) で ServiceLoader<Message> を取り出して拡張 for 文でぶん回すと、 Hello のインスタンスを利用できます。
com.example.api.Message.java
package com.example.api; public interface Message { String message(); }
com.example.impl.Hello.java
package com.example.impl; public class Hello implements Message { @Override public String message() { return "hello"; } }
META-INF/services/com.example.api.Message
com.example.impl.Hello
main メソッドのあるクラス
import com.example.api.Message; class Main { public static void main(String[] args) { for (Message message: ServiceLoader.load(Message.class)) { System.out.println(message.message()); // 多分 hello と表示される } } }
コメントにも書いてあるとおり、このプログラムを実行すると hello と表示されるはずです
モジュールによる ServiceLoader
これについては前回の記事にまとめてあります
モジュールによって追加された ServiceLoader の static ファクトリーメソッドによるインスタンス生成方法
static なファクトリーメソッド provider という提供したいインターフェースを返すメソッドを持つクラスを provides ... with ... ディレクティブの with ... に指定すると、 ServiceLoader から provider メソッドを呼び出して生成されたインスタンスを取得できます。
先程の例のインターフェース com.example.api.Message に対して次のようなクラスを作ります
com.example.factory.Factory.java
package com.example.factory; import com.example.api.Message; public class Factory { // (1) public static Message provider() { //(2) return () -> "factory"; } }
(1) Factory クラスは Message クラスとは次のメソッド以外には何の関係もありません
(2) public static な provider という引数なしのメソッドで Message のインスタンスを作成・返します
factory の方の module-info.java
import com.example.factory.Factory; module lib.factory { requires lib.api; // (1) provides com.example.api.Message with Factory; // (2) }
(1) Message インターフェースを参照する
(2) Message の実装クラスと同じように Factory クラスを with に指定する
このようにして作成されたモジュールを含むように調整して、先程の Main クラスを実行すると factory と出力されます。
疑問
ここで、一つ疑問を持ちます。インターフェース自体が、自分自身のデフォルトなインスタンスを返す static メソッド provider を持った上で、 provides ... with ... で自分自身を指定した場合、そのデフォルト実装が返されるのか?
実験してみましょう。
先程の Message インターフェースに static で名前が provider という引数なし・ Message のインスタンスを返すメソッドを追加します。そして、同じモジュールで provides ... with ... ディレクティブに自分自身を指定します。
com.example.api.Message.java
package com.example.api; public interface Message { String message(); static Message provider() { // (1) return () -> "message"; } }
(1) 名前が provider な引数なし、 Message を返す static メソッドを追加する
module-info.java
module lib.api {
exports com.example.api;
provides Message
with Message; // (1)
}
(1) 自分自身の提供するクラスを指定するのに自分自身を用います
このようにして作成されたモジュールを含むように調整して、先程の Main クラスを実行すると factory とともに message が出力されます(なお、出力される順番が何によって定まるのかはわかりません)。
レポジトリー
ここまでの内容のプロジェクトを、 gradle で組むのは面倒かと思いますので、今回もリポジトリーを作ってあります。
モジュールのものを動かす場合は runModuleApp を起動してください。
同じ内容になるように provider configuration を構成した jar プロジェクトも作成してありますが、こちらは provider() メソッドには対応していないので、実行時にエラーになります。