Java
jigsaw

モジュールシステムを学ぶ

今まで何となくで使ってた Jigsaw (モジュールシステム)をしっかり学ぶ。

環境

OS

Windows 10

Java

java 10.0.1

モジュールの解決

モジュールの解決 | java.lang.module (Java SE 10 & JDK 10 )

Java 9 で追加されたモジュールシステムでは、複数のパッケージやリソースをモジュールと呼ばれる単位でまとめることができるようになった。

モジュールでは、次のようなことを定義できる1

  • どのパッケージを外部から見えるようにするか
  • 他のどのモジュールに依存するか

これらは module-info.java という専用のファイルを使い、次のように記述する。

module-info.java
module foo {
    exports foo.api;
    requires bar;
}

この例では、 foo というモジュールを定義している。
そして、 exports foo.apifoo.api パッケージを外部に公開し、 requires barbar モジュールに依存していることを宣言している。

コンパイルしたりプログラムを起動すると、この定義情報がまず最初に読み取られる2
そして、各モジュールがどのような関係になっているかが分析される。

これをモジュールの解決 (Resolution)と呼び、非常に重要なステップとなっている。

このモジュールの解決によって得られた情報をもとに、コードの妥当性のチェックやクラスの検索といった処理が行われる。
つまり、モジュールの解決がどのように行われるかを理解しておかないと、モジュール絡みのエラーが発生したときに何が原因なのかあたりをつけることが困難になる。

モジュールグラフ

例えば、次のようなモジュール達が定義されていたとする。

fooモジュールのmodule-info.java
module foo {
    requires bar;
}
barモジュールのmodule-info.java
module bar {
    requires fizz;
    requires buzz;
}

foo は、 bar モジュールに依存し、 bar モジュールは fizz, buzz モジュールに依存している。

これらのモジュールの関係を図にすると、下のような有向グラフができあがる3

jigsaw.jpg

このようにモジュール同士の依存関係を表現したものを、モジュールグラフと呼ぶ。

矢印は requires の関係を表現している。
あるモジュールが他のモジュールを参照するためには、この矢印が直接つながっていなければならない4

つまり、 foo モジュールは bar モジュールと直接つながっているので参照できるが、 fizz, buzz モジュールとはつながっていないので参照はできないということになる。

モジュールの解決周りの問題に遭遇した場合は、このモジュールグラフの構築が意図通りになっているかを調べることで原因が特定しやすくなると思う。

間接エクスポート

foo モジュールは fizz, buzz モジュールを直接 requires していないため、参照することはできない。

しかし、もし barfizz, buzz間接エクスポート(Indirect Exports)している場合は、 foofizz, buzz を参照できる。

barが間接エクスポートしている場合のmodule-info.java
module bar {
    requires transitive fizz;
    requires transitive buzz;
}

requirestransitive をつけている。
これにより、barrequires したモジュールは fizz, buzz モジュールも requires したことになる。

つまり、

jigsaw.jpg

は、

jigsaw.jpg

と同じことになる。

java.base モジュール

標準 API の中にある java.base モジュールは、少し特別扱いとなっている。

このモジュールには java.lang など、 Java を実行するうえで基礎となるパッケージが含まれている。
このため、このモジュールに限っては requires を明示していなくても常に requires している扱いとなる。

つまり、上述のモジュールグラフの図は、 java.base も含めると次のようになる。

jigsaw.jpg

ルート・モジュールの決定

ルート・モジュール | パッケージ java.lang.module

モジュールの解決でモジュールグラフを構築するには、まずはじめにグラフの先頭となるモジュールを決定する。
これをルート・モジュールと呼ぶ。

ルート・モジュールは、コンパイル時と実行時でそれぞれ次のように決まる。

コンパイル時

コンパイル対象のモジュール

実行時

--module (-m) オプションで指定したモジュール

--module の意味

Jigsaw を触りたての頃、 --module は「メインクラスを指定するためのオプション」くらいの認識だった5

しかし、モジュールの解決について勉強しなおすなかで、 java コマンドのリファレンスで次のように説明されていることに気付いた。

java | Oracle Help Center

module[/mainclass]
Specifies the name of the initial module to resolve and, if it isn’t specified by the module, then specifies the name of the mainclass to execute.

【訳】
(モジュールの)解決のために初期モジュールの名前を指定する。そして、もしモジュールによって(メインクラスが)指定されていない場合は、実行するメインクラスの名前を指定する。

メインクラスを指定する目的ももちろんあるが、それ以上に初期モジュール(ルート・モジュール)を決めるという重要な役割を持っているのが、この --module ということを知った。

観測可能なモジュール

観測可能なモジュール | パッケージ java.lang.module

ルート・モジュールが決定したら、次はそのルート・モジュールの module-info.java を見て6依存するモジュールを辿っていく。

このとき、依存するモジュールは次の4つの場所から順番に検索されていく。

  1. --module-source-path で指定されたモジュール(コンパイル時のみ)
  2. --upgrade-module-path で指定されたモジュール(詳細後述)
  3. システムモジュール
    • ランタイムが用意している組み込みのモジュール
    • java.* とか jdk.* のモジュール
      • 固定で決まっているものではない
      • jlink で作られたカスタムのランタイムなら標準の状態よりも少なくなることがありえる
  4. アプリケーションモジュール
    • --module-path で指定した場所に存在するモジュール

これら検索可能な場所に存在するモジュールを総称して、観測可能なモジュール (Observable modules)と呼ぶ。

requires で指定するモジュールは、この観測可能な範囲に存在していなければならない
もし観測可能でないモジュールを requires しようとすると、コンパイルまたは実行時にエラーになる。

システムモジュールを調べる

package sample;

import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;

