中級 40分 Lesson 9

Javaプラットフォームモジュールシステム(JPMS)

JPMSの3つの背景問題・module-info.java全キーワード・requires transitive・exports/opens違い・3種類のモジュール完全解説

Java Java Silver SE21 JPMS モジュール

Chapter 09 ─ Java プラットフォームモジュールシステム(JPMS)

試験の 7%。「module-info.java に何を書くか」「exportsopens の違い」「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 Hellrequires で明示的な依存宣言 → バージョン衝突を検出しやすく
カプセル化の欠如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.javaexports 宣言を確認する癖をつけること。

exports ... to ─ 特定モジュールにのみ公開

module com.example.shop {
    exports com.example.shop.admin to com.example.admin.app;
    // admin パッケージは com.example.admin.app にだけ公開
    // 他のモジュールからはアクセスできない
}

9-6. opens ─ リフレクション用に開放する

exportsopens の違い

通常の 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 なし moduleopens P;(s あり)は別物。試験で openopens を混同させる選択肢が出ることがある。


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.langObject, String, Integer, Thread 等)
  • java.util(コレクション, ラムダ, Optional 等)
  • java.ioInputStream, OutputStream 等)
  • java.nio(NIO ファイル操作等)
  • java.mathBigInteger, BigDecimal 等)

JDK の主要モジュール一覧

モジュール名主な内容
java.base基本クラス(自動依存)
java.sqlJDBC(java.sql.Connection 等)
java.desktopAWT, Swing(GUI)
java.net.httpHTTP クライアント(Java 11+)
java.loggingjava.util.logging
java.xmlXML 処理
java.seSE の全モジュールをまとめたアグリゲートモジュール

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.jarfoo.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.servicerequires している shop.client モジュールは shop.model のクラスを使えるか?

Q2: shop.service.internal パッケージの public クラスを shop.web モジュールから使えるか?

Q3: shop.service.entityprivate フィールドに対して、hibernate.core モジュールはリフレクションでアクセスできるか?

Q4: shop.logging.moduleshop.servicerequires している場合、java.logging のクラスを使えるか?

答え

Q1: 使える

shop.modelrequires transitive で宣言されているので、shop.servicerequires しているモジュール(shop.client)も自動的に shop.model を使える。

Q2: 使えない

shop.service.internalexports shop.service.internal to shop.admin なので、shop.admin だけがアクセスできる。shop.web はアクセスできない(コンパイルエラー)。

Q3: アクセスできる

opens shop.service.entity to hibernate.core が宣言されているので、hibernate.core はリフレクションでこのパッケージのクラスの private フィールドにもアクセスできる。

Q4: 使えない

java.loggingrequires java.loggingtransitive なし)なので、shop.servicerequires しても java.logging は自動的には使えない。java.logging を使いたいモジュールは自分で requires java.logging; を書く必要がある。

問題 2: モジュール種別の判定

以下の説明に最も合うモジュール種別を答えよ。

  1. module-info.java を持たない JAR をモジュールパスに置いた
  2. クラスパスに置いた JAR
  3. module-info.java を持つ JAR をモジュールパスに置いた
  4. 名前付きモジュールから requires できない種別はどれか
答え
  1. 自動モジュール(Automatic Module)
  2. 名前なしモジュール(Unnamed Module)
  3. 名前付きモジュール(Named Module)
  4. 名前なしモジュール(名前がないため 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 できない