🗂

【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