public class Main {
    public static void main(String[] args) {
        ModuleFinder
            .ofSystem()
            .findAll()
            .stream()
            .map(ModuleReference::descriptor)
            .map(ModuleDescriptor::name)
            .sorted()
            .forEach(System.out::println);
    }
}
実行結果
java.activation
java.base
java.compiler
java.corba
java.datatransfer
java.desktop
java.instrument
java.jnlp
java.logging
java.management
java.management.rmi
java.naming
java.prefs
java.rmi
java.scripting
java.se
java.se.ee
java.security.jgss
java.security.sasl
java.smartcardio
java.sql
java.sql.rowset
java.transaction
java.xml
java.xml.bind
java.xml.crypto
java.xml.ws
java.xml.ws.annotation
javafx.base
javafx.controls
javafx.deploy
javafx.fxml
javafx.graphics
javafx.media
javafx.swing
javafx.web
jdk.accessibility
jdk.aot
jdk.attach
jdk.charsets
jdk.compiler
jdk.crypto.cryptoki
jdk.crypto.ec
jdk.crypto.mscapi
jdk.deploy
jdk.deploy.controlpanel
jdk.dynalink
jdk.editpad
jdk.hotspot.agent
jdk.httpserver
jdk.incubator.httpclient
jdk.internal.ed
jdk.internal.jvmstat
jdk.internal.le
jdk.internal.opt
jdk.internal.vm.ci
jdk.internal.vm.compiler
jdk.internal.vm.compiler.management
jdk.jartool
jdk.javadoc
jdk.javaws
jdk.jcmd
jdk.jconsole
jdk.jdeps
jdk.jdi
jdk.jdwp.agent
jdk.jfr
jdk.jlink
jdk.jshell
jdk.jsobject
jdk.jstatd
jdk.localedata
jdk.management
jdk.management.agent
jdk.management.cmm
jdk.management.jfr
jdk.management.resource
jdk.naming.dns
jdk.naming.rmi
jdk.net
jdk.pack
jdk.packager
jdk.packager.services
jdk.plugin
jdk.plugin.server
jdk.rmic
jdk.scripting.nashorn
jdk.scripting.nashorn.shell
jdk.sctp
jdk.security.auth
jdk.security.jgss
jdk.snmp
jdk.unsupported
jdk.xml.bind
jdk.xml.dom
jdk.xml.ws
jdk.zipfs
oracle.desktop
oracle.net
  • ModuleFinder.ofSystem() メソッドを使えば、システムモジュールを検索する ModuleFinder を取得できる
  • これらのモジュールはランタイムに組み込まれており、 --module-path で指定していなくても requires できる

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

java コマンドのオプションで --show-module-resolution というものが存在する。
これを指定してプログラムを起動すると、モジュールの解決の様子が出力されるようになる。

> java --show-module-resolution 【中略】 -m foo/foo.Main

root foo file:///.../foo/classes/
foo requires bar file:///.../bar/classes/
bar requires buzz file:///.../buzz/classes/
bar requires fizz file:///.../fizz/classes/
java.base binds jdk.localedata jrt:/jdk.localedata
java.base binds java.desktop jrt:/java.desktop
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
java.base binds jdk.zipfs jrt:/jdk.zipfs
java.base binds jdk.crypto.cryptoki jrt:/jdk.crypto.cryptoki
java.base binds jdk.crypto.ec jrt:/jdk.crypto.ec
java.base binds java.naming jrt:/java.naming
java.base binds java.xml.crypto jrt:/java.xml.crypto
java.base binds jdk.crypto.mscapi jrt:/jdk.crypto.mscapi
java.base binds jdk.deploy jrt:/jdk.deploy
java.base binds java.security.sasl jrt:/java.security.sasl
java.base binds java.security.jgss jrt:/java.security.jgss
java.base binds jdk.security.jgss jrt:/jdk.security.jgss
java.base binds java.smartcardio jrt:/java.smartcardio
java.base binds jdk.jlink jrt:/jdk.jlink
java.base binds jdk.javadoc jrt:/jdk.javadoc
java.base binds jdk.jartool jrt:/jdk.jartool
java.base binds jdk.packager jrt:/jdk.packager
java.base binds jdk.jdeps jrt:/jdk.jdeps
java.base binds jdk.compiler jrt:/jdk.compiler
jdk.compiler requires java.compiler jrt:/java.compiler
jdk.jdeps requires jdk.compiler jrt:/jdk.compiler
jdk.jdeps requires java.compiler jrt:/java.compiler
jdk.packager requires java.logging jrt:/java.logging
jdk.packager requires jdk.jlink jrt:/jdk.jlink
jdk.packager requires java.xml jrt:/java.xml
jdk.packager requires java.desktop jrt:/java.desktop
jdk.javadoc requires java.xml jrt:/java.xml
jdk.javadoc requires java.compiler jrt:/java.compiler
jdk.javadoc requires jdk.compiler jrt:/jdk.compiler
jdk.jlink requires jdk.jdeps jrt:/jdk.jdeps
jdk.jlink requires jdk.internal.opt jrt:/jdk.internal.opt
jdk.security.jgss requires java.logging jrt:/java.logging
jdk.security.jgss requires java.security.sasl jrt:/java.security.sasl
jdk.security.jgss requires java.security.jgss jrt:/java.security.jgss
java.security.jgss requires java.naming jrt:/java.naming
java.security.sasl requires java.logging jrt:/java.logging
jdk.deploy requires java.scripting jrt:/java.scripting
jdk.deploy requires java.management jrt:/java.management
jdk.deploy requires jdk.unsupported jrt:/jdk.unsupported
jdk.deploy requires java.naming jrt:/java.naming
jdk.deploy requires java.xml jrt:/java.xml
jdk.deploy requires java.prefs jrt:/java.prefs
jdk.deploy requires java.desktop jrt:/java.desktop
jdk.deploy requires java.rmi jrt:/java.rmi
jdk.deploy requires java.logging jrt:/java.logging
java.xml.crypto requires java.xml jrt:/java.xml
java.xml.crypto requires java.logging jrt:/java.logging
java.naming requires java.security.sasl jrt:/java.security.sasl
jdk.crypto.cryptoki requires jdk.crypto.ec jrt:/jdk.crypto.ec
jdk.security.auth requires java.naming jrt:/java.naming
jdk.security.auth requires java.security.jgss jrt:/java.security.jgss
java.desktop requires java.xml jrt:/java.xml
java.desktop requires java.datatransfer jrt:/java.datatransfer
java.desktop requires java.prefs jrt:/java.prefs
java.prefs requires java.xml jrt:/java.xml
java.rmi requires java.logging jrt:/java.logging
java.naming binds jdk.naming.dns jrt:/jdk.naming.dns
java.naming binds jdk.naming.rmi jrt:/jdk.naming.rmi
java.scripting binds jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
java.management binds java.management.rmi jrt:/java.management.rmi
java.management binds jdk.management jrt:/jdk.management
java.management binds jdk.management.cmm jrt:/jdk.management.cmm
java.management binds jdk.internal.vm.compiler.management jrt:/jdk.internal.vm.compiler.management
java.management binds jdk.management.jfr jrt:/jdk.management.jfr
java.desktop binds jdk.accessibility jrt:/jdk.accessibility
java.compiler binds jdk.compiler jrt:/jdk.compiler
java.compiler binds jdk.javadoc jrt:/jdk.javadoc
java.datatransfer binds java.desktop jrt:/java.desktop
jdk.accessibility requires java.desktop jrt:/java.desktop
jdk.management.jfr requires jdk.management jrt:/jdk.management
jdk.management.jfr requires jdk.jfr jrt:/jdk.jfr
jdk.management.jfr requires java.management jrt:/java.management
jdk.internal.vm.compiler.management requires java.management jrt:/java.management
jdk.internal.vm.compiler.management requires jdk.internal.vm.compiler jrt:/jdk.internal.vm.compiler
jdk.internal.vm.compiler.management requires jdk.management jrt:/jdk.management
jdk.internal.vm.compiler.management requires jdk.internal.vm.ci jrt:/jdk.internal.vm.ci
jdk.management.cmm requires java.management jrt:/java.management
jdk.management.cmm requires jdk.management jrt:/jdk.management
jdk.management requires java.management jrt:/java.management
java.management.rmi requires java.rmi jrt:/java.rmi
java.management.rmi requires java.naming jrt:/java.naming
java.management.rmi requires java.management jrt:/java.management
jdk.scripting.nashorn requires java.logging jrt:/java.logging
jdk.scripting.nashorn requires java.scripting jrt:/java.scripting
jdk.scripting.nashorn requires jdk.dynalink jrt:/jdk.dynalink
jdk.naming.rmi requires java.rmi jrt:/java.rmi
jdk.naming.rmi requires java.naming jrt:/java.naming
jdk.naming.dns requires java.naming jrt:/java.naming
jdk.internal.vm.compiler requires jdk.management jrt:/jdk.management
jdk.internal.vm.compiler requires jdk.internal.vm.ci jrt:/jdk.internal.vm.ci
jdk.internal.vm.compiler requires jdk.unsupported jrt:/jdk.unsupported
jdk.internal.vm.compiler requires java.instrument jrt:/java.instrument
jdk.internal.vm.compiler requires java.management jrt:/java.management
jdk.dynalink requires java.logging jrt:/java.logging
jdk.dynalink binds jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
jdk.internal.vm.ci binds jdk.internal.vm.compiler jrt:/jdk.internal.vm.compiler

