【Java Gold】SPI(Service Provider Interface)について
はじめに
JavaのSPI(Service Provider Interface)について、試験対策として整理しました。
SPIとは
SPIとは「Service Provider Interface」の略で、サービスを提供する側が実装するインターフェースです。
利用者(クライアント)はその具象クラス(実装クラス)を知らなくても、インターフェースだけを通じてサービスを利用できます。
例として、JDBCの java.sql.Driver インターフェースがあります。これは代表的なSPIであり、MySQLやPostgreSQLなどのJDBCドライバがこのインターフェースを実装しています。
SPIの構成要素
- サービスインターフェース(SPI):サービス提供者が実装すべきインターフェースや抽象クラス
- サービスプロバイダ:SPIを実装した具象クラス
- ServiceLoaderクラス:実行時にサービスプロバイダを検索・読み込む仕組み
モジュールを使わない実装方法
1. サービスインターフェースの定義
public interface GreetingService {
String greet(String name);
}
2. サービス実装クラスの作成
public class EnglishGreetingService implements GreetingService {
public String greet(String name) {
return "Hello, " + name;
}
}
このクラスは必ず public で、引数なしのデフォルトコンストラクタを持っている必要があります。
ServiceLoader によって自動的にインスタンス化されるためです。
3. サービス定義ファイルの配置
プロジェクトの META-INF/services/ ディレクトリに、
サービスインターフェースの完全修飾クラス名(FQCN)をファイル名としたファイルを作成し、
その中に実装クラスのFQCNを書きます。
例:
-
ファイルパス:
META-INF/services/com.example.GreetingService -
ファイルの内容:
com.example.EnglishGreetingService
※ 完全修飾クラス名(FQCN)とは、クラスのパッケージ名とクラス名をピリオドで繋げたものです(例:java.util.List)。
4. クライアントコード
ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
for (GreetingService service : loader) {
System.out.println(service.greet("wakame"));
}
ServiceLoader はクラスパス上の META-INF/services を探索し、対応するサービス実装を動的に読み込みます。
モジュールを使う実装方法
モジュールシステムを使うと、SPIの構成と動作をより厳密かつ明確に制御できます。
以下は3つのモジュールに分けて構成します。
| モジュール名 | 役割 |
|---|---|
greeting.api |
サービスインターフェースの定義 |
greeting.impl |
実装の提供と登録 |
app |
サービスの利用 |
モジュール1:greeting.api(インターフェース定義)
package com.example.spi;
public interface GreetingService {
String greet(String name);
}
module-info.java
module greeting.api {
exports com.example.spi;
}
-
exportsは、他のモジュールからこのパッケージの中身(クラスなど)を見えるようにするための宣言です。これを記述しないと、他のモジュールから
GreetingServiceを利用できません。
モジュール2:greeting.impl(サービスの実装と提供)
package com.example.impl;
import com.example.spi.GreetingService;
public class HelloGreetingService implements GreetingService {
public String greet(String name) {
return "Hello, " + name;
}
}
module-info.java
module greeting.impl {
requires greeting.api;
provides com.example.spi.GreetingService
with com.example.impl.HelloGreetingService;
}
-
requires greeting.api;→ このモジュールが
greeting.apiモジュールに依存していることを示します。つまり、GreetingServiceを使うにはこの宣言が必要です。 -
provides ... with ...;→
GreetingServiceに対する実装クラス(HelloGreetingService)を Javaのモジュールシステムに登録します。これにより、
ServiceLoaderがモジュールシステムを通じてこの実装を見つけられるようになります。
モジュール3:app(サービスを利用するクライアント)
package com.example.app;
import com.example.spi.GreetingService;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
for (GreetingService service : loader) {
System.out.println(service.greet("wakame"));
}
}
}
module-info.java
module app {
requires greeting.api;
uses com.example.spi.GreetingService;
}
-
requires greeting.api;→
GreetingServiceを利用するためには、その定義があるモジュールへの依存を明示する必要があります。 -
uses com.example.spi.GreetingService;→ このモジュールが
GreetingServiceを動的に使う(ServiceLoaderを使う)ことをモジュールシステムに伝えます。これがないと、モジュールシステムは他モジュールの
providesを探索してくれません。
モジュールを使う場合のメリット
| 項目 | 内容 |
|---|---|
| 依存関係の明確化 |
requires, uses, provides によって関係がコード上に表現される |
| セキュリティ |
exports で必要なAPIだけを外部に公開できる |
| 最適化 |
jlink などのツールによって不要なモジュールを省いた軽量化が可能 |
| 柔軟性 | 古い META-INF/services 方式との併用も可能 |
モジュールあり・なしの比較表
| 比較項目 | モジュールなし | モジュールあり |
|---|---|---|
| 対応バージョン | Java 8以前から | Java 9以降 |
| 構成 | クラスパスとサービス定義ファイル | モジュールと module-info.java による宣言 |
| 依存関係の記述 | 暗黙的(ファイルで記述) | 明示的(requires, provides, uses) |
| 柔軟性 | 高いが依存が見えにくい | 設計が明確、堅牢な構成に向いている |
SPIを活用する利点
- 実装の切り替えが容易(プラグインのように扱える)
- クライアント側は実装を意識せずにサービスを利用可能(疎結合)
- Javaの標準API(JDBC、JAXP、ログAPIなど)でも広く採用されている
まとめ
- SPI(Service Provider Interface)は、サービスの提供者が実装し、利用者はその実装を意識せずに使用できるインターフェースの仕組みです
- 実装と利用を分離することで、柔軟で拡張性の高いアーキテクチャを実現できます
- SPIの実装には、モジュールを使用する方法とモジュールを使用しない方法の2通りがあります
おわりに
誤記や改善点があればコメントなどでご指摘ください。
Discussion