Javaプラットフォームモジュールシステム(JPMS)
JPMSの3つの背景問題・module-info.java全キーワード・requires transitive・exports/opens違い・3種類のモジュール完全解説
Chapter 09 ─ Java プラットフォームモジュールシステム(JPMS)
試験の 7%。「
module-info.javaに何を書くか」「exportsとopensの違い」「requires transitiveの効果」「3 種類のモジュール(名前付き・自動・名前なし)」の 4 点を押さえれば対応できる。試験コミュニティでは「publicクラスでもexportsなしでは外からアクセスできない」という事実を知らなかったという声が多い。
9-1. なぜモジュールシステムが必要だったか ─ 3 つの問題
Java 9 以前の Java には 3 つの構造的問題があった。
問題 1: Classpath Hell(クラスパス地獄)
現実の比喩: 会社の書庫に「報告書 2020 年版」と「報告書 2021 年版」が混在していて、どちらを参照しているか分からなくなった状態。
-cp lib/foo-1.0.jar:lib/foo-2.0.jar
同名クラスが複数の JAR に存在するとき、「どちらが使われるか」はクラスパスの順序次第で不定。バージョン競合による実行時エラーが多発した。しかもエラーになるのは実行時で、コンパイル時には気づかない。
問題 2: カプセル化の欠如
現実の比喩: 「社内限定資料」と書いてある書類が、実は廊下の棚に置いてあって誰でも持っていける状態。
// Java 8 以前: sun.misc.Unsafe(JDK の内部 API)にフルアクセスできた
import sun.misc.Unsafe;
// ↑ 「内部実装なので触らないで」という意図が表現できなかった
// → JDK のバージョンアップで内部実装が変わるたびに、
// これを使っていたライブラリが壊れた
public クラスはクラスパス上のすべてのコードからアクセスできた。「このパッケージは API として公開しているが、こちらのパッケージは内部実装だから外から触らないで」という意図を Java の型システムで表現する方法がなかった。
問題 3: JDK の肥大化
rt.jar という 1 つの巨大ファイル(64MB 以上)に JDK のすべてが入っていた。IoT デバイスや組み込みシステムでも「AWT/Swing(GUI ライブラリ)なんて使わない」のに、全部含める必要があった。
JPMS の解決策
Java 9 で導入された JPMS(Java Platform Module System):
| 問題 | JPMS の解決策 |
|---|---|
| Classpath Hell | requires で明示的な依存宣言 → バージョン衝突を検出しやすく |
| カプセル化の欠如 | exports しないパッケージには外部からアクセスできない(public でも!) |
| JDK の肥大化 | JDK 自体を java.base, java.sql 等に分割し、必要なものだけ使える |
9-2. モジュールとは ─「パッケージのグループ + 宣言ファイル」
モジュール = 関連するパッケージのグループ + module-info.java
module-info.java というファイル 1 枚に、モジュール名・依存先・公開パッケージをすべて宣言する。
ディレクトリ構造
src/
└── com.example.shop/ ← モジュールのルートディレクトリ
├── module-info.java ← ここに配置!(パッケージ内に置かない)
└── com/
└── example/
└── shop/
├── api/ ← 外部に公開するパッケージ
│ └── ShopService.java
└── impl/ ← 内部実装(外部に見せない)
└── ShopServiceImpl.java
module-info.java の置き場所: モジュールのルート(src/モジュール名/ の直下)。パッケージのディレクトリの中には置かない。
ファイル名は必ず module-info.java(他の名前は不可)。パッケージ宣言(package ...)も不要(ファイル自体がモジュール定義ファイルだから)。
9-3. module-info.java の書き方 ─ 全キーワード解説
基本構文
module モジュール名 {
キーワード 対象;
...
}
モジュール名はドット区切りの識別子。慣習的にルートパッケージ名と合わせる(例: com.example.shop)。
完全な例
// module-info.java(com.example.shop モジュールの定義)
module com.example.shop {
// ── 依存宣言(requires) ──
requires java.sql; // java.sql モジュールに依存
requires transitive java.logging; // 推移的依存(後述)
requires static org.junit.jupiter.api; // コンパイル時のみ依存
// ── パッケージの公開(exports) ──
exports com.example.shop.api; // 全モジュールに公開
exports com.example.shop.util to com.example.client; // 特定モジュールだけに公開
// ── リフレクション用開放(opens) ──
opens com.example.shop.model; // 全モジュールにリフレクション許可
opens com.example.shop.dto to hibernate.core; // 特定モジュールにのみ許可
// ── SPI(サービスプロバイダ) ──
uses com.example.shop.spi.PaymentProvider; // このサービスを使う
provides com.example.shop.spi.PaymentProvider
with com.example.shop.impl.CreditCardProvider; // 実装を提供する
}
9-4. requires ─ 依存するモジュールを宣言
3 種類の requires
requires java.sql; // 通常の依存
requires transitive java.logging; // 推移的依存
requires static org.junit.jupiter.api;// コンパイル時のみ
| 形式 | 意味 |
|---|---|
requires M | このモジュールは M に依存する |
requires transitive M | このモジュールの依存者も M を暗黙的に使える |
requires static M | コンパイル時のみ依存(実行時は任意) |
requires transitive の効果(試験頻出)
現実の比喩: 会社 A が仕入れ先 B と取引していて、A の商品を仕入れた会社 C は、B の製品を間接的に手に入れられるイメージ。
A: requires transitive B
↓
C: requires A
↓
C は A も B も両方のクラスを使える(B を直接 requires しなくても)
具体例:
// java.sql は内部で requires transitive java.logging を持っている
// → java.sql を requires したモジュールは自動的に java.logging も使える
module com.example.app {
requires java.sql;
// java.logging は直接書かなくてもよい(java.sql が transitive で連れてくる)
}
transitive がない場合:
// もし java.sql が requires java.logging(transitiveなし)だったとしたら…
module com.example.app {
requires java.sql;
// java.logging を使いたいなら自分でも書く必要がある
requires java.logging; // ← これが必要になる
}
requires static の用途
コンパイル時にのみ必要で、実行時は任意(あってもなくてもいい)ライブラリに使う。典型的な用途はアノテーションプロセッサ・テストライブラリ。
requires static org.junit.jupiter.api; // テスト時(コンパイル時)だけ使う
// 本番実行時に JUnit が classpath になくても動く
9-5. exports ─ パッケージを外部に公開する
exports の基本
exports しないパッケージは、public クラスでも外部モジュールからアクセスできない。これが JPMS の最大のポイント。
// モジュール A の module-info.java
module com.example.a {
exports com.example.a.api; // api パッケージだけ公開
// impl パッケージは exports しない → 非公開
}
// モジュール B から A を使う
import com.example.a.api.ShopService; // OK(exports されている)
import com.example.a.impl.InternalUtil; // コンパイルエラー!(exports されていない)
// InternalUtil が public クラスでもアクセス不可
試験の引っかけ: 「モジュール A の
implパッケージにはpublicクラスがあるので、外部から使える」→ ×。exportsがなければpublicでも外部からアクセスできない。試験口コミ: 「
publicクラスだから当然使えると思って選んだら不正解だった。モジュールシステムではexportsの有無が全て」という声が頻繁に見られる。試験でモジュールのコードを読むときは、まずmodule-info.javaのexports宣言を確認する癖をつけること。
exports ... to ─ 特定モジュールにのみ公開
module com.example.shop {
exports com.example.shop.admin to com.example.admin.app;
// admin パッケージは com.example.admin.app にだけ公開
// 他のモジュールからはアクセスできない
}
9-6. opens ─ リフレクション用に開放する
exports と opens の違い
通常の exports は「通常のコード(コンパイル時・実行時)のアクセス」を許可するが、リフレクション(Field.get(), Method.invoke() 等)は含まない。
Spring や Hibernate のような DI/ORM フレームワークは、リフレクションでフィールドやメソッドにアクセスする。そのため opens が必要。
module com.example.app {
opens com.example.app.model; // 全モジュールにリフレクション許可
opens com.example.app.dto to hibernate.core; // hibernate.core だけにリフレクション許可
}
アクセス種別の比較表(試験頻出)
| 宣言 | コンパイル時アクセス | 実行時アクセス | リフレクション |
|---|---|---|---|
| なし | ✗ | ✗ | ✗ |
exports P | ○ | ○ | ✗ |
opens P | ✗ | ✗ | ○ |
exports P + opens P | ○ | ○ | ○ |
exports: 「読んでいいよ、使っていいよ(でもリフレクションはダメ)」opens: 「リフレクションで中を見てもいいよ(でも通常コードでは参照できない)」
open module ─ 全パッケージを一括でリフレクション開放
opens を個別に書く代わりに、module キーワードの前に open を付けると、モジュール内のすべてのパッケージが一括で開放された状態になる。
// ── 個別に opens を書く場合(通常スタイル)──
module com.example.app {
requires java.base;
opens com.example.app.model;
opens com.example.app.dto;
opens com.example.app.entity;
// パッケージが増えるたびに追加が必要
}
// ── open module で一括開放(全パッケージが自動的に opens 状態)──
open module com.example.legacy {
requires java.base;
// 個別の opens 宣言が不要
}
open module はレガシーコードやテスト用モジュールで使われる。本番コードでは必要なパッケージだけを opens ... to で指定するのが推奨(最小権限の原則)。
スペル注意:
open module(スペースあり)がopenなしmoduleとopens P;(s あり)は別物。試験でopenとopensを混同させる選択肢が出ることがある。
9-7. uses / provides ─ SPI(サービスプロバイダ)
Silver 試験では「名前と概念を知っていればよい」レベル。
SPI(Service Provider Interface)パターン: インターフェースを定義し、その実装を別モジュールが提供する仕組み。ロガーやデータベースドライバのプラグイン機構のようなもの。
// サービスを使う側のモジュール
module com.example.app {
uses com.example.spi.Logger; // このインターフェースの実装をどこかから探す
}
// サービスを提供する側のモジュール
module com.example.logger.impl {
provides com.example.spi.Logger
with com.example.logger.FileLogger; // FileLogger が Logger の実装
}
ServiceLoader.load(Logger.class) で実装を動的にロードできる。依存関係を切り離したプラグイン設計が可能。
9-8. java.base ─ すべてのモジュールの親
java.base はすべてのモジュールが自動的に依存する特別なモジュール。
requires java.base; は不要(書いても問題ないが省略するのが通常)。
java.base に含まれる主要パッケージ(試験で問われる範囲):
java.lang(Object,String,Integer,Thread等)java.util(コレクション, ラムダ,Optional等)java.io(InputStream,OutputStream等)java.nio(NIO ファイル操作等)java.math(BigInteger,BigDecimal等)
JDK の主要モジュール一覧
| モジュール名 | 主な内容 |
|---|---|
java.base | 基本クラス(自動依存) |
java.sql | JDBC(java.sql.Connection 等) |
java.desktop | AWT, Swing(GUI) |
java.net.http | HTTP クライアント(Java 11+) |
java.logging | java.util.logging |
java.xml | XML 処理 |
java.se | SE の全モジュールをまとめたアグリゲートモジュール |
9-9. 3 種類のモジュール ─ 試験頻出の分類
名前付きモジュール(Named Module)
module-info.java を持つ JAR。モジュールシステムに正式に「参加」している。
exports,opensを明示的に宣言する- 他のモジュールから
requiresできる
自動モジュール(Automatic Module)
module-info.java を持たない JAR をモジュールパス(--module-path)に置いた場合、JVM が自動的にモジュールとして扱う。
- モジュール名は JAR のファイル名から自動生成(
foo-bar-1.2.jar→foo.bar) - すべてのパッケージが自動的に
exportsされる - 他の名前付きモジュールから
requires foo.bar;で依存できる - クラスパスのコード(名前なしモジュール)にも暗黙的に依存する
名前なしモジュール(Unnamed Module)
クラスパス(-cp)に置かれたコードや JAR は「名前なしモジュール」として扱われる。
- すべてのパッケージにアクセスできる(後方互換のため)
- 他の名前付きモジュールから
requiresできない(名前がないため参照不可) - Java 8 以前のコードをそのまま動かすための後方互換仕組み
3 種類の比較表
| 名前付きモジュール | 自動モジュール | 名前なしモジュール | |
|---|---|---|---|
module-info.java | あり | なし | なし |
| 配置場所 | モジュールパス | モジュールパス | クラスパス |
他から requires できるか | ○ | ○ | ✗ |
exports | 明示的(宣言した分のみ) | 全パッケージ自動 | 全パッケージ自動 |
| 用途 | 新規開発 | 移行期の古いライブラリ | 後方互換 |
試験の引っかけ: 「名前なしモジュールは他の名前付きモジュールから
requiresできる」→ ×。名前がないのでrequiresで指定できない。
9-10. コマンドラインオプション(参考)
試験では「主要オプションの意味を理解する」程度でよい。
# コンパイル時のモジュールパス指定
javac --module-path mods -d out src/com.example.shop/module-info.java \
src/com.example.shop/com/example/shop/Main.java
# 実行時のモジュール指定(--module = -m の略)
java --module-path mods --module com.example.shop/com.example.shop.Main
# アクセス権を実行時に追加(開発・テスト用)
java --add-opens java.base/java.lang=ALL-UNNAMED ...
java --add-exports java.base/sun.misc=ALL-UNNAMED ...
# ALL-UNNAMED = 名前なしモジュール(クラスパス上のコード)を指す
✏️ 練習問題
問題 1: module-info.java の読み取り
次の module-info.java を読んで設問に答えよ。
module shop.service {
requires java.logging;
requires transitive shop.model;
exports shop.service.api;
exports shop.service.internal to shop.admin;
opens shop.service.entity to hibernate.core;
}
Q1: shop.service を requires している shop.client モジュールは shop.model のクラスを使えるか?
Q2: shop.service.internal パッケージの public クラスを shop.web モジュールから使えるか?
Q3: shop.service.entity の private フィールドに対して、hibernate.core モジュールはリフレクションでアクセスできるか?
Q4: shop.logging.module が shop.service を requires している場合、java.logging のクラスを使えるか?
答え
Q1: 使える
shop.model は requires transitive で宣言されているので、shop.service を requires しているモジュール(shop.client)も自動的に shop.model を使える。
Q2: 使えない
shop.service.internal は exports shop.service.internal to shop.admin なので、shop.admin だけがアクセスできる。shop.web はアクセスできない(コンパイルエラー)。
Q3: アクセスできる
opens shop.service.entity to hibernate.core が宣言されているので、hibernate.core はリフレクションでこのパッケージのクラスの private フィールドにもアクセスできる。
Q4: 使えない
java.logging は requires java.logging(transitive なし)なので、shop.service を requires しても java.logging は自動的には使えない。java.logging を使いたいモジュールは自分で requires java.logging; を書く必要がある。
問題 2: モジュール種別の判定
以下の説明に最も合うモジュール種別を答えよ。
module-info.javaを持たない JAR をモジュールパスに置いた- クラスパスに置いた JAR
module-info.javaを持つ JAR をモジュールパスに置いた- 名前付きモジュールから
requiresできない種別はどれか
答え
- 自動モジュール(Automatic Module)
- 名前なしモジュール(Unnamed Module)
- 名前付きモジュール(Named Module)
- 名前なしモジュール(名前がないため
requires文で参照できない)
Chapter 09 チェックリスト
- JPMS が解決する 3 つの問題(Classpath Hell・カプセル化欠如・JDK 肥大化)
-
module-info.javaはモジュールのルートに配置(パッケージ内には置かない) -
module-info.javaにはパッケージ宣言(package)は不要 -
requires M: M モジュールに依存する -
requires transitive M: このモジュールをrequiresした側も M を使える -
requires static M: コンパイル時のみ依存(実行時は任意) -
exports P: P パッケージを全モジュールに公開 -
exports P to M: P パッケージを M モジュールだけに公開 -
exportsされていないpublicクラスは外部モジュールからアクセスできない(試験最頻出) -
opens P: リフレクション用に P パッケージを全モジュールに開放 -
opens P to M: リフレクション用に P パッケージを M モジュールだけに開放 -
exportsは通常アクセス(コンパイル時・実行時)を許可、リフレクションは許可しない -
opensはリフレクションのみを許可、通常コードでのアクセスは許可しない -
open module: モジュール全体の全パッケージを一括 opens -
java.baseはすべてのモジュールが自動依存(requires java.base;は不要) -
uses: SPI のインターフェースを使う宣言 -
provides ... with ...: SPI の実装を提供する宣言 - 名前付きモジュール:
module-info.javaあり、モジュールパスに配置 - 自動モジュール:
module-info.javaなし、モジュールパスに配置、全パッケージ自動 exports - 名前なしモジュール: クラスパスに配置、他の名前付きモジュールから
requiresできない