皆様、こんにちは!シニアJavaエンジニアの〇〇です。(※筆者の名前を想定)
Javaアプリケーションが大規模化するにつれて、コードベースの複雑性、依存関係の管理、そして内部実装の意図しない公開といった課題に直面することは少なくありません。特に、巨大なJARファイル一つで全てを管理するようなプロジェクトでは、「依存関係地獄」や「クラスパスの衝突」といった問題が頻繁に発生しがちです。
Java 9で導入されたJava Platform Module System (JPMS)、通称 Project Jigsaw は、これらの課題を根本的に解決するために生まれました。今回は、このモジュールシステムの中核である「モジュールの宣言」に焦点を当て、その重要性と使い方を皆さんと一緒に見ていきましょう。
1. 導入: なぜモジュールが重要なのか
JPMSは、アプリケーションをより小さく、管理しやすい「モジュール」という単位に分割することを可能にします。これにより、以下のような大きなメリットが得られます。
- 強いカプセル化: モジュールは、どのパッケージを外部に公開し、どのパッケージを内部に隠蔽するかを明確に宣言できます。これにより、意図しない内部実装へのアクセスを防ぎ、システムの堅牢性を高めます。
- 明示的な依存関係: 各モジュールは、自分がどの他のモジュールに依存しているかを`module-info.java`ファイルで明示的に宣言します。これにより、アプリケーション全体の依存関係が明確になり、ビルド時や実行時のエラーを早期に発見できます。
- アプリケーションの軽量化: 必要なモジュールだけを組み合わせてランタイムイメージを構築できるようになるため、不要なクラスを含まない、よりコンパクトなアプリケーションを作成できます。これは特にマイクロサービスや組み込みシステムにおいて大きな利点となります。
- JDK自身のモジュール化: JDK自体もモジュール化されたことで、必要なJDKモジュールだけを選択してJREを構築できるようになり、フットプリントの削減とセキュリティの向上が図られました。
2. 基礎知識: モジュールとは何か?
Javaにおけるモジュールとは、関連するパッケージ、リソース、およびサービスプロバイダーの集合体です。モジュールは、その振る舞いを記述する特別なファイル `module-info.java` を持つことで識別されます。
`module-info.java`ファイルでは、主に以下の要素を宣言します。
- `module モジュール名 { … }`: モジュール自体の名前を宣言します。慣例として、逆ドメイン名形式(例: `com.example.myapp`)が使われます。
- `requires 依存モジュール名;`: このモジュールが機能するために必要とする、他のモジュールを指定します。これにより、依存関係が明示されます。
- `exports パッケージ名;`: このモジュールが外部の他のモジュールに対して公開するパッケージを指定します。`exports`されていないパッケージは、そのモジュールの内部でのみ使用可能です。これが強いカプセル化の鍵となります。
- `opens パッケージ名;`: `exports`と同様にパッケージを公開しますが、`exports`が「コンパイル時および実行時に型へのアクセスを許可する」のに対し、`opens`は「実行時にリフレクションによるアクセスを許可する」という点が異なります。リフレクションを使って非公開メンバーにアクセスする必要がある場合に利用します。
3. 実装/解決策: モジュール宣言の具体例
それでは、簡単な例でモジュールの宣言方法を見ていきましょう。
ここでは、ユーティリティ機能を提供するモジュール(`myapp.util`)と、そのユーティリティを利用するメインアプリケーションモジュール(`myapp.main`)を作成します。
シナリオ:
1. `myapp.util` モジュールは、文字列操作のユーティリティクラス(`StringUtils`)を提供します。
2. `myapp.main` モジュールは、`myapp.util` モジュールに依存し、`StringUtils` を利用します。
この場合、`myapp.util` は `StringUtils` が含まれるパッケージを `exports` する必要があり、`myapp.main` は `myapp.util` を `requires` する必要があります。
4. サンプルプログラム
以下のディレクトリ構造を想定します。
project_root/
├── myapp.main/
│ ├── src/
│ │ └── myapp.main/
│ │ ├── module-info.java
│ │ └── com/example/main/
│ │ └── MainApp.java
├── myapp.util/
│ ├── src/
│ │ └── myapp.util/
│ │ ├── module-info.java
│ │ └── com/example/util/
│ │ └── StringUtils.java
ファイル内容:
`myapp.util/src/myapp.util/module-info.java`
// myapp.utilモジュールの定義
module myapp.util {
// com.example.utilパッケージを外部に公開します。
// これにより、他のモジュールがこのパッケージ内の公開クラスを利用できるようになります。
exports com.example.util;
}
`myapp.util/src/myapp.util/com/example/util/StringUtils.java`
package com.example.util;
/
- 文字列操作ユーティリティクラス
/
public class StringUtils {
/
- 指定された文字列が空(nullまたは空白文字のみ)かどうかを判定します。
- @param str 判定する文字列
- @return 文字列が空であればtrue、そうでなければfalse
/
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
/
- 文字列を逆順にします。
- @param str 逆順にする文字列
- @return 逆順になった文字列
/
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}
}
`myapp.main/src/myapp.main/module-info.java`
// myapp.mainモジュールの定義
module myapp.main {
// myapp.utilモジュールに依存することを宣言します。
// これにより、myapp.mainはこのモジュールで公開されているパッケージを利用できます。
requires myapp.util;
}
`myapp.main/src/myapp.main/com/example/main/MainApp.java`
package com.example.main;
// myapp.utilモジュールで公開されているStringUtilsクラスをインポート
import com.example.util.StringUtils;
/
- メインアプリケーションクラス
/
public class MainApp {
public static void main(String[] args) {
String text1 = “Hello Module!”;
String text2 = “”;
String text3 = null;
// myapp.utilモジュールのStringUtilsクラスを利用
System.out.println(“元の文字列: ” + text1);
System.out.println(“逆順にした文字列: ” + StringUtils.reverse(text1));
System.out.println(“isEmpty(\”” + text1 + “\”): ” + StringUtils.isEmpty(text1));
System.out.println(“\n元の文字列: \”” + text2 + “\””);
System.out.println(“isEmpty(\”” + text2 + “\”): ” + StringUtils.isEmpty(text2));
System.out.println(“\n元の文字列: ” + text3);
System.out.println(“isEmpty(” + text3 + “): ” + StringUtils.isEmpty(text3));
}
}
コンパイルと実行方法:
1. まず、モジュールパス(`mlib`)とコンパイル済みモジュールクラスの出力先(`mods`)を作成します。
mkdir -p mlib mods
2. `myapp.util` モジュールをコンパイルします。
javac -d mods –module-source-path project_root myapp.util/src/myapp.util/module-info.java myapp.util/src/myapp.util/com/example/util/StringUtils.java
- `-d mods`: コンパイル結果を`mods`ディレクトリに出力します。
- `–module-source-path project_root`: ソースモジュールがあるルートディレクトリを指定します。
3. `myapp.main` モジュールをコンパイルします。
`myapp.main` は `myapp.util` に依存しているため、`myapp.util` が含まれるモジュールパスを指定する必要があります。
javac -d mods –module-path mods –module-source-path project_root myapp.main/src/myapp.main/module-info.java myapp.main/src/myapp.main/com/example/main/MainApp.java
- `–module-path mods`: 依存するモジュール(`myapp.util`)が`mods`ディレクトリにあることを指定します。
4. アプリケーションを実行します。
java –module-path mods -m myapp.main/com.example.main.MainApp
- `-m myapp.main/com.example.main.MainApp`: 実行するモジュール名と、そのモジュール内のメインクラスを指定します。
出力例:
元の文字列: Hello Module!
逆順にした文字列: !eludoM olleH
isEmpty(“Hello Module!”): false
元の文字列: “”
isEmpty(“”): true
元の文字列: null
isEmpty(null): true
5. 応用・注意点
- 推移的依存 (Transitive Requires):
ライブラリモジュールを開発する際、そのライブラリがさらに別のモジュールに依存している場合、`requires transitive 他のモジュール名;` と宣言できます。これにより、あなたのモジュールを`requires`するモジュールは、自動的に「他のモジュール」にも依存することになり、個別に`requires`を書く手間が省けます。
- サービスプロバイダー (Provides/Uses):
プラグインのような拡張可能なアーキテクチャを構築する際に非常に強力です。
`provides インターフェース名 with 実装クラス名;` でサービスを提供し、`uses インターフェース名;` でサービスを利用することを宣言します。`ServiceLoader` クラスを使って、実行時に動的にサービス実装を発見・ロードできます。
- 自動モジュール (Automatic Modules):
Java 9より前のレガシーなJARファイルをモジュールパスに置くと、それらは自動的に「自動モジュール」として扱われます。JARファイル名からモジュール名が推測され、全てのパッケージが`exports`されます。これは、既存のライブラリをJPMSに段階的に移行させるための仕組みです。
- 名前のないモジュール (Unnamed Module):
モジュールパスにもクラスパスにも明確に属さないクラスは、「名前のないモジュール」に属します。これは、Java 9以前のクラスパスベースの挙動を維持するための特殊なモジュールで、全てのパッケージが他のモジュールから見えます。
- クラスパスとの共存:
JPMSはモジュールパスとクラスパスの両方をサポートしており、両者を組み合わせて使用できます。ただし、モジュールパス上のモジュールはクラスパス上のリソースに依存できません。モジュールとクラスパスの間には「One-Way Barrier(一方通行の壁)」が存在します。
- リフレクションの制限と`–add-opens`:
モジュールシステムは強いカプセル化を徹底するため、`exports`されていないパッケージに対しては、たとえリフレクションを使っても外部からアクセスできません。既存のフレームワークやライブラリが内部実装にリフレクションでアクセスしている場合、`module-info.java`に `opens パッケージ名;` を追加するか、JVM起動オプションで `–add-opens モジュール名/パッケージ名=ターゲットモジュール名` を指定する必要があります。
Project Jigsawは、Javaアプリケーションの設計と構築方法に大きな変革をもたらしました。最初は少し学習コストがあるかもしれませんが、そのメリットは計り知れません。ぜひ、皆さんのプロジェクトでもモジュール宣言を活用し、より堅牢で保守しやすいJavaアプリケーションを構築してみてください。
それでは、また次回の豆知識でお会いしましょう!

コメント