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


自己紹介


  • 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 されていないパッケージのクラス





より詳細な話