\( \def\vector#1{\boldsymbol{#1}} \)

Service Provider Interface

Takami Torao Java 11
  • このエントリーをはてなブックマークに追加

概要

SPI (service provider interface) は特定のインターフェースに一致する実装を Java のエコシステムが検出して暗黙的にロードするための機能。Java 6 において正式に導入された。アプリケーションは SPI を利用することで実行時に拡張可能な設計を行うことができる。

アプリケーションは SPI に対応した JAR をクラスパスに追加するだけで特定の API に対する新しい実装が利用可能になる。例えば JDBC は Java のコアライブラリで API を提供している。各データベースベンダーは JDBC 準拠の実装を SPI を用いた JAR として配布する。ユーザサイドではその JAR をクラスパスに追加することでアプリケーションから利用することができる。Java 6 より前は DB へ接続する前に:

Class.forName("org.postgresql.Driver")

のようにアプリケーション側で明示的に JDBC ドライバをロードする必要があった。このような方法は Currency, TimeZone, Locale, DateFormat, NumberFormat でも使用している。

サービスプロバイダは自身の JAR ファイル内のプロバイダ設定ファイル META-INF/services/service_api に SPI 実装クラスの完全修飾名を記述する。

実装例

フレームワーク実装者が API (service) を提供し、サードパーティベンダー (service provider) がその実装を提供するケースについて考える。アプリケーションは SPI を使って実行環境で利用可能な API 実装を検索する。

  1. フレームワーク実装者は MyAPI というインターフェースを定義すると同時に MyImplA, MyImplB の 2 つの実装を提供するとしよう。

    package framework;
    public interface MyAPI {
      String name();
    }
    
    package framework;
    public class MyImplA implements MyAPI {
      public String name(){
        return "ImplA";
      }
    }
    
    package framework;
    public class MyImplB implements MyAPI {
      public String name(){
        return "ImplB";
      }
    }
    

    Service Loader が framework.MyAPI の実装を列挙できるように META-INF/services/framework.MyAPI に実装クラス名を記述する。クラス名の列挙には改行または空白区切りが使用できる。

    framework.MyImplA
    framework.MyImplB

    フレームワークはこれらを JAR にまとめて配布する。

    framework/ $ javac -d classes src/framework/*.java
    framework/ $ jar cvf framework.jar -C classes/ .
    added manifest
    adding: framework/(in = 0) (out= 0)(stored 0%)
    adding: framework/MyAPI.class(in = 139) (out= 120)(deflated 13%)
    adding: framework/MyImplA.class(in = 303) (out= 226)(deflated 25%)
    adding: framework/MyImplB.class(in = 303) (out= 226)(deflated 25%)
    ignoring entry META-INF/
    adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
    adding: META-INF/services/framework.MyAPI(in = 36) (out= 25)(deflated 30%)
  2. アプリケーションは SPI を使ってフレームワークの提供した MyAPI に対する実装を参照することができる。

    package app;
    import framework.MyAPI;
    import java.util.Iterator;
    import java.util.ServiceLoader;
    public class Main {
      public static void main(String[] args) {
        ServiceLoader<MyAPI> loader = ServiceLoader.load(MyAPI.class);
        Iterator<MyAPI> it = loader.iterator();
        while (it.hasNext()) {
          MyAPI api = it.next();
          System.out.printf("%s (%s)%n", api.name(), api.getClass().getName());
        }
      }
    }
    
    app/ $ javac -d classes -cp framework.jar src/app/Main.java
    app/ $ java -cp classes:framework.jar app.Main
    ImplA (framework.MyImplA)
    ImplB (framework.MyImplB)
  3. サードパーティ実装者は framework.MyAPI 実装クラスを作成し、自分が配布しようとしている thirdparty.jarMETA-INF/services/framework.MyAPI実装クラス名を記述する。

    package thirdparty;
    import framework.MyAPI;
    public class MyImplC implements MyAPI {
      public String name(){
        return "ImplC";
      }
    }
    
    thirdparty.MyImplC
    thirdparty/ $ javac -d classes -cp framework.jar src/thirdparty/*.java
    thirdparty/ $ jar cvf thirdparty.jar -C classes .
    added manifest
    ignoring entry META-INF/
    adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
    adding: META-INF/services/framework.MyAPI(in = 18) (out= 20)(deflated -11%)
    adding: thirdparty/(in = 0) (out= 0)(stored 0%)
    adding: thirdparty/MyImplC.class(in = 304) (out= 234)(deflated 23%)

    この thirdparty.jar をクラスパスに追加して 2. で使用したアプリケーションを動かしてみるとサードパーティ製の実装が参照できるようになっていることがわかる。

    app/ $ java -cp classes:framework.jar:thirdparty.jar app.Main
    ImplA (framework.MyImplA)
    ImplB (framework.MyImplB)
    ImplC (thirdparty.MyImplC)

この例で注目したいのは、アプリケーションでサードパーティの実装を利用可能にするためにアプリケーションやフレームワークのコードを修正する必要がない点だ。SPI を使用することによってクラスパスにライブラリを追加するだけでアプリケーションは実行環境で利用可能な実装を検索することができる。