Help us understand the problem. What is going on with this article?

モジュールグラフが作られる様子を学ぼう

モジュールグラフが作られる様子を学ぼう

by opengl-8080
1 / 35

自己紹介

  • opengl-8080
  • 主に Qiita で技術メモを書いたり
  • 関西の SIer 勤務

現場はまだまだ Java 8 が多い?

  • Java 9 以降の変化に警戒
    • リリースサイクルの変更
    • サポートポリシーの変更
  • Java 8 でしのぐ
  • その Java 8 も、来年1月で無償のサポートが終了
  • Java 11 からは LTS 付きの JDK がいくつか
    • Oracle JDK
    • AdoptOpenJDK
    • Amazon Correto
    • etc ?

そろそろ本格的に Java 11 へ移行?


立ちはだかるモジュールシステム


ビルド設定の不備に起因するエラーが増えた

  • Java 8 以前
    • 「パッケージ...は存在しません」
    • 「シンボルを見つけられません」
    • java.lang.ClassNotFoundException
    • java.lang.NoClassDefFoundError
    • java.lang.NoSuchMethodError
  • Java 9 以降(上記に加え。。。)
    • 「モジュールが見つかりません: ...」
    • 「パッケージ...は表示不可です」
      • 「パッケージ...はモジュール...で宣言されていますが、エクスポートされていません」
      • 「パッケージ...はモジュール...で宣言されていますが、モジュールfooに読み込まれていません」
      • 「パッケージ...はモジュール...で宣言されていますが、モジュール・グラフにありません」
    • java.lang.module.FindException
    • java.lang.IllegalAccessException

Java 8 以前のビルドエラーの原因

  • クラスパスに必要な jar がない
  • クラスパスに入れている jar のバージョンが間違っている

だいたい、クラスパスの設定を調べれば解決した(要出典)。


Java 9 で追加されたモジュールシステム

  • クラスをロードするときの仕組みが変わった
  • コンパイラや JVM は、アプリケーションが利用するモジュールの情報を集めて、各モジュールの関係を解析する
  • このステップを モジュールの解決 と呼び、モジュール間の関係を表したものを モジュールグラフ と呼ぶ

Java 9 以降のビルドエラーの原因

  • 設定漏れなどにより、モジュールの解決が正常に完了できなかった
  • 解決されたモジュールグラフが必要な形を満たしていない

モジュールグラフの作られ方を理解していないと、
エラーの原因が特定しづらい(気がする)


モジュールグラフが作られる様子を学ぼう


前提(詳しくは話さないこと、知ってる前提)

  • モジュールシステムが導入された背景やメリット、使い方など
  • Java 8 以前のクラスパスとかの説明
  • Oracle JDK と OpenJDK の違いや、ここ半年ほどの変遷
  • Java 9 以降の新規機能(jlink とか)

モジュールシステムでできること

  • 複数のパッケージを モジュール というカタマリにまとめられる
  • モジュール間の依存関係を定義できる
  • パッケージ単位で外部への公開・非公開を制御できる

jjug-ccc-2018.jpg


モジュールの定義

module-info.java
module foo {      // foo モジュールの定義
    exports foo;  // foo パッケージの公開
    requires bar; // bar モジュールへの依存を宣言
}
  • module-info.java(class) ファイルをモジュール内のルートに配置
  • モジュール名、公開するパッケージ、依存するモジュールなどを宣言できる

モジュールを使った場合の大前提

あるモジュール内のクラスが別のモジュールのクラスを使用するためには、次の条件を満たしている必要がある。

  • 対象のクラスが存在するモジュールを直接 requires している
  • 対象のクラスが存在するパッケージが exports されている

jjug-ccc-2018.jpg

java.base モジュールは自動的に requires されるので明示的な宣言が不要


推移的な requires

module-info.java
module bar {
    requires transitive fuga; // transitive 修飾子をつける
}
  • requirestransitive 修飾子をつけると、 requires が伝播するようになる
  • この場合、 barrequires すると、自動的に fugarequires したことになる

jjug-ccc-2018.jpg


モジュールグラフ

  • モジュール間の関係を表したもの
  • コンパイルや JVM 起動時に、このモジュールグラフが作られる

jjug-ccc-2018.jpg


ルートモジュールを決める

  • モジュールグラフの起点となるモジュール = ルートモジュール を決める

jjug-ccc-2018.jpg


コンパイル時と JVM 起動時でルートモジュールは異なる

コンパイル時

  • コンパイル対象のソース(モジュール)

JVM 起動時

  • --module (-m) オプションで指定したモジュール
  • メインクラスの指定というよりも、先頭のモジュールを指定するためのオプション

module[/mainclass]

解決する初期moduleの名前を指定します。
また、moduleで指定されていない場合は、実行するmainclassの名前を指定します。