なにやら大量に出力されているが、とりあえず先頭の数行だけを確認してみる。

root foo file:///.../foo/classes/
foo requires bar file:///.../bar/classes/
bar requires buzz file:///.../buzz/classes/
bar requires fizz file:///.../fizz/classes/

root foo が、 foo モジュールがルート・モジュールとして処理されたことを意味している。
そして、 foo から順番に requires を辿って各モジュールが解決していることが読み取れる。

ちなみに、 java.base は必ず requires される扱いなので、 foo requires java.base というログは出力されないようになっている。

その他の出力

foo から始まるモジュールグラフを作り終わったあとも、ログは大量に出力されている。

これらは java.base が読み込まれたことによって出力されている。

java.base binds jdk.localedata jrt:/jdk.localedata
java.base binds java.desktop jrt:/java.desktop
java.base binds jdk.charsets jrt:/jdk.charsets
java.base binds java.logging jrt:/java.logging
...

まず最初に java.base binds **** という記述が十数行出力されている。
この binds は、 ServiceLoader におけるサービスプロバイダが解決されたときに出力されている7

java.base は他のモジュールを requires することはないが、 uses の宣言は大量に存在している(ServiceLoader は使っている)。

java.baseのmodule-info.java
    ...
    uses java.lang.System.LoggerFinder;
    uses java.net.ContentHandlerFactory;
    uses java.net.spi.URLStreamHandlerProvider;
    uses java.nio.channels.spi.AsynchronousChannelProvider;
    ...

例えば、 java.net.ContentHandlerFactory の実装は java.desktop モジュールで提供されている。

java.desktopモジュールのmodule-info.java
    provides java.net.ContentHandlerFactory with sun.awt.www.content.MultimediaContentHandlers;

この結果、

  1. java.baseuses しているサービスのプロバイダーとなるモジュールを探してロードする
  2. プロバイダーとなったモジュールが別のモジュールを requires しているのでそちらもロードする

という処理が再帰的に繰り返されて、上記のような長いログ出力になっている。

ちなみに、 java.desktop モジュールは、ロードはされているがモジュールグラフ上には存在しない。
なので、 java.desktop 内にあるクラスを foo モジュールなどで import しようとするとエラーになる。
import できるのは、あくまで requires で依存関係を定義し、モジュールグラフ上で依存関係が直接つながっているモジュールに限られる。

後方互換の話が入るとややこしくなっていく

ここまでは、コンパイルや実行の対象が全てモジュール化されている場合の話。

正直これだけなら、割と簡単な話だと思う。
要はルート・モジュールを決めて、そこから requiresを辿ってグラフを作るというだけの話。

ここに後方互換性のために存在する無名モジュールや自動モジュールが関わってくると、特別ルールが追加されて話がややこしくなっていく。

クラスパスの問題点とモジュールシステムの Reliable configuration

モジュールシステムが導入された背景の1つとして、クラスパスの持つ脆さがある。

クラスパスには、アーティファクトを区別する仕組みはなく、型の検索は全てのアーティファクトが入ったクラスパス内から行われるようになっていた。
このため、アーティファクトの単位で事前に不足があるかどうかを判定できなかったり、異なるアーティファクトに同じパッケージが含まれていても検知する仕組みはなかった8

モジュールシステムでは、プログラムの集合をモジュールという単位で定義し、それぞれの依存関係を管理できる。これにより、事前にモジュール単位で不足を検知できるようになった。
また、あるモジュールが別のモジュールのパッケージを読み込むときは、そのパッケージを含むモジュールは1つに定まることが保証されるようになった(requires している複数の異なるモジュールに同じパッケージがあるとエラーになる)。

このようなモジュールシステムが持つ高い信頼性を Reliable configuration (信頼できる構成) と言う。
これは、モジュールシステムの重要な特徴の1つとなっている。

