この記事から得られる知識:
- Javaモジュールシステム(Project Jigsaw)の基本概念と導入背景
module-info.java
の具体的な書き方と各ディレクティブ(requires
,exports
など)の詳細な意味java.lang.module
パッケージの主要なAPI(Module
,ModuleLayer
など)の役割と実践的な使い方- モジュール環境下でリフレクションを利用する際の注意点と対処法
- 従来のクラスパスベースのプロジェクトからモジュールパスへ移行するための知識(自動モジュールなど)
第1章: Javaモジュールシステム(Project Jigsaw)とは?
Java 9で正式に導入されたモジュールシステムは、Project Jigsawというプロジェクト名で長年開発が進められてきました。この仕組みが導入された背景には、従来のJava開発が抱えていた深刻な問題、通称「クラスパス地獄(Classpath Hell)」があります。
クラスパス地獄とは?
従来のJavaアプリケーションでは、ライブラリ(JARファイル)の依存関係をクラスパスで管理していました。しかし、この方法には以下のような問題点がありました。
- 依存関係の曖昧さ: どのJARがどのJARに依存しているのかが不明確で、不要なライブラリが含まれたり、逆に必要なライブラリが不足したりすることが頻繁にありました。
- バージョンの競合: 異なるライブラリが同じライブラリの異なるバージョンに依存している場合、クラスパスの順序によって意図しないクラスが読み込まれ、実行時エラーを引き起こすことがありました。
- カプセル化の欠如:
public
修飾子が付与されたクラスは、ライブラリの内部実装用であっても外部からアクセス可能でした。これにより、意図しない使われ方をされてしまい、ライブラリのバージョンアップを困難にする一因となっていました。
Javaモジュールシステムは、これらの問題を解決するために生まれました。主な目的は以下の2つです。
- 信頼性の高いカプセル化 (Reliable Configuration): モジュールは、どのパッケージを外部に公開(エクスポート)するかを明示的に宣言します。これにより、モジュール内部の実装詳細を隠蔽し、意図しないアクセスを防ぐことができます。
- 強力なカプセル化 (Strong Encapsulation): モジュール間の依存関係を明確に定義します。アプリケーションの起動時に、モジュールシステムがこれらの依存関係を検証し、不足や競合があればエラーを報告してくれるため、実行時エラーのリスクを大幅に低減できます。
このモジュールシステムの導入により、JDK自身も複数のモジュールに分割されました。これにより、アプリケーションが必要とするモジュールだけを含む、より軽量なJavaランタイム環境を構築することも可能になっています。
第2章: モジュールの基本 `module-info.java`
モジュールシステムの中心となるのが、モジュール記述子ファイル (`module-info.java`)です。これはソースディレクトリのルートに配置される特別なJavaファイルで、モジュールの定義情報を記述します。コンパイルされると `module-info.class` ファイルが生成され、JARファイルのルートに格納されます。
`module-info.java` には、モジュールの名前、依存関係、公開するパッケージなどを記述するためのディレクティブが用意されています。
主要なディレクティブ
以下に、`module-info.java` で使用される主要なディレクティブをまとめます。
ディレクティブ | 説明 |
---|---|
requires modulename; | このモジュールが `modulename` に依存することを示します。コンパイル時および実行時に、この依存関係が解決される必要があります。 |
requires transitive modulename; | 推移的な依存関係を宣言します。このモジュールを利用する他のモジュールは、暗黙的に `modulename` も読み込めるようになります。ライブラリ開発で便利な機能です。 |
exports packagename; | 指定した `packagename` を他のすべてのモジュールに対して公開します。この宣言がないパッケージの `public` なクラスやインタフェースは、モジュール外からアクセスできません。 |
exports packagename to modulename1, modulename2; | 指定したパッケージを、特定のモジュール (`modulename1`, `modulename2`) に限定して公開します。より厳密なアクセス制御が可能です。 |
opens packagename; | 指定したパッケージを、リフレクションによる深いアクセス(privateメンバーへのアクセスなど)に対して公開します。 |
opens packagename to modulename1, modulename2; | 指定したパッケージを、特定のモジュールからのリフレクションアクセスに対してのみ公開します。 |
uses servicename; | このモジュールが `servicename` というサービス(通常はインタフェース)を利用することを示します。ServiceLoader機構と連携します。 |
provides servicename with implementationname; | このモジュールが `servicename` というサービスの具体的な実装 (`implementationname`) を提供することを示します。 |
コード例: `module-info.java`
具体的な例を見てみましょう。あるアプリケーション `com.example.myapp` が、ロギングライブラリ `org.slf4j` と、自作のコアライブラリ `com.example.core` に依存しているとします。
まず、コアライブラリ `com.example.core` の `module-info.java` です。APIとして `com.example.core.api` パッケージを公開します。
// com.example.core/src/module-info.java
module com.example.core { // このモジュールは com.example.core.api パッケージを外部に公開します。 exports com.example.core.api;
}
次に、アプリケーション `com.example.myapp` の `module-info.java` です。
// com.example.myapp/src/module-info.java
module com.example.myapp { // slf4j モジュールに依存します。 requires org.slf4j; // 自作のコアライブラリに依存します。 requires com.example.core;
}
このように、`module-info.java` を記述することで、モジュール間の関係性が明確になります。もし `com.example.myapp` のコードが、`com.example.core` で `exports` されていないパッケージ(例: `com.example.core.internal`)のクラスを使おうとすると、コンパイルエラーが発生します。
第3章: `java.lang.module` パッケージの深掘り
モジュールシステムをプログラム的に操作するためのAPIが、`java.lang.module` パッケージとして提供されています。これにより、実行時にモジュール情報を取得したり、動的にモジュールをロードしたりといった高度な操作が可能になります。
ここでは、主要なクラスとインターフェースを紹介します。
Module
: 実行時にロードされた単一のモジュールを表すクラスです。`Class.getModule()` を使うことで、任意のクラスが属する `Module` オブジェクトを取得できます。レイヤー、記述子、アノテーションなどの情報を保持しています。ModuleDescriptor
: モジュールの静的な記述情報を表します。`module-info.java` に書かれた内容(モジュール名、バージョン、依存関係、公開パッケージなど)がこのクラスで表現されます。`Module` オブジェクトから `getDescriptor()` メソッドで取得できます。ModuleFinder
: 指定されたパス(モジュールパスなど)からモジュールを検索するためのインターフェースです。ModuleReference
: `ModuleFinder` によって見つかったモジュールの参照です。`ModuleDescriptor` を含み、モジュールの内容を読み込むための `open()` メソッドを提供します。Configuration
: ルートモジュール群から始まり、依存関係を解決した結果のモジュールグラフを表します。ModuleLayer
: JVM内におけるモジュールの「層」を表します。モジュールとクラスローダーをマッピングし、実際にクラスをロード可能にする役割を担います。JVM起動時には、`java.base` を含むブートレイヤーが作成されます。
コード例: 実行中のモジュール情報を取得する
`java.lang.module` APIを使って、現在実行中のアプリケーションがどのモジュールに依存しているかを出力するコードを書いてみましょう。
import java.lang.module.ModuleDescriptor;
import java.util.stream.Collectors;
public class ModuleInfoPrinter { public static void main(String[] args) { // ModuleInfoPrinter クラスが属するモジュールを取得 Module selfModule = ModuleInfoPrinter.class.getModule(); // モジュール記述子を取得 ModuleDescriptor descriptor = selfModule.getDescriptor(); if (descriptor != null) { System.out.println("Module Name: " + descriptor.name()); System.out.println("Is an open module? " + descriptor.isOpen()); System.out.println("\n--- Dependencies (requires) ---"); descriptor.requires().stream() .forEach(req -> { System.out.printf(" - Module: %s, Modifiers: %s%n", req.name(), req.modifiers().stream().map(Enum::name).collect(Collectors.joining(", "))); }); System.out.println("\n--- Exported Packages ---"); descriptor.exports().stream() .forEach(exp -> { System.out.printf(" - Package: %s%s%n", exp.source(), exp.isQualified() ? " to " + exp.targets() : ""); }); } else { System.out.println("This class is not in a named module (likely in the unnamed module)."); } }
}
このコードをモジュール化されたプロジェクト内で実行すると、そのモジュールの `module-info.java` に基づいた情報(依存モジュールや公開パッケージなど)がコンソールに出力されます。
第4章: リフレクションとモジュールシステム
モジュールシステムの強力なカプセル化は、リフレクションAPIの動作に大きな影響を与えます。従来のJavaでは、`setAccessible(true)` を呼び出すことで、`private` なフィールドやメソッドにもリフレクションでアクセスできました。しかし、モジュールシステムでは、この「深いリフレクション」がデフォルトでは許可されていません。
モジュールAのコードが、モジュールBの非公開パッケージ(`exports`されていないパッケージ)内のクラスにリフレクションでアクセスしようとすると、`IllegalAccessException` が発生します。これは、たとえそのクラスが `public` であっても同様です。
`opens` ディレクティブによる解決
この問題を解決するのが `opens` ディレクティブです。モジュールBの `module-info.java` で特定のパッケージを `opens` することで、他のモジュールからのリフレクションアクセスを明示的に許可できます。
// モジュールB (library.module) の module-info.java
module library.module { // com.example.lib.internal パッケージを // リフレクションアクセスに対して公開する opens com.example.lib.internal;
}
これにより、`library.module` に依存する他のモジュールは、`com.example.lib.internal` パッケージ内のクラスに対して `setAccessible(true)` を使ったリフレクションが可能になります。
コマンドラインオプションによる動的な許可
ライブラリの `module-info.java` を変更できない場合でも、JVMの起動時オプションで動的にアクセスを許可する方法があります。
--add-opens <module>/<package>=<target-module>
: 特定のモジュールの特定のパッケージを、対象モジュールからのリフレクションに対して開きます。対象モジュールを `ALL-UNNAMED` にすると、クラスパス上のすべてのコードからアクセス可能になります。--add-exports <module>/<package>=<target-module>
: 同様に、特定のパッケージを動的にエクスポートします。
これらのオプションは、特に古いライブラリをモジュール環境で利用する際に、一時的な回避策として非常に役立ちます。
第5章: 実践的な使い方と移行戦略
すべてのライブラリがモジュール化されているわけではありません。既存のアプリケーションをモジュール化する際には、モジュール化されていない従来のJARファイルを扱う必要があります。そのための仕組みが自動モジュール(Automatic Module)と無名モジュール(Unnamed Module)です。
自動モジュール (Automatic Module)
`module-info.class` を含まない従来のJARファイルをモジュールパスに置くと、それは「自動モジュール」として扱われます。
自動モジュールには以下の特徴があります。
- モジュール名: JARファイルのマニフェストファイル (`META-INF/MANIFEST.MF`) に `Automatic-Module-Name` エントリがあればその値がモジュール名になります。なければ、JARファイル名からバージョン情報などを除いたものが自動的にモジュール名として使われます。
- 依存関係 (requires): 他のすべてのモジュール(名前付きモジュール、自動モジュール)を暗黙的に `requires` します。つまり、モジュールパス上のすべてのモジュールにアクセスできます。
- 公開 (exports): 自身のすべてのパッケージを暗黙的に `exports` および `opens` します。これにより、他のモジュールからすべての `public` クラスにアクセスでき、リフレクションも可能です。
この仕組みにより、まだモジュール化されていないライブラリも、名前付きモジュールから `requires` して利用することができます。
無名モジュール (Unnamed Module)
一方、従来のクラスパスからロードされたクラスやJARはすべて、単一の「無名モジュール」に属するものとして扱われます。
無名モジュールは、後方互換性のための仕組みであり、以下のルールに従います。
- 無名モジュールは、モジュールパス上のすべてのモジュール(名前付きモジュール、自動モジュール)を読み込むことができます。
- しかし、名前付きモジュールは、無名モジュールを `requires` することはできず、その中のクラスにアクセスすることもできません。
この制約は重要で、アプリケーションをモジュール化する際には、すべての依存関係をモジュールパスに配置し、クラスパスを空にすることが理想的な状態となります。
高度なユースケース: `ModuleLayer`による動的ロード
`ModuleLayer` API を使うと、プラグイン機構のように、アプリケーション実行中に動的にモジュールをロードして利用する、といった高度な実装が可能です。
大まかな手順は以下のようになります。
- プラグインのJARファイル(モジュール)が置かれているディレクトリを指す `ModuleFinder` を作成します。
- 現在の `ModuleLayer` を親として、新しい `Configuration` を作成します。このとき、ロードしたいプラグインモジュールをルートモジュールとして指定します。
- 作成した `Configuration` とクラスローダーのマッピング関数を使って、新しい `ModuleLayer` を定義します。
- 新しい `ModuleLayer` から、プラグインのクラスをロードし、インスタンス化して利用します。
この機能により、アプリケーションを再起動することなく機能を拡張できる、柔軟なアーキテクチャを構築できます。
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
public class DynamicModuleLoader { public static void main(String[] args) throws Exception { // プラグインが置かれているディレクトリ Path pluginDir = Paths.get("plugins"); // 1. ModuleFinderを作成 ModuleFinder finder = ModuleFinder.of(pluginDir); // ロードしたいプラグインモジュール名 String pluginModuleName = "com.example.plugin.hello"; // 2. 新しいConfigurationを作成 // 現在のレイヤー(ブートレイヤー)を親レイヤーとする ModuleLayer bootLayer = ModuleLayer.boot(); Configuration parentConfig = bootLayer.configuration(); Configuration newConfig = parentConfig.resolve(finder, ModuleFinder.of(), Set.of(pluginModuleName)); // 3. 新しいModuleLayerを定義 // 各モジュールに新しいクラスローダーを割り当てる ModuleLayer newLayer = bootLayer.defineModulesWithManyLoaders(newConfig, ClassLoader.getSystemClassLoader()); // 4. プラグインのクラスをロードして利用 // 新しいレイヤーから目的のモジュールとクラスローダーを取得 ClassLoader pluginClassLoader = newLayer.findLoader(pluginModuleName); Class<?> pluginClass = pluginClassLoader.loadClass("com.example.plugin.hello.HelloPlugin"); // インタフェースを介してプラグインを実行 Runnable pluginInstance = (Runnable) pluginClass.getDeclaredConstructor().newInstance(); pluginInstance.run(); // プラグインの処理が実行される }
}
まとめ
Javaモジュールシステムと、それを支える `java.lang.module` パッケージは、Javaプラットフォームに大きな進化をもたらしました。クラスパス地獄からの脱却、信頼性と安全性の向上、そして軽量なランタイムの実現など、その恩恵は多岐にわたります。
最初は `module-info.java` の記述や、リフレクションの制約に戸惑うかもしれません。しかし、その根底にある「明確な依存関係」と「強力なカプセル化」という思想を理解することで、より堅牢でメンテナンス性の高いアプリケーションを構築できるようになります。
自動モジュールなどの移行支援機能を活用しつつ、ぜひ自身のプロジェクトへのモジュールシステムの導入を検討してみてください。それは、あなたのJava開発を次のレベルへと引き上げる、価値ある一歩となるはずです。