【Java学習|実務向け】Javaモジュールシステム実践:`uses`句でサービスを柔軟に利用する方法

Java 9で導入されたモジュールシステム(Project Jigsaw)は、アプリケーションの構造化と依存関係の管理を劇的に改善しました。中でも `uses` 句は、サービスプロバイダーパターンをより強力にし、疎結合で拡張性の高いアプリケーションを構築するための鍵となります。本記事では、`uses` 句の基本的な使い方から、実践的な応用例、そして注意点までを、現役Javaエンジニアの視点から解説します。

1. なぜ`uses`句が重要なのか? ~課題と解決策~

従来のJavaアプリケーションでは、特定のインターフェースを実装したクラスを利用する際、その実装クラスを明示的にクラスパスに含める必要がありました。これは、アプリケーションが大きくなるにつれて依存関係が複雑化し、管理が困難になるという課題を生んでいました。

`uses` 句は、この課題を解決します。モジュールが特定のサービスインターフェースを利用することを宣言することで、そのインターフェースの実装を提供するモジュール(サービスプロバイダー)を、実行時に動的にロードできるようになります。これにより、アプリケーションのコア部分と、それを拡張するサービス部分を明確に分離し、依存関係を大幅に簡略化できます。

2. `uses`句とサービスプロバイダーパターンの基礎知識

`uses` 句を理解するには、まずJavaのモジュールシステムとサービスプロバイダーパターンの基本を把握する必要があります。

2.1. Javaモジュールシステム(Project Jigsaw)

Java 9から導入されたモジュールシステムは、`module-info.java` ファイルを使って、モジュールの名前、他のモジュールへの依存関係 (`requires`)、外部に公開するパッケージ (`exports`)、そして利用するサービス (`uses`) などを定義します。これにより、Javaアプリケーションはより独立した、再利用可能なコンポーネントとして構成できるようになりました。

2.2. サービスプロバイダーパターン

サービスプロバイダーパターンは、インターフェース(サービスインターフェース)とその実装(サービスプロバイダー)を分離するデザインパターンです。

  • サービスインターフェース: クライアントコードが利用するAPIを定義します。
  • サービスプロバイダー: サービスインターフェースを実装した具体的なクラスです。
  • サービスローダー (`java.util.ServiceLoader`): 実行時に、指定されたサービスインターフェースの実装クラスをクラスパス(モジュールシステムではモジュールパス)から探し出し、インスタンスを生成します。

`uses` 句は、このサービスローダーの働きをモジュールシステム上でより明確に宣言するための仕組みです。

3. `uses`句の実装と利用方法

`uses` 句は、サービスを利用する側のモジュールの `module-info.java` ファイルに記述します。

3.1. サービスを利用するモジュール(コンシューマーモジュール)

サービスを利用するモジュールでは、`module-info.java` で `uses` 句を使って、利用したいサービスインターフェースを宣言します。

// my.consumer.module/module-info.java
module my.consumer.module {
// my.provider.module が提供するサービスインターフェースを利用することを宣言
uses com.example.spi.MyService;

// 他のモジュールへの依存関係があれば記述
requires my.api.module;
}

この宣言により、`my.consumer.module` は `com.example.spi.MyService` の実装を `ServiceLoader` を使って利用できるようになります。

3.2. サービスを提供するモジュール(プロバイダーモジュール)

サービスを提供するモジュールでは、`module-info.java` で `exports` 句を使って、サービスインターフェースが定義されているパッケージを公開し、さらに `provides` 句を使って、どの実装クラスがどのサービスインターフェースを提供するかを宣言します。

// my.provider.module/module-info.java
module my.provider.module {
// サービスインターフェースが定義されているパッケージを公開
exports com.example.spi;

// com.example.impl.MyServiceImpl が com.example.spi.MyService を提供することを宣言
provides com.example.spi.MyService with com.example.impl.MyServiceImpl;
}

また、サービスインターフェース自体は別のモジュール(APIモジュール)で定義されている場合が多いです。

// my.api.module/module-info.java
module my.api.module {
// サービスインターフェースを定義したパッケージを公開
exports com.example.spi;
}

// my.api.module/com/example/spi/MyService.java
package com.example.spi;

public interface MyService {
String doSomething();
}

// my.provider.module/com/example/impl/MyServiceImpl.java
package com.example.impl;

import com.example.spi.MyService;

public class MyServiceImpl implements MyService {
@Override
public String doSomething() {
return “Hello from MyServiceImpl!”;
}
}

3.3. サービスを利用するコード

サービスを利用する側のコードでは、`ServiceLoader` を使ってサービスの実装を取得します。

// my.consumer.module/com/example/consumer/MainApp.java
package com.example.consumer;

import com.example.spi.MyService; // APIモジュールからインポート
import java.util.ServiceLoader;
import java.util.Optional;

public class MainApp {
public static void main(String[] args) {
// ServiceLoader を使って MyService の実装を探す
ServiceLoader serviceLoader = ServiceLoader.load(MyService.class);

// 見つかったサービス実装を利用する
Optional service = serviceLoader.findFirst();

if (service.isPresent()) {
System.out.println(“Found service: ” + service.get().doSomething());
} else {
System.out.println(“No implementation found for MyService.”);
}
}
}

4. サンプルプログラム:モジュール構成と実行

ここでは、上記の概念を具体的に示すための簡単なサンプルプログラムを提示します。

4.1. モジュール構成

以下の3つのモジュールで構成します。

  • `my.api.module`: サービスインターフェース (`MyService`) を定義。
  • `my.provider.module`: `MyService` の実装 (`MyServiceImpl`) を提供。
  • `my.consumer.module`: `MyService` を利用するクライアントコード (`MainApp`) を含む。