モジュールグラフは、この Reliable configuration を実現するための基礎となっている。

モジュールのバージョン管理は範囲外

同じモジュールのバージョン違いがモジュールパスに含まれていた場合、モジュールシステムはそれを検知しない(最初に見つかったモジュールが優先される)。

Non-Requirements | Java Platform Module System: Requirements

バージョンの選択については Maven のような既存のツールに任せることになっている。
そしてモジュールシステムは、バージョン選択が済んでいる前提で、残ったモジュールたちの検証に注力する方針となっている。

クラスパスは残っているが全く同じではない

クラスパスを捨てて全てをモジュール化できれば、この Reliable configuration を享受できる。
ところが、実際は後方互換を捨てるわけにはいかないので、クラスパスは Java 8 以前と同じ様な感じで使えるようになっている。

しかし、 -classpath オプションは残っているものの、裏ではクラスパスで指定されたクラス達もモジュールシステム上で動くことになる。

従って、クラスパスを利用していればモジュールシステムは知らなくていいというわけではない。
クラスパスを利用していてもモジュール絡みのエラーが出る可能性は十分ある。
そういうときにモジュールシステムのことを知らないと、問題の解決に苦労するかもしれない。

無名モジュール

クラスパスから読み込まれたパッケージや型は、無名モジュール (Unnamed module) という特別なモジュールに属するようになる。

無名モジュールはモジュール定義 (module-info.java) を持つことができないため、 requiresexports に関する定義を指定することはできない。

代わりに、無名モジュールはデフォルトで次のように振る舞う。

  • 無名モジュール内の全てのパッケージは exports されている扱いになる
  • 無名モジュールは、モジュールグラフ内の全てのモジュールを requires している扱いになる

名前付きモジュールから無名モジュールは参照できない

無名モジュールは全てのパッケージを exports していることになるが、それを名前付きモジュール9から参照することはできない。

これは、前述の Reliable configuration を維持するため、意図的にそういう仕様になっている。
もし名前付きモジュールから無名モジュール(クラスパス上のクラス)を参照できるようにすると、クラスパスの問題が再発することになり、せっかくモジュールシステムを導入した意味がなくなってしまう。

jigsaw.jpg

同じパッケージが、名前付きモジュールと無名モジュールの両方に存在した場合

同じパッケージが名前付きモジュールと無名モジュールの両方に存在した場合、名前付きモジュールに存在するパッケージが優先される
このとき、エラーや警告は出ない

jigsaw.jpg

例えば上図のように foo パッケージが名前付きモジュールと無名モジュールの両方に存在したとする。

このとき、 foo パッケージ内のクラスにアクセスしようとすると、名前付きモジュールの方が優先される。このため、無名モジュール側に存在する foo.FooService にはアクセスすることができない。無理にアクセスしようとすると、コンパイル時なら型が見つからずエラーになり、実行時なら NoClassDefFoundError が発生する。

意図せずこのような状態になっても警告は出ないため、モジュールグラフとクラスパスがどうなっているかを把握できていないと原因が分からず困ることになる10

無名モジュールが起点となった場合のルート・モジュールの決定(2018-11-04 更新)

通常のモジュールをコンパイルした場合は対象のモジュールが、実行時は --module で指定したモジュールがルート・モジュールとなった。

一方、コンパイル対象のソースに module-info.java が含まれていなかったり、実行時のメインクラスがクラスパスからロードされた場合――すなわち無名モジュールを起点としてコンパイルやプログラムの起動が行われた場合、ルート・モジュールの決定方法は大きく変化する。

Java 11 以降

Java 11 で動作を確認したら、 Java 10 までとは違う動きになっていたので追記。

先に結論を書くと、 Java 11 からは次のルールでルートモジュールが決まる。

  • --upgrade-module-path で指定した場所に存在する全てのモジュール
  • システムモジュール内に存在し、 to による公開先の制限なしで最低1つはパッケージを exports しているモジュール

java.se どうのこうのの話はなくなっている。

OpenJDK 11 で、無名モジュールを起点とし --show-module-resolution オプションを指定して起動すると、次のように出力される。

root jdk.management.jfr jrt:/jdk.management.jfr
root java.rmi jrt:/java.rmi
root java.sql jrt:/java.sql
root jdk.jdi jrt:/jdk.jdi
root java.xml.crypto jrt:/java.xml.crypto
root java.transaction.xa jrt:/java.transaction.xa
root java.logging jrt:/java.logging
root jdk.xml.dom jrt:/jdk.xml.dom
root java.xml jrt:/java.xml
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 jdk.sctp jrt:/jdk.sctp
root jdk.jsobject jrt:/jdk.jsobject
root java.management jrt:/java.management
root java.sql.rowset jrt:/java.sql.rowset
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 java.compiler jrt:/java.compiler
root jdk.security.auth jrt:/jdk.security.auth
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
(以下略)

よく見ると、 java.se モジュールが読み込まれていない。

修正が入ったコミットが次のコミットで(DefaultRoots.javacompute(ModuleFinder, ModuleFinder) メソッド)、
jdk/jdk11: 6cc2dc161c64

対応する Issue が次になる。
[JDK-8197532] Re-examine policy for the default set of modules when compiling or running code on the class path - Java Bug System

Java 11 で Java EE 系のモジュールが削除されたことで、ルートモジュールの解決が単純になったことが理由となっている。
java.se.ee モジュールが削除されたことで、単純にシステムモジュール内のモジュールを全てルートにすればよくなったと思われる。

Java 10 まで

ルート・モジュールには、大きく次の2つが読み込まれる。

  1. ランタイムに java.se モジュールが存在する場合は、 java.se モジュールがルートとなる。
    ランタイムに java.se モジュールが存在しない場合は、次の条件をすべて満たすモジュールがルート・モジュールとして読み込まれる
    • システムモジュール、または --upgrade-module-path で指定した場所に存在する
    • モジュール名が java.* にマッチする
    • to で公開先を限定していないパッケージが、 最低1つは exports されている
  2. さらに、次の条件をすべて満たすモジュールもルート・モジュールとして読み込まれる
    • システムモジュール、または --upgrade-module-path で指定した場所に存在する
    • モジュール名が java.* にマッチしない
    • to で公開先を限定していないパッケージが、 最低1つは exports されている

