Service Provider Interface
概要
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 実装を検索する。
フレームワーク実装者は
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%)
アプリケーションは 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)
サードパーティ実装者は
framework.MyAPI
実装クラスを作成し、自分が配布しようとしているthirdparty.jar
のMETA-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 を使用することによってクラスパスにライブラリを追加するだけでアプリケーションは実行環境で利用可能な実装を検索することができる。