- はじめに
- Java Platform Module System (JPMS) の振り返り
- Gradleにおけるモジュールシステムの扱い
- Gradleのモジュールの扱い具体例
- モジュールパスの個別指定による対応
- プラグインによる対応
- アーティファクト変換による対応
はじめに
Gradle でモジュールシステムを扱う場合、特にモジュールシステムに未対応な依存JARを扱うのは色々と注意しなければならないことが多い。
ここでは最初に、モジュールシステム自体について概説した後、Gradle でのモジュールの扱いの実例を見ていく。
最後に、Gradle で発生するモジュールシステムにまつわる問題と、その対応方法を紹介する。
Java Platform Module System (JPMS) の振り返り
クラスパスとモジュールパス
- クラスパス
- コンパイル時のクラス検索場所を
--class-path
,-classpath
または-cp
で指定 - 旧来互換
- コンパイル時のクラス検索場所を
- モジュールパス
- コンパイル時のモジュール検索場所を
--module-path
,-p
で指定
- コンパイル時のモジュール検索場所を
どちらのパスで読み込んだかにより、モジュールシステム上での扱いが変わる(後述)。
モジュラーJAR
- モジュラーJAR:モジュール記述子
module-info.class
のあるJAR - 非モジュラーJAR:モジュール記述子
module-info.class
のないJAR(旧来通り)
モジュール
JARは、クラスパスに置かれるか、モジュールパスに置かれるかで、扱いが変わる。
- 明示モジュール(Explicit Module)
- モジュールパスに置かれたモジュール記述子のあるJARで定義されたモジュール
- 自動モジュール(Automatic Module)
- モジュールパスに置かれたモジュール記述子の無いJAR
MANIFEST.MF
のAutomatic-Module-Name
属性が定義されていれば、その名前がモジュール名となる- 定義が無い場合は JAR ファイル名から自動的にモジュール名が設定(後述)される
- 無名モジュール(Unnamed Module)
- クラスパスに置かれたJAR(モジュール記述子の有無を問わない)
モジュールパスに置かれたモジュール記述子のあるJARは Named Module と呼ばれることも多いがModuleFinder.html#of(java.nio.file.Path...)) の記載に合わせて、ここでは明示モジュール(Explicit Module)と呼ぶことにする
JARの種類と配備場所の関係をまとめると以下のようになる。
クラスパスへ配備 | モジュールパスへ配備 | |
---|---|---|
モジュラーJAR | 無名モジュール | 明示モジュール |
非モジュラーJAR(Automatic-Module-Name ) |
無名モジュール | 自動モジュール(Automatic-Module-Name で定義されたモジュール名) |
非モジュラーJAR | 無名モジュール | 自動モジュール(JARファイル名から自動設定されたモジュール名) |
クラスパスへ配備した場合は、全て無名モジュールの扱いとなり、これはモジュールシステム以前の旧来互換となる。
自動モジュールのモジュール名
モジュールパスに置かれた非モジュラーJARにAutomatic-Module-Name
属性が定義されていれば、その名前がモジュール名となる。
この定義が無い場合は、JARファイル名からモジュール名が自動的に決定される。
モジュール名自動決定のロジックは ModuleFinder.of(Path...)
で定義されており、以下のルールとなる。
.jar
のサフィックスを削除- 正規表現
-(\\d+(\\.|$))
にマッチする場合、モジュール名は最初に出現したハイフンより前の部分となり、ハイフンの後の部分はバージョンと解釈される - 英数字以外の文字(
[^A-Za-z0-9]
)はすべて.
に置き換えられる(連続したドットは統合され、先頭と末尾のドットは削除される)
つまり foo-bar-1.2.3-SNAPSHOT.jar
という名前のJARファイルは、foo.bar
というモジュール名となり、1.2.3-SNAPSHOT
がバージョンとして解釈される。
明示/自動/無名モジュールの参照範囲
モジュールが参照(依存)する他のモジュールと、モジュールが他のモジュールに公開するパッケージは module-info.java
にて定義する(コンパイルされ module-info.class
としてJARのルートに配備される)。
module-info.class
が無いJARをモジュールパスに配備した場合、自動モジュールとなることは前述の通りだが、この時、以下のような module-info.java
が暗黙的に追加されたように振る舞う(この例はあくまでもイメージ)。
opens module <moduleName> { requires *; exports *; }
- 全てのパッケージが
exports
扱い - モジュールグラフ上のすべてのモジュールを
requires
している扱い
クラスパスへ配備され、無名モジュールとなっているJARについても同様に、全てのパッケージが exports
扱いで、すべてのモジュールを requires
している扱いとなるが、明示モジュール(Explicit Module) から無名モジュール(Unnamed Module)を参照することはできない。
明示モジュール → 自動モジュールの参照は可能なので、どうしても必要な場合は、自動モジュールをブリッジする形で無名モジュールを参照できる。
Gradleにおけるモジュールシステムの扱い
JPMS は後発で、Gradle 内部でのモジュールパスの扱いは場当たり的な対応になっている。
Gradle 8.5 時点でも、APIを介してモジュールパスの操作は行えず、java.modularity.inferModulePath
で、モジュールパス推測を行うかどうかを設定できるのみである(拡張予定はあるものの)。
java.modularity.inferModulePath
は Gradle 7.0 からデフォルト true
となり、依存ライブラリをクラスパスに配備するかモジュールパスに配備するかを JavaModuleDetector で判定する。
結果、モジュール記述子が定義されている、または Automatic-Module-Name
属性があれば、モジュールパスへの配備となり、それ以外はクラスパスに配備される。この挙動の決め打ちが問題となる。
モジュラーアプリケーションを作成する場合、module-info.java
で requires
なモジュールを指定する。非モジュラーJARでも、モジュールパスに配備されれば、JAR名がモジュール名となるため参照できるが、Gradle ではクラスパスに配備するため(名前が定義されないため)モジュール名を指定することができない。
このケースにおいて、例えば IntellJ ではモジュールパスに配備されたものとして扱うため、IDEではコンパイルできるがGradleではコンパイルエラーとなる、といった状況が発生する。
Gradleのモジュールの扱い具体例
具体例でGradleでのモジュールの扱いをまとめておく。
アプリケーションが依存するJARとして以下を考える。
ライブラリ | 説明 |
---|---|
jakarta.annotation:jakarta.annotation-api:2.1.1 |
モジュラーJAR |
org.apache.commons:commons-lang3:3.14.0 |
非モジュラーJAR(Automatic-Module-Name エントリあり) |
org.apache.commons:commons-math3:3.6.1 |
非モジュラーJAR |
アプリケーション側(自身で作成しているアプリケーション)で、module-info.java
を定義しない場合は、今まで通りに、全てのJARはクラスパスに配備される。
これは、MANIFEST.MF
にAutomatic-Module-Name
を定義した場合でも同じで、モジュラーアプリケーションとは解釈されない(resources/META-INF/MANIFEST.MF
に定義しても、JARタスク無いで manifest { attributes["Automatic-Module-Name"] = "xxx" }
のようにしても同じで全てのJARはクラスパスに配意される)。
アプリケーション側で module-info.java
を定義した場合、以下のようにコンパイル時引数が指定されることになる。
-classpath .../commons-math3-3.6.1.jar --module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar
commons-math3
は無名モジュールとなるので、以下はコンパイルエラーとなり、commons-math3
を使うことができない。
module foo {
requires jakarta.annotation;
requires org.apache.commons.lang3;
requires commons.math3; // error
exports com.example.hello;
}
なお、java.modularity.inferModulePath.set(false)
とすれば、ライブラリは全て -classpath
への配備となる。
モジュラーアプリケーション側からは無名モジュールを参照できないので、この場合もコンパイルすることはできない。
モジュールパスの個別指定による対応
コンパイル時の引数として直接モジュールパスを設定することで、前述の問題に対応できる。
tasks.withType<JavaCompile> { options.compilerArgs.addAll(listOf("--module-path", classpath.asPath)) classpath = files() // clear class path }
これによりクラスパスは利用せず、依存ライブラリは全てモジュールパスに配備される。
--module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar;.../commons-math3-3.6.1.jar --module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar;.../commons-math3-3.6.1.jar
--module-path
が2重定義されるのは、Gradleの従来バグによるもの。特に動作に影響はない。
プラグインによる対応
org.javamodularity.moduleplugin
プラグインにて対応することもできる。
plugins { ... id("org.javamodularity.moduleplugin") version "1.8.12" } java.modularity.inferModulePath.set(false)
inferModulePath
は false
に設定しておく。
これにより、依存は全て --module-path
に配備されるようになる(--module-path
が2重定義されるのは、前述と同)。
--module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar;.../commons-math3-3.6.1.jar --module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar;.../commons-math3-3.6.1.jar
org.javamodularity.moduleplugin
プラグインでは、テスト時のモジュールパスによる対応もサポートされるため、一先ずorg.javamodularity.moduleplugin
プラグインを適用しておくのが楽。
アーティファクト変換による対応
非モジュラーJARに無理やり module-info.class
を追加することでモジュラーJARに変換するのが org.gradlex:extra-java-module-info
プラグインであり、これにより対応することもできる。
plugins { ... id("org.gradlex.extra-java-module-info") version "1.6.1" } extraJavaModuleInfo { module("org.apache.commons:commons-math3", "commons.math3") { exports("org.apache.commons.math3.util") requires("java.logging") } }
extraJavaModuleInfo
ブロックの定義により、モジュール記述子がJARに追加される。上記の例では、以下の module-info.class
が自動的に追加される。
open module commons.math3 { requires java.base; requires java.logging; exports org.apache.commons.math3.util; }
これによりモジュールパスは以下のように配備される。
--module-path .../jakarta.annotation-api-2.1.1.jar;.../commons-lang3-3.14.0.jar;.../commons-math3-3.6.1-module.jar
commons-math3-3.6.1-module.jar
がモジュール記述子追加済みのJARとなる。
モジュールシステム上で考えると理にかなった対応となるが、多くの非モジュラーJARに依存する場合は、それぞれについてモジュール記述子を定義する必要があるため手軽さという点で劣る。
さらに署名付きJARの場合は、改ざんとみなされるのでこのプラグインによる対応は採用できないといった問題もある。