つまり、ランタイムが提供するモジュールをできる限り根こそぎルートとして登録するようになっている。

これによって作成されるモジュールグラフは、ルート・モジュールだらけになる。
ここに、無名モジュールはモジュールグラフ上のすべてのモジュールを requires している扱いになるという前述したルールが適用されることで、無名モジュールはランタイムが提供するほとんどのモジュールにアクセスできるようになる。

結果、 Java 8 以前に作られたプログラムでも、クラスパスに入れて実行すれば(ほぼ)問題なく動作するようになっている11

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

実際に、先程モジュールの解決を確認した実装を、今度はクラスパスに入れて起動してみる。

> java --show-module-resolution -cp 【中略】 foo.Main

root jdk.management.jfr jrt:/jdk.management.jfr
root jdk.jdi jrt:/jdk.jdi
root javafx.web jrt:/javafx.web
root jdk.xml.dom jrt:/jdk.xml.dom
root jdk.jfr jrt:/jdk.jfr
root jdk.packager.services jrt:/jdk.packager.services
root jdk.httpserver jrt:/jdk.httpserver
root javafx.base jrt:/javafx.base
root jdk.net jrt:/jdk.net
root javafx.controls jrt:/javafx.controls
root jdk.management.resource jrt:/jdk.management.resource
root java.se jrt:/java.se
root jdk.compiler jrt:/jdk.compiler
root jdk.jconsole jrt:/jdk.jconsole
root jdk.attach jrt:/jdk.attach
root jdk.management.cmm jrt:/jdk.management.cmm
root jdk.javadoc jrt:/jdk.javadoc
root jdk.jshell jrt:/jdk.jshell
root jdk.sctp jrt:/jdk.sctp
root jdk.jsobject jrt:/jdk.jsobject
root oracle.desktop jrt:/oracle.desktop
root javafx.swing jrt:/javafx.swing
root jdk.packager jrt:/jdk.packager
root jdk.unsupported jrt:/jdk.unsupported
root jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
root oracle.net jrt:/oracle.net
root jdk.management jrt:/jdk.management
root javafx.graphics jrt:/javafx.graphics
root jdk.security.auth jrt:/jdk.security.auth
root javafx.fxml jrt:/javafx.fxml
root jdk.dynalink jrt:/jdk.dynalink
root javafx.media jrt:/javafx.media
root jdk.accessibility jrt:/jdk.accessibility
root jdk.jartool jrt:/jdk.jartool
root jdk.security.jgss jrt:/jdk.security.jgss
...

root が大量に出力されていることから、多くのモジュールがルート・モジュールとして読み込まれていることがわかる。

実行した環境では java.se モジュールが存在したため、 java.* にマッチするモジュールは java.se のみがルートとなっている。
一方 javafx.*jdk.* は、システムモジュールに存在したモジュールが軒並みルート・モジュールとして読み込まれている。

いくつか、システムモジュールにあったのにルートになっていないモジュールが存在する。
例えば jdk.aot がルートになっていない。このモジュールの module-info.java を確認してみると、次のようになっている。

jdk.aotのmodule-info.java
module jdk.aot {
    requires jdk.internal.vm.ci;
    requires jdk.internal.vm.compiler;
    requires jdk.management;
}

exports されているパッケージが存在しない。

よって、前述の条件にあった「to で公開先を限定していないパッケージが、 最低1つは exports されている」を満たしていないため、対象からは除外されているものと思われる(確認はしていないが、おそらく他もそうなのだろう)。

java.se が読み込まないモジュールたち

無名モジュールを起点とした場合、ランタイムに用意されているほとんどのモジュールが無名モジュールから参照できるようになる。
しかし、すべてのモジュールが参照できるようになっているかというと、例外がある。

以下のモジュールたちは、システムモジュールにはあるが java.se からは間接エクスポートされていない。

モジュール 説明
java.activation 非推奨(削除予定)
java.corba 非推奨(削除予定)
java.jnlp Java Web Start 関係
java.se.ee 非推奨(削除予定)
java.smartcardio スマート・カード入出力API
java.transaction 非推奨(削除予定)
java.xml.bind 非推奨(削除予定)
java.xml.ws 非推奨(削除予定)
java.xml.ws.annotation 非推奨(削除予定)

非推奨(削除予定)となっているモジュールは、全て Java EE から Java SE に一部が取り込まれたものの、結局 Java SE からは削除されることになった API たちになる。
これらは Java 11 で完全に削除されるので、 Java 9 の時点ですでに非推奨となっている。

これらのモジュールは、 java.se がルートになっただけではモジュールグラフには現れない。
そのため、もし無名モジュール内のクラスがこれらのモジュールに依存している場合、コンパイルや実行ができない。

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

コンパイルや実行時のときに --add-modules というオプションを指定できる。
このオプションを使うと、観測可能な範囲にある任意のモジュールをルート・モジュールとして指定できる。

このオプションを使えば、前述のデフォルトではモジュールグラフに読み込まれないモジュールも強制的にルート・モジュールにできるので、無名モジュールから参照できるようになる。

よく、Java 9 で非推奨となった前述のモジュールを使っている場合の対応方法として --add-modules を指定する方法が紹介されるが、こういう仕組になっている。

実際に、 --add-modules を追加してルート・モジュールがどう解決されるか確認してみる。

>java --show-module-resolution --add-modules java.transaction,java.xml.bind -cp 【中略】 foo.Main

root jdk.management.jfr jrt:/jdk.management.jfr
root jdk.jdi jrt:/jdk.jdi
root javafx.web jrt:/javafx.web
root jdk.xml.dom jrt:/jdk.xml.dom
root jdk.jfr jrt:/jdk.jfr
root jdk.packager.services jrt:/jdk.packager.services
root jdk.httpserver jrt:/jdk.httpserver
root javafx.base jrt:/javafx.base
root jdk.net jrt:/jdk.net
root javafx.controls jrt:/javafx.controls
root jdk.management.resource jrt:/jdk.management.resource
root java.se jrt:/java.se
root jdk.compiler jrt:/jdk.compiler
root jdk.jconsole jrt:/jdk.jconsole
root jdk.management.cmm jrt:/jdk.management.cmm
root jdk.attach jrt:/jdk.attach
root jdk.javadoc jrt:/jdk.javadoc
root java.transaction jrt:/java.transaction ★ここと
root jdk.jshell jrt:/jdk.jshell
root jdk.jsobject jrt:/jdk.jsobject
root jdk.sctp jrt:/jdk.sctp
root javafx.swing jrt:/javafx.swing
root oracle.desktop jrt:/oracle.desktop
root jdk.unsupported jrt:/jdk.unsupported
root jdk.packager jrt:/jdk.packager
root jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
root java.xml.bind jrt:/java.xml.bind ★ここ
root oracle.net jrt:/oracle.net
root jdk.management jrt:/jdk.management
root jdk.security.auth jrt:/jdk.security.auth
root javafx.graphics jrt:/javafx.graphics
root javafx.fxml jrt:/javafx.fxml
root jdk.dynalink jrt:/jdk.dynalink
root jdk.accessibility jrt:/jdk.accessibility
root javafx.media jrt:/javafx.media
root jdk.jartool jrt:/jdk.jartool
root jdk.security.jgss jrt:/jdk.security.jgss
...

