1. 導入: 大規模開発の救世主、Javaモジュールシステム
皆さん、こんにちは!シニアJavaエンジニアの〇〇です。
Javaで少し複雑なアプリケーションを開発していると、「依存関係がごちゃごちゃしてきた…」「このクラス、本来は外部から使われたくないんだけどな…」といった悩みに直面したことはありませんか?そんな課題を解決するために、Java 9から導入されたのが「Javaモジュールシステム(Project Jigsaw)」です。
これは、従来のJARファイルよりもさらに上位の概念で、アプリケーションの部品(モジュール)間の関係性を明確にし、より堅牢で保守しやすいシステムを構築するための仕組みです。このシステムを理解することは、現代のJava開発において非常に重要になってきています。
2. 基礎知識: モジュールシステムのキーワードを理解しよう
まずは、モジュールシステムを理解するために必要な基本的な用語を解説します。
- Project Jigsaw(プロジェクト ジグソー):
Java 9で導入されたモジュールシステムの開発プロジェクト名です。これにより、Javaプラットフォーム自体もモジュール化され、不要な部分を省いた軽量なJREを構築できるようになりました。 - モジュール:
関連するパッケージ、リソース、サービスなどを論理的にまとめた単位です。従来のJARファイルと似ていますが、モジュールは「他のどのモジュールに依存するか」「どのパッケージを外部に公開するか」といった情報を明示的に宣言します。 - `module-info.java`:
モジュールの「設計図」となる特別なファイルです。このファイルに、モジュールの名前、他のモジュールへの依存関係、外部に公開するパッケージなどを記述します。コンパイル時に、この情報がモジュールのメタデータとして埋め込まれます。 - `java.lang.module.ModuleDescriptor`:
`module-info.java`の内容がコンパイルされた後に、JVMが内部的に利用するモジュール情報を表すクラスです。開発者が直接このクラスを操作することは稀ですが、`module-info.java`で定義した内容が、最終的にこの`ModuleDescriptor`としてJVMに認識されると理解しておくと良いでしょう。 - `requires`(リクワイヤーズ):
`module-info.java`内で使用するキーワードで、「このモジュールは、指定されたモジュールに依存します」と宣言します。これにより、依存関係が明確になり、実行時に必要なモジュールが確実にあることを保証できます。 - `exports`(エクスポート):
`module-info.java`内で使用するキーワードで、「このモジュール内の、指定されたパッケージを外部のモジュールに公開します」と宣言します。これにより、意図しない内部クラスへのアクセスを防ぎ、モジュールのカプセル化を強力に保つことができます。 - `opens`(オープンズ):
`module-info.java`内で使用するキーワードで、「このモジュール内の、指定されたパッケージは、実行時にリフレクションによるアクセスを許可します」と宣言します。`exports`とは異なり、コンパイル時には見えず、リフレクション(プログラムが自身の構造を検査・操作する機能)を使う場合にのみ利用されます。主にSpring FrameworkのようなDIコンテナやORMなどで内部的に利用されることがあります。
3. 実装/解決策: モジュール化されたプロジェクトを作ってみよう
モジュール化されたJavaアプリケーションを構築する基本的な手順を見ていきましょう。
プロジェクトの構造は少し変わりますが、一度理解してしまえばとても強力なツールになります。
基本的なプロジェクト構造:
モジュールごとにソースコードのルートディレクトリを分け、その中に`module-info.java`ファイルを配置するのが一般的です。
project_root/
├── myapp.main/ // メインアプリケーションのモジュール
│ ├── module-info.java // myapp.mainモジュールの定義ファイル
│ └── com/
│ └── myapp/
│ └── main/
│ └── MainApp.java // メインクラス
└── myapp.util/ // ユーティリティモジュール
├── module-info.java // myapp.utilモジュールの定義ファイル
└── com/
└── myapp/
└── util/
└── calc/
└── Calculator.java // 計算ロジック
`module-info.java`の記述例:
// myapp.util/module-info.java
module myapp.util { // モジュール名を宣言します
exports com.myapp.util.calc; // このモジュールは、com.myapp.util.calcパッケージを外部に公開します
}
// myapp.main/module-info.java
module myapp.main { // モジュール名を宣言します
requires myapp.util; // このモジュールは、myapp.utilモジュールに依存します
// java.base モジュールは暗黙的にrequiresされるため、通常は記述不要です。
// requires java.base;
}
このように、モジュールはどのパッケージを公開し、どのモジュールに依存するかを`module-info.java`で明確に宣言します。
4. サンプルプログラム: 実際に動かしてみよう!
上記で説明した構造で、簡単な計算アプリケーションを作成してみましょう。
1. ディレクトリ構造の作成:
まず、以下のディレクトリ構造を作成してください。
mkdir project_root
mkdir project_root/myapp.main
mkdir project_root/myapp.main/com/myapp/main -p
mkdir project_root/myapp.util
mkdir project_root/myapp.util/com/myapp/util/calc -p
2. 各ファイルの作成とコードの記述:
// project_root/myapp.util/module-info.java
module myapp.util {
// このモジュールは、com.myapp.util.calc パッケージを外部のモジュールに公開します。
// これにより、他のモジュールはこのパッケージ内のpublicなクラスやインターフェースを利用できるようになります。
exports com.myapp.util.calc;
}
// project_root/myapp.util/com/myapp/util/calc/Calculator.java
package com.myapp.util.calc;
public class Calculator {
/
- 2つの整数を足し算します。
- @param a 最初の数値
- @param b 2番目の数値
- @return 合計
/
public int add(int a, int b) {
return a + b;
}
// このメソッドはprivateなので、そもそも外部からはアクセスできません。
// 仮にpublicだったとしても、module-info.java で exports されていない限り、
// 他のモジュールからは利用できません。
private String getInfo() {
return "Simple Calculator Utility";
}
}
// project_root/myapp.main/module-info.java
module myapp.main {
// このモジュールは、myapp.util モジュールに依存します。
// myapp.util モジュールが公開しているパッケージ内のクラスを利用できます。
requires myapp.util;
// Javaの基本APIを提供するjava.baseモジュールは、すべてのモジュールから暗黙的に利用できるため、
// 通常は明示的に requires 宣言する必要はありません。
// requires java.base;
}
// project_root/myapp.main/com/myapp/main/MainApp.java
package com.myapp.main;
// myapp.util モジュールが exports している com.myapp.util.calc パッケージのCalculatorクラスをインポートします。
import com.myapp.util.calc.Calculator;
public class MainApp {
public static void main(String[] args) {
System.out.println("--- Javaモジュールシステムサンプル ---");
// myapp.util モジュールから提供されるCalculatorクラスのインスタンスを作成します。
Calculator calculator = new Calculator();
int result = calculator.add(10, 20); // addメソッドを呼び出します。
System.out.println("計算結果 (10 + 20): " + result); // 出力: 計算結果 (10 + 20): 30
// 以下はコンパイルエラーになる例です。
// CalculatorクラスのgetInfo()メソッドはprivateであり、
// 仮にpublicであったとしても、myapp.utilモジュールのmodule-info.javaでexportsされていないため、
// myapp.mainモジュールからはアクセスできません。
// System.out.println(calculator.getInfo()); // コンパイルエラー!
}
}
3. コンパイルと実行:
`project_root`ディレクトリに移動して、以下のコマンドを実行してください。
1. コンパイル済みモジュールを格納するディレクトリを作成します
mkdir mods
2. myapp.util モジュールをコンパイルします。
-d mods/myapp.util : コンパイル結果を mods/myapp.util ディレクトリに出力します。
–module-source-path . : ソースコードのルートディレクトリを現在のディレクトリ(.)と指定します。
これにより、myapp.util/module-info.java などのパスが解決されます。
myapp.util/module-info.java myapp.util/com/myapp/util/calc/Calculator.java : コンパイル対象のファイルを指定します。
javac -d mods/myapp.util –module-source-path . myapp.util/module-info.java myapp.util/com/myapp/util/calc/Calculator.java
3. myapp.main モジュールをコンパイルします。
myapp.main は myapp.util に依存しているため、–module-path で myapp.util モジュールの場所を指定する必要があります。
–module-path mods : mods ディレクトリに依存するモジュール(myapp.util)があることを指定します。
javac -d mods/myapp.main –module-source-path . –module-path mods myapp.main/module-info.java myapp.main/com/myapp/main/MainApp.java
4. myapp.main モジュールを実行します。
–module-path mods : 実行時に依存するモジュール(myapp.util)が mods ディレクトリにあることを指定します。
-m myapp.main/com.myapp.main.MainApp : 実行するモジュール名と、そのモジュール内のメインクラスを指定します。
java –module-path mods -m myapp.main/com.myapp.

コメント