java | Java Platform, Standard Editionツール・リファレンス


ルートモジュールから依存関係を辿る

  • ルートモジュールが決まったら、 module-info の内容を確認する
  • 依存している(requires している) モジュールを調べ、モジュールグラフを構築していく

jjug-ccc-2018.jpg


観測可能なモジュール

  • モジュールを検索してくる場所
  • ルートモジュールおよび requires しているモジュールは、 観測可能なモジュール でなければならない
  • 大きく2つ存在する
    • システムモジュール
    • モジュールパス
  • 厳密には他にも観測可能なモジュールの候補は存在するが、ここでは割愛(詳細は Javadoc などを参照

システムモジュール

  • 実行環境に組み込まれているモジュール
  • java.base, java.desktop, java.sql などなど
  • 固定ではない
    • jlink で作られたカスタム JRE
    • Oracle Java 9, 10 では JavaFX 系のモジュールが含まれていた
  • ModuleFinder.ofSystem() で一覧を取得可能

モジュールパス

  • --module-path (-p) で指定
  • システムモジュールに存在しないモジュールは、モジュールパスに追加しなければ requires できない
    • アプリケーション自身
    • サードパーティのライブラリ

モジュールグラフの話を整理

jjug-ccc-2018.jpg

  • モジュールグラフとは、モジュール間の関係を表したもの
  • ルートモジュールを起点にして、 requires しているモジュールを紐づけていく
  • ルートモジュールは、コンパイル時は対象のソースが、実行時は --module で指定したモジュールがなる
  • ルートモジュールおよび requires するモジュールは、観測可能なモジュールでなければならない
  • 代表的な観測可能なモジュールとして、システムモジュールとモジュールパスに含まれるモジュールがある

モジュールの解決の様子を確認する

  • java コマンドのオプションに --show-module-resolution を追加すると、モジュールの解決の様子を確認できる
$ java --show-module-resolution \ # モジュールの解決の様子を出力するオプション
       --module-path ... \
       --module ...
実行結果
root foo file:///.../foo/classes/ # ルートモジュール
foo requires bar file:///.../bar/classes/ # requires をたどっている
java.base binds java.desktop jrt:/java.desktop # サービスプロバイダの解決
java.base binds jdk.jartool jrt:/jdk.jartool
java.base binds jdk.jlink jrt:/jdk.jlink
java.base binds jdk.compiler jrt:/jdk.compiler
java.base binds jdk.javadoc jrt:/jdk.javadoc
java.base binds jdk.jdeps jrt:/jdk.jdeps
java.base binds jdk.localedata jrt:/jdk.localedata
java.base binds java.security.sasl jrt:/java.security.sasl
java.base binds jdk.crypto.mscapi jrt:/jdk.crypto.mscapi
java.base binds java.security.jgss jrt:/java.security.jgss
java.base binds java.smartcardio jrt:/java.smartcardio
java.base binds java.xml.crypto jrt:/java.xml.crypto
java.base binds jdk.crypto.cryptoki jrt:/jdk.crypto.cryptoki
java.base binds jdk.security.jgss jrt:/jdk.security.jgss
java.base binds java.naming jrt:/java.naming
java.base binds jdk.crypto.ec jrt:/jdk.crypto.ec
java.base binds jdk.zipfs jrt:/jdk.zipfs
java.base binds jdk.charsets jrt:/jdk.charsets
java.base binds java.logging jrt:/java.logging
java.base binds java.management jrt:/java.management
java.base binds jdk.security.auth jrt:/jdk.security.auth
jdk.security.auth requires java.naming jrt:/java.naming # サービスプロバイダとして読み込まれたモジュールの requires をたどっている
jdk.security.auth requires java.security.jgss jrt:/java.security.jgss
java.naming requires java.security.sasl jrt:/java.security.sasl
jdk.security.jgss requires java.security.jgss jrt:/java.security.jgss
jdk.security.jgss requires java.logging jrt:/java.logging
jdk.security.jgss requires java.security.sasl jrt:/java.security.sasl
jdk.crypto.cryptoki requires jdk.crypto.ec jrt:/jdk.crypto.ec
java.xml.crypto requires java.logging jrt:/java.logging
java.xml.crypto requires java.xml jrt:/java.xml
java.security.jgss requires java.naming jrt:/java.naming
java.security.sasl requires java.logging jrt:/java.logging
jdk.jdeps requires jdk.compiler jrt:/jdk.compiler
jdk.jdeps requires java.compiler jrt:/java.compiler
jdk.javadoc requires jdk.compiler jrt:/jdk.compiler
jdk.javadoc requires java.xml jrt:/java.xml
jdk.javadoc requires java.compiler jrt:/java.compiler
jdk.compiler requires java.compiler jrt:/java.compiler
jdk.jlink requires jdk.internal.opt jrt:/jdk.internal.opt
jdk.jlink requires jdk.jdeps jrt:/jdk.jdeps
java.desktop requires java.datatransfer jrt:/java.datatransfer
java.desktop requires java.xml jrt:/java.xml
java.desktop requires java.prefs jrt:/java.prefs
java.prefs requires java.xml jrt:/java.xml
java.datatransfer binds java.desktop jrt:/java.desktop
java.management binds jdk.management.jfr jrt:/jdk.management.jfr
java.management binds jdk.management jrt:/jdk.management
java.management binds java.management.rmi jrt:/java.management.rmi
java.desktop binds jdk.accessibility jrt:/jdk.accessibility
java.desktop binds jdk.unsupported.desktop jrt:/jdk.unsupported.desktop
java.compiler binds jdk.javadoc jrt:/jdk.javadoc
java.compiler binds jdk.compiler jrt:/jdk.compiler
java.naming binds jdk.naming.rmi jrt:/jdk.naming.rmi
java.naming binds jdk.naming.dns jrt:/jdk.naming.dns
jdk.naming.dns requires java.naming jrt:/java.naming
jdk.naming.rmi requires java.rmi jrt:/java.rmi
jdk.naming.rmi requires java.naming jrt:/java.naming
jdk.unsupported.desktop requires java.desktop jrt:/java.desktop
jdk.accessibility requires java.desktop jrt:/java.desktop
java.management.rmi requires java.management jrt:/java.management
java.management.rmi requires java.rmi jrt:/java.rmi
java.management.rmi requires java.naming jrt:/java.naming
jdk.management requires java.management jrt:/java.management
jdk.management.jfr requires java.management jrt:/java.management
jdk.management.jfr requires jdk.management jrt:/jdk.management
jdk.management.jfr requires jdk.jfr jrt:/jdk.jfr
java.rmi requires java.logging jrt:/java.logging

下位互換の話が入ってくるとややこしくなる


Java 8 以前のコードを Java 9 以上で動かす

$ java --classpath path/to/classes foo.bar.MainClassName
  • Java 8 以前同様、 jar ファイルなどをクラスパスを追加して起動する
  • クラスパスは引き続き存在している
  • 表面上は変わっていないが、内部の動きは大きく変わっている

無名モジュール(unnamed module)

  • クラスパスから読み込まれたクラスは、無名モジュール に所属するようになる
    • モジュール定義(module-info)を持つ通常のモジュールは、名前付きモジュール(named module)と呼ぶ
  • 無名モジュールはモジュール定義を持っていない(持てない)
    • 全てのパッケージを exports している扱いになる
    • モジュールグラフ上の全てのモジュールを requires している扱いになる

無名モジュールが起点となった場合

無名モジュールが起点(コンパイル対象, Main クラス)になった場合、ルートモジュールの決定方法が変わる
ザックリ言うと、システムモジュール内のほぼ全てのモジュールがルートになる。

厳密なルールは以下。

  • --upgrade-module-path で指定した場所に存在するモジュール
    • 任意のモジュールの内容を上書きするオプション
  • システムモジュール内に存在し、 to による公開先の制限なしで最低1つはパッケージを exports しているモジュール
    • exportsto で公開先のモジュールを制限できる
    • ルートになるのは、 to による制限のないモジュールが1つ以上存在するモノのみ

※Java 10 まではもっと複雑だった。詳細は モジュールシステムを学ぶ - Qiita を参照。


無名モジュールで起動したときのモジュールの解決を見る

$ java --show-module-resolution \
       --classpath ... \
       ...
実行結果
root java.sql jrt:/java.sql
root jdk.management.jfr jrt:/jdk.management.jfr
root java.rmi jrt:/java.rmi
root jdk.jdi jrt:/jdk.jdi
root java.transaction.xa jrt:/java.transaction.xa
root java.logging jrt:/java.logging
root java.xml.crypto jrt:/java.xml.crypto
root java.xml jrt:/java.xml
root jdk.xml.dom jrt:/jdk.xml.dom
root jdk.jfr jrt:/jdk.jfr
root java.datatransfer jrt:/java.datatransfer
root jdk.httpserver jrt:/jdk.httpserver
root jdk.net jrt:/jdk.net
root java.desktop jrt:/java.desktop
root java.naming jrt:/java.naming
root java.prefs jrt:/java.prefs
root java.net.http jrt:/java.net.http
root jdk.compiler jrt:/jdk.compiler
root java.security.sasl jrt:/java.security.sasl
root jdk.jconsole jrt:/jdk.jconsole
root jdk.attach jrt:/jdk.attach
root java.base jrt:/java.base
root jdk.javadoc jrt:/jdk.javadoc
root jdk.jshell jrt:/jdk.jshell
root java.sql.rowset jrt:/java.sql.rowset
root jdk.sctp jrt:/jdk.sctp
root jdk.jsobject jrt:/jdk.jsobject
root java.management jrt:/java.management
root jdk.unsupported jrt:/jdk.unsupported
root java.smartcardio jrt:/java.smartcardio
root jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
root java.instrument jrt:/java.instrument
root java.security.jgss jrt:/java.security.jgss
root jdk.management jrt:/jdk.management
root jdk.security.auth jrt:/jdk.security.auth
root java.compiler jrt:/java.compiler
root java.scripting jrt:/java.scripting
root jdk.dynalink jrt:/jdk.dynalink
root jdk.unsupported.desktop jrt:/jdk.unsupported.desktop
root jdk.accessibility jrt:/jdk.accessibility
root jdk.jartool jrt:/jdk.jartool
root java.management.rmi jrt:/java.management.rmi
root jdk.security.jgss jrt:/jdk.security.jgss
jdk.security.jgss requires java.security.sasl jrt:/java.security.sasl
(以下略)

システムモジュール内の様々なモジュールがルートとして読み込まれているのがわかる。


互換性が保たれる仕組み

  • システムモジュール内のモジュールは、ほぼ全てルートモジュールになる(モジュールグラフに追加される)
  • 無名モジュールは、モジュールグラフ内の全てのモジュールを requires する

結果、無名モジュールは基本的に全てのモジュールを利用できるようになり、互換性が保たれる。


--add-modules で任意のモジュールをルートに追加する

  • Java 9, 10 の頃は、システムモジュールにあってもルートにならないモジュールがあった
    • Java EE 関係のモジュール
    • java.transaction, java.xml.bind など
    • Java 11 で削除された
  • これらのモジュールに依存しているアプリケーションをクラスパスを使ってビルドすると、コンパイルが通らない
    • 無名モジュールはモジュールグラフ上のモジュールを自動的に requires する
    • 言い替えると、モジュールグラフに存在しないモジュールは requires されない(参照できない)
    • Java EE 関係のモジュールはルートにならなかったので、そのままだと requires されない

jjug-ccc-2018.jpg

  • --add-modules オプションを使うと、観測可能なモジュール内の任意のモジュールをルートモジュールに追加できる
$ javac --add-modules java.xml.bind \
        --classpath ...

jjug-ccc-2018.jpg

  • Java EE 関係のモジュールは Java 11 で削除されたので、 11 以降は普通にサードパーティのライブラリ扱いで利用すればいい
  • クラスパスに jar を追加すれば、アプリケーションのコードからは参照できるようになる(--add-modules は不要)

まとめ

モジュールグラフの作られ方

  1. ルートモジュールを決める
    • コンパイル対象 or --module で指定したモジュール
    • --add-modules で指定したモジュール
  2. ルートモジュールから requires をたどってモジュールグラフを構築する

観測可能なモジュール

  • ルートモジュールおよび requires するモジュールは、観測可能なモジュールでなければならない
    • システムモジュール
    • モジュールパス

他のモジュールのクラスを利用できる条件

  • そのモジュールを requires している
  • そのクラスが所属するパッケージが exports されている

互換の話が入るとちょっとややこしい

  • 無名モジュール
    • クラスパスから読み込んだクラスが所属するモジュール
    • 無名モジュールが起点になると、ルートモジュールの決定方法が変わる
    • 全てのパッケージを exports し、モジュールグラフ上の全てのモジュールを requires

エラーとの関係

前述のルールから外れた設定があると、モジュール絡みのエラーが発生する。

モジュールが見つかりません

  • 観測可能でないモジュールを利用しようとした(コンパイル時)
    • --add-modules で指定したモジュールが存在しない
    • --module で指定したモジュールが存在しない

パッケージ...は表示不可です

パッケージ...はモジュール...で宣言されていますが、エクスポートされていません

  • import しているパッケージが exports されていない

パッケージ...はモジュール...で宣言されていますが、モジュール...に読み込まれていません

  • import しているパッケージが所属するモジュールを requires で読み込んでいない

パッケージ...はモジュール...で宣言されていますが、モジュール・グラフにありません

  • 無名モジュール内で import しているパッケージを含むモジュールが、モジュールグラフ上に存在しない
  • 観測可能なモジュールには存在している

java.lang.module.FindException

  • 観測可能でないモジュールを利用しようとした(実行時)

java.lang.IllegalAccessException

  • 参照できないクラスにリフレクションでアクセスしようとした
    • requires していないモジュール
    • exports されていないパッケージのクラス

より詳細な話

opengl-8080
ただのSE。Java好き。
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした