mike-neckのブログ

Java or Groovy or Swift or Golang

Jigsaw でサービスプロバイダーを定義した場合には、 ServiceLoader は従来の引数なしコンストラクターに加えて provider という名前の static ファクトリーメソッドでオブジェクトを生成できる

GraalVM の native-image はモジュールには未対応ということで、モジュールによるアプリケーションのネイティブ化には一旦諦めましたが、モジュールの仕組みがまだよくわかってないので、もう少し勉強を続けることにした日本で 1,000,000 番目に Java のモジュールシステムに詳しいものです。

今回は Java9 で追加されていた ServiceLoader のファクトリーメソッドによるオブジェクト生成について紹介します。


時間のない人のための 1 行まとめ

  • Java9 のモジュールシステムによって、 ServiceLoader は生成するオブジェクトを従来の引数なしコンストラクターだけでなく、 provide ... with ... ディレクティブに指定されたクラスの providers という static ファクトリーメソッドによって生成できるようになっていた

で、いつもどおりですが、詳しくは ServiceLoaderjavadoc を読むのがおすすめです。


従来の 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

これについては前回の記事にまとめてあります

mike-neck.hatenadiary.com


モジュールによって追加された 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 staticprovider という引数なしのメソッドで 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 で組むのは面倒かと思いますので、今回もリポジトリーを作ってあります。

github.com

モジュールのものを動かす場合は runModuleApp を起動してください。

同じ内容になるように provider configuration を構成した jar プロジェクトも作成してありますが、こちらは provider() メソッドには対応していないので、実行時にエラーになります。