4.2. ファイル構造

modules/
├── my.api.module/
│ ├── module-info.java
│ └── com/example/spi/MyService.java
├── my.provider.module/
│ ├── module-info.java
│ └── com/example/impl/MyServiceImpl.java
└── my.consumer.module/
├── module-info.java
└── com/example/consumer/MainApp.java

4.3. コード例

`my.api.module`

// modules/my.api.module/module-info.java
module my.api.module {
exports com.example.spi; // サービスインターフェースを外部に公開
}

// modules/my.api.module/com/example/spi/MyService.java
package com.example.spi;

public interface MyService {
String sayHello(); // サービスインターフェースの定義
}

`my.provider.module`

// modules/my.provider.module/module-info.java
module my.provider.module {
requires my.api.module; // サービスインターフェースが定義されているモジュールに依存
exports com.example.impl; // 実装クラス(ここでは不要だが例として)

// MyService インターフェースの提供者として MyServiceImpl を登録
provides com.example.spi.MyService with com.example.impl.MyServiceImpl;
}

// modules/my.provider.module/com/example/impl/MyServiceImpl.java
package com.example.impl;

import com.example.spi.MyService;

public class MyServiceImpl implements MyService {
@Override
public String sayHello() {
return “Hello from MyServiceImpl!”; // 実装
}
}

`my.consumer.module`

// modules/my.consumer.module/module-info.java
module my.consumer.module {
requires my.api.module; // MyService インターフェースを利用するために必要
uses com.example.spi.MyService; // MyService の実装を利用することを宣言
}

// modules/my.consumer.module/com/example/consumer/MainApp.java
package com.example.consumer;

import com.example.spi.MyService; // APIモジュールからサービスインターフェースをインポート
import java.util.ServiceLoader;
import java.util.Optional;

public class MainApp {
public static void main(String[] args) {
System.out.println(“Looking for MyService implementations…”);

// ServiceLoader は module-info.java で宣言された uses を考慮して、
// 利用可能なサービスプロバイダーをロードする
ServiceLoader serviceLoader = ServiceLoader.load(MyService.class);

Optional service = serviceLoader.findFirst();

if (service.isPresent()) {
MyService myService = service.get();
System.out.println(“Service found: ” + myService.sayHello());
} else {
System.out.println(“No implementation found for MyService.”);
}
}
}

4.4. コンパイルと実行

各モジュールをコンパイルし、実行します。

1. コンパイル:
各モジュールディレクトリで以下のコマンドを実行します。

# my.api.module
javac -d ../../out/my.api.module module-info.java com/example/spi/MyService.java

# my.provider.module
javac -d ../../out/my.provider.module module-info.java com/example/impl/MyServiceImpl.java

# my.consumer.module
javac -d ../../out/my.consumer.module module-info.java com/example/consumer/MainApp.java

2. 実行:
モジュールパスを指定して `MainApp` を実行します。

java –module-path out –module my.consumer.module/com.example.consumer.MainApp

実行結果:

Looking for MyService implementations…
Service found: Hello from MyServiceImpl!

`my.consumer.module` は `my.provider.module` に直接依存していませんが、`uses` 句と `ServiceLoader` の仕組みにより、`MyService` の実装をロードして利用できています。

5. 応用・注意点

`uses` 句は非常に強力ですが、いくつか注意すべき点があります。

5.1. `exports` と `opens` の関係

  • `exports` は、コンパイル時にリフレクションを使用しない場合に、パッケージ内のクラスへのアクセスを許可します。
  • `opens` は、リフレクション(`setAccessible(true)` など)を使用してパッケージ内のクラスにアクセスすることを許可します。
  • `ServiceLoader` はデフォルトでは `exports` されたパッケージ内のクラスをロードしますが、リフレクションが絡む場合は `opens` も考慮する必要が出てくることがあります。通常、サービスプロバイダーのロード自体は `exports` で十分です。

5.2. 複数実装の扱い

`ServiceLoader` は、指定されたサービスインターフェースに対するすべての実装をロードできます。`findFirst()` ではなく `iterator()` を使ってループ処理することで、複数の実装をすべて利用することも可能です。

// ServiceLoader のイテレータを利用する例
ServiceLoader serviceLoader = ServiceLoader.load(MyService.class);
for (MyService service : serviceLoader) {
System.out.println(“Processing service: ” + service.sayHello());
}

5.3. 依存関係の明確化

`uses` 句は、モジュールが「どの種類のサービスを必要としているか」を示すものであり、「どの具体的なモジュールからそのサービスを取得するか」を直接指定するものではありません。サービスプロバイダーは、モジュールパス上の任意のモジュールからロードされる可能性があります。そのため、依存関係を管理する上では、`requires` 句と `uses` 句を適切に組み合わせることが重要です。

例えば、`my.consumer.module` が `my.provider.module` にも直接依存している場合(例えば、プロバイダー固有の設定を行いたい場合など)、`requires my.provider.module;` も `module-info.java` に追加する必要があります。

5.4. `META-INF/services` ファイルとの互換性

`ServiceLoader` は、モジュールシステム以前の `META-INF/services` ディレクトリにあるサービス定義ファイルも認識します。モジュールシステムとレガシーなクラスパスが混在する環境では、この互換性に注意が必要です。

まとめ

`uses` 句は、Javaモジュールシステムにおけるサービスプロバイダーパターンの利用を宣言する強力な機能です。これにより、アプリケーションのモジュール間の依存関係を疎結合に保ち、拡張性と保守性を高めることができます。本記事で紹介したサンプルコードや注意点を参考に、ぜひ実際の開発で `uses` 句を活用してみてください。

コメント

タイトルとURLをコピーしました