Javaモジュールシステム徹底解説:java.lang.moduleを使いこなす

この記事から得られる知識:

  • 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つです。

  1. 信頼性の高いカプセル化 (Reliable Configuration): モジュールは、どのパッケージを外部に公開(エクスポート)するかを明示的に宣言します。これにより、モジュール内部の実装詳細を隠蔽し、意図しないアクセスを防ぐことができます。
  2. 強力なカプセル化 (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)` を使ったリフレクションが可能になります。

`exports` と `opens` の違い
  • exports: コンパイル時のアクセス(通常のコードからの参照)を許可します。リフレクションによる `private` メンバーへのアクセスは許可しません。
  • opens: 実行時のリフレクションアクセスを許可します。コンパイル時のアクセスは許可しません。
もし、コンパイル時のアクセスとリフレクションアクセスの両方を許可したい場合は、`exports` と `opens` の両方を記述する必要があります。

コマンドラインオプションによる動的な許可

ライブラリの `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 を使うと、プラグイン機構のように、アプリケーション実行中に動的にモジュールをロードして利用する、といった高度な実装が可能です。

大まかな手順は以下のようになります。

  1. プラグインのJARファイル(モジュール)が置かれているディレクトリを指す `ModuleFinder` を作成します。
  2. 現在の `ModuleLayer` を親として、新しい `Configuration` を作成します。このとき、ロードしたいプラグインモジュールをルートモジュールとして指定します。
  3. 作成した `Configuration` とクラスローダーのマッピング関数を使って、新しい `ModuleLayer` を定義します。
  4. 新しい `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開発を次のレベルへと引き上げる、価値ある一歩となるはずです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です