--add-modulesjava.transactionjava.xml.bind を指定した結果、確かにルート・モジュールとして新たに読み込まれていることが確認できる。

削除予定のモジュールに関してはあくまで一時しのぎ

--add-modules を使用すると、 Java 9 で非推奨となったモジュールを利用していてもコンパイルや実行が引き続き可能となる。

しかし、この方法はあくまで一時しのぎの対処療法でしかない。
これらのモジュールは、 Java 11 で削除される予定なので、そうなるとこれらのモジュールは観測可能な範囲からも存在しなくなってしまう。

--add-modules は、あくまで観測可能な範囲に存在するモジュールを、強制的にルート・モジュールに追加するオプションでしかない。
モジュールがランタイムから削除されて観測可能な範囲からいなくなってしまうと、この方法では対処できなくなってしまう。

根本的に対処するには、ランタイムとは別にこれらのモジュールを含むアーティファクトを用意して、それをモジュールパスに追加する必要がある(もしくは依存を止めるか)。

詳しくは Java 9 で deprecated になったモジュールによる例外発生の問題にちゃんと対処したい - k11i.biz が非常に参考になる。

--add-modules に指定できる特殊な値

普通、 --add-modules にはモジュール名を指定する。
しかし、モジュール名とは別に次の3つの特殊な値を指定することもできる。

ALL-DEFAULT

  • 無名モジュールが起点となった場合と同じ要領でルート・モジュールが追加される

ALL-SYSTEM

  • システムモジュールに存在するすべてのモジュールがルートになる
  • テストのときとかに使うと便利らしい(よくわかってない)
  • 当然多くのモジュールが読み込まれてしまうので、通常は ALL-DEFAULT の利用が推奨される

ALL-MODULE-PATH

  • モジュールパスに存在するすべてのモジュールがルートになる
  • Maven のような依存関係を解決できるツールを使っている場合に利用することが想定されているっぽい
  • Maven は依存するライブラリを過不足なく集めてくるので、モジュールパスに存在する = 全部必要なモジュールということになる
    • こっちのほうが、クラスの検索とかが早くなったりするのだろうか?(よくわかってない)
  • 自動モジュールをまとめてルートに設定したいときにも便利らしい(よくわかってない)

これらは、例えば --add-modules ALL-MODULE-PATH のような感じで指定する。

自動モジュール

無名モジュールは、主に自分のアプリケーションがモジュールに対応していない場合に利用することになる。

一方、自分のアプリケーションはモジュール化しているが、使用しているサードパーティのライブラリがモジュールに対応していないような場合に関係してくるのが、自動モジュール (Automatic modules)になる。

モジュール化されていない(module-info.class が含まれていない)アーティファクトをモジュールパスに入れて利用した場合、そのアーティファクトは自動モジュールとして認識される。

自動モジュールの名前

自動モジュールのモジュール名は、次の優先順位で自動的に決定される。

  1. MANIFEST.MFAutomatic-Module-Name で指定された名前
  2. jar ファイル名から自動的に解決された名前12

自分のアプリケーションで自動モジュールを requires する場合は、ここで決定されたモジュール名を指定する。

自動モジュールの requires と exports の扱い

自動モジュールも、無名モジュールと同じで明示的に requiresexports を定義することはできない。

そのため、自動モジュールは次のように振る舞うようになっている。

  • すべてのパッケージを exports している扱いになる
  • モジュールグラフ上のすべてのモジュールを requires している扱いになる

つまり、 requiresexports については無名モジュールと同じ扱いになっている。

それ以外は、普通のモジュールと同じように requires の対象として指定できるし、パッケージの重複を検出することもできる。

自動モジュールを含めたモジュールグラフのイメージ

例えば、自動モジュールなしの状態でグラフが下図のようになっていたとする。

jigsaw.jpg

ここに、仮に automatic という名前の自動モジュールを追加すると、グラフは次のようになる。

jigsaw.jpg

さらに、自動モジュールは他の普通の名前付きモジュールから requires できるので、次のように依存関係を追加することができる。

jigsaw.jpg

--upgrade-module-path でモジュールを差し替える

例えば、次のようなソースが存在したとする。

