自己紹介
- 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 とか)
モジュールシステムでできること
- 複数のパッケージを モジュール というカタマリにまとめられる
- モジュール間の依存関係を定義できる
- パッケージ単位で外部への公開・非公開を制御できる
モジュールの定義
module-info.java
module foo { // foo モジュールの定義
exports foo; // foo パッケージの公開
requires bar; // bar モジュールへの依存を宣言
}
-
module-info.java(class)
ファイルをモジュール内のルートに配置 - モジュール名、公開するパッケージ、依存するモジュールなどを宣言できる
モジュールを使った場合の大前提
あるモジュール内のクラスが別のモジュールのクラスを使用するためには、次の条件を満たしている必要がある。
- 対象のクラスが存在するモジュールを直接
requires
している - 対象のクラスが存在するパッケージが
exports
されている
※java.base
モジュールは自動的に requires
されるので明示的な宣言が不要
推移的な requires
module-info.java
module bar {
requires transitive fuga; // transitive 修飾子をつける
}
-
requires
にtransitive
修飾子をつけると、requires
が伝播するようになる - この場合、
bar
をrequires
すると、自動的にfuga
もrequires
したことになる
モジュールグラフ
- モジュール間の関係を表したもの
- コンパイルや JVM 起動時に、このモジュールグラフが作られる
ルートモジュールを決める
- モジュールグラフの起点となるモジュール = ルートモジュール を決める
コンパイル時と JVM 起動時でルートモジュールは異なる
コンパイル時
- コンパイル対象のソース(モジュール)
JVM 起動時
-
--module
(-m
) オプションで指定したモジュール - メインクラスの指定というよりも、先頭のモジュールを指定するためのオプション
module[/mainclass]
解決する初期moduleの名前を指定します。
また、moduleで指定されていない場合は、実行するmainclassの名前を指定します。
java | Java Platform, Standard Editionツール・リファレンス
ルートモジュールから依存関係を辿る
- ルートモジュールが決まったら、
module-info
の内容を確認する - 依存している(
requires
している) モジュールを調べ、モジュールグラフを構築していく
観測可能なモジュール
- モジュールを検索してくる場所
- ルートモジュールおよび
requires
しているモジュールは、 観測可能なモジュール でなければならない - 大きく2つ存在する
- システムモジュール
- モジュールパス
- 厳密には他にも観測可能なモジュールの候補は存在するが、ここでは割愛(詳細は Javadoc などを参照 )
システムモジュール
- 実行環境に組み込まれているモジュール
-
java.base
,java.desktop
,java.sql
などなど - 固定ではない
- jlink で作られたカスタム JRE
- Oracle Java 9, 10 では JavaFX 系のモジュールが含まれていた
-
ModuleFinder.ofSystem()
で一覧を取得可能
モジュールパス
-
--module-path
(-p
) で指定 - システムモジュールに存在しないモジュールは、モジュールパスに追加しなければ
requires
できない- アプリケーション自身
- サードパーティのライブラリ
モジュールグラフの話を整理
- モジュールグラフとは、モジュール間の関係を表したもの
- ルートモジュールを起点にして、
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
しているモジュール-
exports
はto
で公開先のモジュールを制限できる - ルートになるのは、
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
されない
- 無名モジュールはモジュールグラフ上のモジュールを自動的に
-
--add-modules
オプションを使うと、観測可能なモジュール内の任意のモジュールをルートモジュールに追加できる
$ javac --add-modules java.xml.bind \
--classpath ...
- Java EE 関係のモジュールは Java 11 で削除されたので、 11 以降は普通にサードパーティのライブラリ扱いで利用すればいい
- クラスパスに jar を追加すれば、アプリケーションのコードからは参照できるようになる(
--add-modules
は不要)
まとめ
モジュールグラフの作られ方
-
ルートモジュールを決める
- コンパイル対象 or
--module
で指定したモジュール -
--add-modules
で指定したモジュール
- コンパイル対象 or
- ルートモジュールから
requires
をたどってモジュールグラフを構築する
観測可能なモジュール
- ルートモジュールおよび
requires
するモジュールは、観測可能なモジュールでなければならない- システムモジュール
- モジュールパス
他のモジュールのクラスを利用できる条件
- そのモジュールを
requires
している - そのクラスが所属するパッケージが
exports
されている
互換の話が入るとちょっとややこしい
- 無名モジュール
- クラスパスから読み込んだクラスが所属するモジュール
- 無名モジュールが起点になると、ルートモジュールの決定方法が変わる
- 全てのパッケージを
exports
し、モジュールグラフ上の全てのモジュールをrequires
エラーとの関係
前述のルールから外れた設定があると、モジュール絡みのエラーが発生する。
モジュールが見つかりません
- 観測可能でないモジュールを利用しようとした(コンパイル時)
-
--add-modules
で指定したモジュールが存在しない -
--module
で指定したモジュールが存在しない
-
パッケージ...は表示不可です
パッケージ...はモジュール...で宣言されていますが、エクスポートされていません
-
import
しているパッケージがexports
されていない
パッケージ...はモジュール...で宣言されていますが、モジュール...に読み込まれていません
-
import
しているパッケージが所属するモジュールをrequires
で読み込んでいない
パッケージ...はモジュール...で宣言されていますが、モジュール・グラフにありません
- 無名モジュール内で
import
しているパッケージを含むモジュールが、モジュールグラフ上に存在しない - 観測可能なモジュールには存在している
java.lang.module.FindException
- 観測可能でないモジュールを利用しようとした(実行時)
java.lang.IllegalAccessException
- 参照できないクラスにリフレクションでアクセスしようとした
-
requires
していないモジュール -
exports
されていないパッケージのクラス
-