JigsawでSPIを使用する

Java9では,ようやくJigsawが導入され,モジュール化ができるようになります.
一方,SPI(SurviceProviderInterface)と呼ばれる仕組みがあり,広く利用されています.
今回は,JigsawでSPIを扱うための方法を紹介します.

Jigsawの基礎知識はある程度あるものと想定します.
もしも分からない部分があれば,櫻庭さんの記事*1 *2 *3 *4 *5 *6が分かりやすくまとまっているので,そちらをご参照ください.

SPIとは

SPIはSurviceProviderIntefaceの略で,あるプログラムやライブラリに対して,第三者が実装を提供するため仕組みです.
SPIはJDBCやPluggable Annotation Processing APIなど,多くの場面で利用されています.
ライブラリはSPIとなるインターフェースを提供し,java.util.ServiceLoaderを用いて,第三者の実装を読み込みます.
三者はライブラリが提供するインターフェースを実装し,所定の方法でパッケージングし,クラスパスに指定します.
今までは,実装を提供する第三者は.jarファイルのMETA-INF/services内にインターフェースのFQCNが名前のファイルを作成し,その中に一行づつ具象クラスのFQCNを記述します.

例を用いて説明しましょう.
例はIndex of /~shinyafox/jigsaw/example/ModuledSPIExample/においてあるので,そちらも参考にしてください.

SPI提供者

まず,以下がSPIとなるインターフェース,HelloInterfaceです.
挨拶文を返すメソッドhelloが宣言されます.

package com.example.hello.spi;

public interface HelloInterface {
    String hello();
}

このSPIを利用するクラスが以下です.
ServiceLoader#loadを用いてクラスパス内からHelloInterfaceを実装するクラスを探します.
HelloInterfaceを実装する全てのクラスに対し,helloメソッドを呼び出して,その結果を出力しています.

package com.example.hello;

import java.util.Iterator;
import java.util.ServiceLoader;

import com.example.hello.spi.HelloInterface;

public class Main {
    public static void main(String... args) {
        Iterator<HelloInterface> i = ServiceLoader.load(HelloInterface.class).iterator();

        while (i.hasNext()) {
            HelloInterface hi = i.next();
            String hello = hi.hello();
            System.out.println(hello);
        }
    }
}

これらは以下のようなファイル構造でhello.jarにパッケージングされています.

hello.jar
└com
 └example
  └hello
   ├spi
   │└HelloInteface.class
   └Main.class
SPI実装者

さて,SPIを実装するクラスは以下のようになります.

package jp.example.hello;

import com.example.hello.spi.HelloInterface;

public class JapaneseHello implements HelloInterface {
    public String hello() {
        return "こんにちは";
    }
}

以下のファイル構造で,JapaneseHelloクラスをJARファイルとしてhello_jp.jarにパッケージングします.

hello_jp.jar
├jp
│└example
│ └hello
│  └JapaneseHello.class
└META-INF
 └services
  └com.example.hello.spi.HelloInterface

/META-INF/services/com.example.hello.spi.HelloInterface内に,このインターフェースを実装するクラスのFQCNを記述することで,ServiceLoaderで読み込むクラスの対象にできます.
JapaneseHelloを読み込ませるため,/META-INF/services/com.example.hello.spi.HelloInterfaceは以下の内容になります.

jp.example.hello.JapaneseHello

複数のクラスをServiceLoaderで読み込む対象とする場合は,改行して複数のクラスを列挙します.

実行

hello_jp.jarをクラスパスに与えて,hello.jarを実行すると,以下の出力が得られます.

$ java -cp "hello_jp.jar:hello.jar" com.example.hello.Main
こんにちは

ここまでが従来のSPIの取り扱い方です.

JigsawでSPIを利用する

ここからはJigsawでSPIを利用する方法を紹介します.

SPI提供者

SPIをモジュール化して提供するには,module-info.java内でexports節で提供するSPIを含むパッケージを指定し,公開します.
同時に,uses節を用いて提供するSPIを指定します.

先ほどのhello.jarを用いて例示します.
実装は同じで,JARファイルの構造が異なります.

hello.jar
├com
│└example
│ └hello
│  ├spi
│  │└HelloInteface.class
│  └Main.class
└module-info.class

module-info.classが加わってることに注意してください.
module-info.classはmodule-info.javaコンパイルして生成されたファイルです.
このファイルがあることで,hello.jarは名前付きモジュールとして扱われます.

module-info.javaを以下に示します.

module com.example.hello {
    exports com.example.hello;
    exports com.example.hello.spi; // SPIであるHelloInterfaceを可視にする

    uses com.example.hello.spi.HelloInterface; // HelloInterfaceがSPIであることを指示
}

ポイントはexports節でHelloInterfaceを含むcom.example.hello.spiが指定されることと,uses節でHelloInterfaceが指定されることです.

SPI実装者

SPIを実装するクラスを含むJARパッケージをモジュール化する時は,module-info.java内のprovides節でSPIを実装するクラスを指定します.
provides節の構文は「provides SPIとなるインターフェースのFQCN with 実装クラスのFQCN」です.

これも先ほどの例を用いて説明します.

実装は先程の例と同じで,hello_jp.jarのファイル構造が異なります.

hello_jp.jar
├jp
│└example
│ └hello
│  └JapaneseHello.class
└module-info.class

/META-INF/services以下が無くなり,module-info.classに変わったことに注意してください.
module-info.classの元となるmodule-info.javaを以下に示します.

module jp.example.hello {
    requires com.example.hello;
    exports jp.example.hello;

    provides com.example.hello.spi.HelloInterface with jp.example.hello.JapaneseHello;
}

provides節で,HelloInterfaceの実装クラスとしてJapaneseHelloが指定されています.

今回の例では違いを分かりやすくするために,/META-INF/servicesを削除しましたが,互換性のために,それらを残すことも可能です.
特に,URLClassLoaderを用いて実装クラスを読み込む場合に,読み込む側の実装が適切でないと,/META-INF/services以下が無い場合は実装クラスが読み込まれません.
これについては別の記事で紹介しますが,互換性のために/META-INF/services以下を残しておくことをオススメします.

実行

それでは実行しましょう.
実行はhello.jarとhello_jp.jarをモジュールとして読み込むように設定し,com.example.helloモジュールのcom.example.hello.Mainを実行するクラスとして指定します.

$ java -mp "hello_jp.jar:hello.jar" -m com.example.hello/com.example.hello.Main
こんにちは

ご覧のようにして,JigsawでSPIを扱うことができました.