フォルダ構成
`-src/
  |-module-info.java
  `-sample/
    `-Main.java
module-info.java
module sample {
    requires java.transaction;
}
Main.java
package sample;

import javax.transaction.Status;

public class Main {
    public static void main(String[] args) {
        System.out.println("Status.STATUS_ACTIVE = " + Status.STATUS_ACTIVE);
    }
}

java.transaction モジュールを requires し、 javax.transaction.Status に宣言されている定数を出力している。

これをコンパイルしようとすると、次のようになる(警告メッセージは除外している)。

>javac -d classes src\module-info.java src\sample\Main.java
src\sample\Main.java:3: エラー: シンボルを見つけられません
import javax.transaction.Status;
                        ^
  シンボル:   クラス Status
  場所: パッケージ javax.transaction

    requires java.transaction;
                 ^
src\sample\Main.java:7: エラー: シンボルを見つけられません
        System.out.println("Status.STATUS_ACTIVE = " + Status.STATUS_ACTIVE);
                                                       ^
  シンボル:   変数 Status
  場所: クラス Main
エラー2個

Status が見つからずにエラーになっている。

これは、 Java SE 環境に存在する java.transaction モジュールが使用されているために発生している。

Java SE にある java.transaction モジュールが持つ javax.transaction パッケージは、次のようになっている。

javax.transaction (Java SE 10 & JDK 10 )

例外クラス3つだけを含み、 Status クラスは存在しない。
一方、本家 Java EE の方の javax.transaction パッケージは次のようになっている。

javax.transaction (Java(TM) EE 8 Specification APIs)

Status クラスもちゃんと存在している。

コンパイルを通すためには、本家 Java EE の jar を持ってくる必要がある。
ということで、 Java EE 版の jar を落としてきて再度コンパイルしてみる。

>javac -d classes -p lib\javax.transaction-api-1.3.jar src\module-info.java src\sample\Main.java
src\sample\Main.java:3: エラー: シンボルを見つけられません
import javax.transaction.Status;
                        ^
  シンボル:   クラス Status
  場所: パッケージ javax.transaction

    requires java.transaction;
                 ^
src\sample\Main.java:7: エラー: シンボルを見つけられません
        System.out.println("Status.STATUS_ACTIVE = " + Status.STATUS_ACTIVE);
                                                       ^
  シンボル:   変数 Status
  場所: クラス Main
エラー2個

変わらない。

これは、おそらくモジュールパスよりもシステムモジュールの java.transaction モジュールが優先されているため、結局同じエラーになっているものと思われる13

この場合、システムモジュールの java.transaction をなんとかしなければならない。

1つの解決方法として、システムモジュールの java.transaction を別の内容で差し替える方法がある。
そのためのオプションが --upgrade-module-path で、次のように指定する。

>javac -d classes --upgrade-module-path lib\javax.transaction-api-1.3.jar src\module-info.java src\sample\Main.java

差し替えるモジュールのアーティファクトの場所を、モジュールパスの代わりに --upgrade-module-path に指定する。
--upgrade-module-path に指定したモジュールはシステムモジュールよりも優先されるため、システムモジュールの java.transaction ではなく、 javax.transaction-api-1.3.jar が持つ java.transaction モジュールが使用されるようになっている。

--limit-modules でモジュールを制限する

無名モジュールを起点とした場合、多くのモジュールがロードされる。

必要ないモジュールのロードを止めたい場合や、デバッグ目的でロードするモジュールの数を絞りたい場合は、 --limit-modules オプションが利用できる。

>java --show-module-resolution --limit-modules java.sql,java.management.rmi -cp classes foo.Main

root java.rmi jrt:/java.rmi
root java.sql jrt:/java.sql
root java.naming jrt:/java.naming
root java.logging jrt:/java.logging
root java.xml jrt:/java.xml
root java.management jrt:/java.management
root java.security.sasl jrt:/java.security.sasl
root java.management.rmi jrt:/java.management.rmi
root java.base jrt:/java.base
java.management.rmi requires java.rmi jrt:/java.rmi
java.management.rmi requires java.management jrt:/java.management
java.management.rmi requires java.naming jrt:/java.naming
java.security.sasl requires java.logging jrt:/java.logging
java.naming requires java.security.sasl jrt:/java.security.sasl
java.sql requires java.logging jrt:/java.logging
java.sql requires java.xml jrt:/java.xml
java.rmi requires java.logging jrt:/java.logging
java.management binds java.management.rmi jrt:/java.management.rmi
java.base binds java.logging jrt:/java.logging
java.base binds java.management jrt:/java.management
java.base binds java.naming jrt:/java.naming
java.base binds java.security.sasl jrt:/java.security.sasl

--limit-modules を指定すると、そのモジュールと依存するモジュールだけが読み込まれるようになる。

上記例では java.sqljava.management.rmi だけに絞っている。

ところで java.management.rmi requires java.naming と出力されていて java.management.rmijava.naming に依存していることになっている。
しかし、 java.management.rmi の Javadoc を見ても java.naming の記述は存在しない。

試しに module-info.java を見てみたが、次のようになっていた。

java.management.rmiのmodule-info.java
module java.management.rmi {
    requires java.naming;
    requires transitive java.management;
    requires transitive java.rmi;
    ...
}

ちゃんと java.namingrequires されている。
Javadoc に記載される情報に、 transitive がついていないモジュールの情報は載らないということだろうか?

モジュールレイヤー

モジュールレイヤーは、モジュールグラフ内の各モジュールとクラスローダーとのマッピングを定義している。

JVM を起動すると、必ず1つのモジュールレイヤーが作成される。
これをブートレイヤーと呼び、 ModuleLayer.boot() で取得できる。

Main.java
package sample;

public class Main {
    public static void main(String[] args) {
        ModuleLayer bootLayer = ModuleLayer.boot();

        ClassLoader javaBaseModuleClassLoader = bootLayer.findLoader("java.base");
        System.out.println("java.base module ClassLoader => " + javaBaseModuleClassLoader);

        ClassLoader javaSqlModuleClassLoader = bootLayer.findLoader("java.compiler");
        System.out.println("java.sql module ClassLoader => " + javaSqlModuleClassLoader.getName());

        ClassLoader sampleModuleClassLoader = bootLayer.findLoader("sample");
        System.out.println("sample module ClassLoader => " + sampleModuleClassLoader.getName());
    }
}
実行結果
java.base module ClassLoader => null
java.sql module ClassLoader => platform
sample module ClassLoader => app
  • ModuleLayer.findLoader(String) で、指定したモジュールにマッピングされた ClassLoader を取得できる
  • ブートストラップクラスローダーは通常 null で表されるのでjava.base のクラスローダーも null になる
  • platform はプラットフォームクラスローダー、 app はシステムクラスローダー(アプリケーションクラスローダー)を表している

いつ使うものか?

  • 普通に Java アプリケーションを作る場合は、このモジュールレイヤーを意識することはほぼない
  • モジュールレイヤーが関わってくるのは、複数のアプリケーションをホストするソフトウェアを作るような場合になる
    • もしくは、 IDE やテストフレームワークのプラグイン機構とかにも使えるとかなんとか(よくわかってない)
  • モジュールレイヤーは、動的に作成したり階層構造にすることができる
  • これは、ちょうど Tomcat のようなコンテナ機能を持つアプリケーションが、デプロイされたアプリケーションごとにクラスローダーを分けるような感じと似ている
  • つまり、ホストするアプリケーションごとにモジュールレイヤーを分けるような使い方ができる
  • モジュールレイヤーを分けて利用するようなアプリケーションを今後使うようになったときに、この辺の話が関わってくるのかもしれない

モジュールレイヤーを作成する

実際にモジュールレイヤーを作成してみる。

フォルダ構成
|-foo/
| |-classes/ <---- foo モジュールのコンパイル結果
| `-src/
|   |-module-info.java
|   `-foo/
|     `-Foo.java
|
|-classes/   <---- sampleモジュールのコンパイル結果
`-src/
  |-module-info.java
  `-sample/
    `-Main.java
fooモジュールのmodule-info.java
module foo {
    exports foo;
}
Foo.java
package foo;

public class Foo {
    public void hello() {
        System.out.println("foooooooo!!!!");
    }
}
sampleモジュールのmodule-info.java
module sample {
}
Main.java
package sample;

import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.reflect.Method;
import java.nio.file.Paths;
import java.util.Set;

public class Main {
    public static void main(String[] args) throws Exception {
        ModuleLayer bootLayer = ModuleLayer.boot();

        // foo をルート・モジュールとしたモジュールグラフ(Configuration)を作成
        ModuleFinder moduleFinder = ModuleFinder.of(Paths.get("foo/classes"));
        ModuleFinder emptyFinder = ModuleFinder.of();
        Set<String> roots = Set.of("foo");

        Configuration bootConfiguration = bootLayer.configuration();
        Configuration newConfiguration = bootConfiguration.resolve(moduleFinder, emptyFinder, roots);

        // 新しい ModuleLayer を作成
        ModuleLayer newLayer = bootLayer.defineModulesWithOneLoader(newConfiguration, ClassLoader.getSystemClassLoader());

        // foo モジュールから Foo クラスをロードして hello() メソッドを実行
        ClassLoader fooClassLoader = newLayer.findLoader("foo");
        Class<?> fooClass = fooClassLoader.loadClass("foo.Foo");
        Object fooInstance = fooClass.getConstructor().newInstance();
        Method helloMethod = fooClass.getMethod("hello");
        helloMethod.invoke(fooInstance);
    }
}
実行結果
> java -p classes -m sample/sample.Main
foooooooo!!!!

Configuration を作成する

Main.java
// foo をルート・モジュールとしたモジュールグラフ(Configuration)を作成
ModuleFinder moduleFinder = ModuleFinder.of(Paths.get("foo/classes"));
ModuleFinder emptyFinder = ModuleFinder.of();
Set<String> roots = Set.of("foo");

Configuration bootConfiguration = bootLayer.configuration();
Configuration newConfiguration = bootConfiguration.resolve(moduleFinder, emptyFinder, roots);
  • モジュールレイヤーを作成するには、まずはじめに Configuration を作成する
  • Configuration はモジュールの解決の結果――――すなわちモジュールグラフを保持している
  • Configuration を作成するには、大きく次の3つを用意する
    • 親となる Configuration
    • モジュールを検索する ModuleFinder
    • ルート・モジュール名のコレクション
  • Configuration は階層構造を取るようになっており、新しく作成するときは親の Configuration が必要となる
    • 何もないときはブートレイヤーの Configuration を使用する(ModuleLayer.boot().configuration() で取得できる)
  • ModuleFinder はモジュールを検索する機能を提供する
    • ModuleFinder.of(Path...) で作成した ModuleFinder は、 Path で指定した場所からモジュールを検索する
    • Configuration.resolve(ModuleFinder, ModuleFinder, Set<String>) では、 beforeafter の2つを指定する
      • モジュールを検索するときに、まず beforeModuleFinder でモジュールが検索される
      • before で見つからなければ、次は親の Configuration が検索される
      • それでも見つからない場合は afterModuleFinder が検索される
  • ルート・モジュール名のコレクションは、モジュールグラフを作成するときのルート・モジュール名を指定するものになる

ModuleLayer を作成する

Main.java
// 新しい ModuleLayer を作成
ModuleLayer newLayer = bootLayer.defineModulesWithOneLoader(newConfiguration, ClassLoader.getSystemClassLoader());
  • ModuleLayer に用意されたメソッドに上で作成した Configuration を渡すことで、新しい ModuleLayer を作成できる
  • ModuleLayer も親のレイヤーを指定する必要があり、特にない場合はブートレイヤーを利用する
  • ModuleLayer を作成するためのメソッドは、大きく以下の3種類が存在する
  • defineModules(Configuration, Function<String, ClassLoader>)
    • 第二引数の Function は、モジュール名を受け取って、そのモジュールに対応する ClassLoader を返す実装を渡す
  • defineModulesWithOneLoader(Configuration, ClassLoader)​
    • これは、モジュールグラフ内の全てのモジュールに対して、1つの新しいクラスローダーが割り当てられる
    • クラスローダーは ModuleLayer が内部で独自のものを作成する
    • 第二引数で渡す ClassLoader は、新規クラスローダーの親として利用される
  • defineModulesWithManyLoaders(Configuration, ClassLoader)
    • これは、モジュールグラフ内のモジュール1つ1つに対して、クラスローダーが作成されて割り当てられる
    • 第二引数で渡す ClassLoader は、新規クラスローダーの親として利用される

参考


  1. 他にも定義できることはあるが、ここでは話を単純にするため基本となるこの2つの情報に絞る 

  2. 厳密には module-info.java だけでなく、実行時のオプションで定義を追加したり書き換えたりすることも可能 

  3. java.base の話は今は割愛 

  4. パッケージにアクセスするためには、さらにそのパッケージが exports されている必要がある 

  5. だからこそ、「なんで --main とかじゃなくて --module なんだろう」みたいなモヤモヤがあった 

  6. 実行時なら module-info.class 

  7. ServiceLoaderについてはJigsaw 勉強メモ - Qiitaを参照 

  8. そして実行時に意図しない方のクラスがロードされNoSuchMethodErrorとかが発生する 

  9. module-info.java を持つ通常のモジュール、および後述する自動モジュールのこと 

  10. 実際遭遇して困った。 

  11. ルート・モジュールの決定ルールをよく読めばわかるが、完全にすべてのモジュールが対象になるわけではないので、そういうモジュールを使っていた場合に問題が発生する 

  12. 解決のされ方については こちら を参照 

  13. 観測可能なモジュール内に同じ名前のモジュールが複数存在した場合、最初に見つかったモジュールが優先されるっぽい(システムモジュールはモジュールパスより先に検索される)