Gradle で Java 9 から追加された Jigsaw (モジュール)を使う方法のメモ。
環境
OS
Windows 10
Java
java 10.0.1 (Oracle JDK)
Gradle
4.7
Jigsaw のサポート状況
While Gradle version 4.7 doesn’t have first-class support for Java 9 modules yet, this guide shows you how to experiment with them before that support is complete.
【訳】
Gradle の version 4.7 では、 Java 9 のモジュールについてのファーストクラスでのサポートはまだありません。
このガイドでは、サポートが完了するまでの間にどのようにしたらモジュールを試せるのかについて説明します。
ということで、 2018 年 6 月現在(version 4.7)、 Gradle は Jigsaw のファーストクラスでのサポートはまだ対応していない。
ただし、これはコンパイルができないわけではない。
従来のタスクのオプションをややハック気味にいじることによって、 Jigsaw に対応したプロジェクトをビルドすることはできるようになっている。
Jigsaw に特化したオプションなどは存在しないということで、ファーストクラスでのサポートはないという状況。
いつ完全にサポートされるようになる?
この「ファーストクラスでのサポートはまだない」という説明は、少なくとも Java 9 が出た当初から書かれているのを見ている。
Java 10 が出た今もまだ対応しておらず、ちょっと気になったので調べたら、 GitHub に次のような Issue があるのを見つけた。
[Epic] Java 9 Jigsaw support · Issue #890 · gradle/gradle
2016 年の 11 月に作られた Issue で、 Milestone が Unscheduled になっている。。。
2018 年になってからの書き込みもあるが、みんな Jigsaw のサポートがいつになるのか気になっているようだ。。。
まずは普通のプロジェクトをモジュール化する
モジュール化前
|-build.gradle
`-src/main/java/
`-sample/jigsaw/
`-Main.java
apply plugin: 'java'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
jar.baseName = "jigsaw-sample"
package sample.jigsaw;
public class Main {
public static void main(String[] args) {
System.out.println("Hello Jigsaw!!");
}
}
> gradle jar
> java -cp build\libs\jigsaw-sample.jar sample.jigsaw.Main
Hello Jigsaw!!
モジュール化後
モジュール化するといっても、 module-info.java をルートに置くだけ。
|-build.gradle
`-src/main/java/
|-module-info.java <<NEW>>
`-sample/jigsaw/
`-Main.java
module sample.jigsaw {
}
> gradle jar
> java -p build\libs\jigsaw-sample.jar -m sample.jigsaw/sample.jigsaw.Main
Hello Jigsaw!!
- クラスパス (
-cp) ではなくモジュールパス (-p,--module-path) で jar を読み込む - メインクラスの指定は
-m(--module) で指定する- 指定方法は
<モジュール名>/<メインクラス名>になる
- 指定方法は
あんまりモジュール化した感はない。
実行可能 jar のモジュール版みたいなことはできないっぽい?
jar {
manifest {
attributes("Main-Class": "sample.jigsaw.Main")
}
}
- jar ファイル内の
MANIFEST.MFのMain-Classを指定しておくと、-jarオプションで jar ファイルを指定するだけでメインクラスが呼ばれるようになる
> java -jar build\libs\jigsaw-sample.jar
- モジュールを使用した場合も同様のことができるのか軽く調べたが、
jarコマンドで指定する--main-classが該当するっぽい気がする- 完全には一致していないが、一番近い気はする
-
jarコマンドで jar ファイルを生成するときに、--main-classでメインクラスの FQCN を指定しておく
> gradle compileJava
> jar -c -f build\jigsaw-sample.jar --main-class sample.jigsaw.Main -C build\classes\java\main .
> java -p build\jigsaw-sample.jar -m sample.jigsaw
Hello Jigsaw!!
-
--main-classを指定して作成した jar ファイルは、-mでモジュール名を指定するだけでメインクラスが呼ばれるようになる - どのクラスがメインクラスかは、
module-info.classに書き込まれる
> javap -v module-info.class
...
ModuleMainClass: #8 // sample.jigsaw.Main
ModulePackages:
#11 // sample.jigsaw
-
ModuleMainClassとかいうのが追加されているのが確認できる - これを Gradle のビルドの際に指定できるのか試したが、結果としてはダメそうな感じがした
- Gradle の jar タスクのクラスを見てみると zip を作成するタスクを継承して作られているのがわかる
- つまり、
jarコマンドを呼び出しているとかいうわけではなく、自力で jar 構造のアーカイブを作っているっぽい気がする- おそらくなので、間違ってたらすみません
-
jarコマンドを呼んでいないとなると、--main-classオプションを挟む余地は現状ないということになる
依存ライブラリのあるプロジェクトをモジュール化する
モジュール化前
|-build.gradle
|-commons-lang3-3.7.jar
`-src/main/java/
`-sample/jigsaw/
`-Main.java
apply plugin: 'java'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
jar.baseName = "jigsaw-sample"
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.7'
}
package sample.jigsaw;
import org.apache.commons.lang3.RandomStringUtils;
public class Main {
public static void main(String[] args) {
System.out.println("Hello Jigsaw!! : " + RandomStringUtils.random(10, "0123456789"));
}
}
> gradle jar
> java -cp build\libs\jigsaw-sample.jar;commons-lang3-3.7.jar sample.jigsaw.Main
Hello Jigsaw!! : 5176080436
- 依存関係として commons-lang3 を追加したプロジェクト
モジュール化後
|-build.gradle
|-commons-lang3-3.7.jar
`-src/main/java/
|-module-info.java <<NEW>>
`-sample/jigsaw/
`-Main.java
module sample.jigsaw {
requires org.apache.commons.lang3;
}
apply plugin: 'java'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
jar.baseName = "jigsaw-sample"
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.7'
}
compileJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
-
Main.javaは変更なし
> gradle jar
> java -p build\libs\jigsaw-sample.jar;commons-lang3-3.7.jar -m sample.jigsaw/sample.jigsaw.Main
Hello Jigsaw!! : 5579416699
説明
-
build.gradleを何も修正しないと、依存関係をクラスパスにいれてビルドしようとする - このため、モジュールが解決できずに次のようなエラーになる
> gradle jar
> Task :compileJava FAILED
...\jigsaw\src\main\java\module-info.java:2: エラー: モジュールが見つかりません: org.apache.commons.lang3
requires org.apache.commons.lang3;
^
エラー1個
FAILURE: Build failed with an exception.
- モジュールをビルドするためには、
compileJavaのオプションを変更する必要がある
compileJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
-
compileJavaのdoFirstブロックでオプションを変更する -
options.compileArgsで、クラスパスに入っている依存関係(classpath.asPath)をモジュールパスに設定する(--module-path) - そして、逆に
classpathにはfiles()をセットすることでクラスパスを空にしておく - これでモジュールパスを使ったビルドが行われるようになる
依存ライブラリのモジュール名を調べる
依存ライブラリを使う場合は、 module-info.java で requires 指定で対象のモジュールを読み込む必要がある。
そのためには、依存ライブラリのモジュール名を知る必要がある。
しかし、依存ライブラリのモジュール名は優先度の高い順に次のいずれかに決まるので、調べるのがやや面倒。
-
module-info.classで定義されているモジュール名 -
MANIFEST.MFのAutomatic-Module-Nameで指定されたモジュール名 - jar ファイル名から自動決定されるモジュール名
特に 2018 年現在は世のライブラリもモジュール化の過渡期にあるので、ライブラリによっては Automatic-Module-Name の指定が入れられているものもあれば、まったく入っていないものもあったりと様々な状況になっている。
ちなみに、 commons-lang3 は最新版(3.3.7)では Automatic-Module-Name が設定されているので、そこで定義されているモジュール名を指定する必要がある。
jdeps コマンドで調べる
- Java 8 から JDK に同梱されている jdeps というコマンドを使うと、依存するモジュールと、その名前を調べることができる
|-build.gradle
`-src/main/java/
`-sample/jigsaw/
`-Main.java
apply plugin: 'java'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
jar.baseName = "jigsaw-sample"
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.7'
}
task jdeps(type: Exec, dependsOn: compileJava) {
commandLine(
"jdeps",
"--module-path", compileJava.classpath.asPath,
"-s",
compileJava.destinationDir
)
}
> gradle jdeps
...
main -> java.base
main -> org.apache.commons.lang3
org.apache.commons.lang3 -> java.base
org.apache.commons.lang3 -> java.desktop
-
jdepsコマンドを使ってモジュールを調べるタスク (jdeps) を追加する -
非モジュールプロジェクトの状態で
jdepsタスクを実行することで、依存するモジュールを調べられる
IDE の機能を使って調べる
IntelliJ IDEA の場合は、クイックフィックスの機能を利用すれば簡単にモジュール名を補完できる。
(Eclipse は未調査だが、たぶん同等の機能があると信じてる)
module sample.jigsaw {
}
package sample.jigsaw;
import org.apache.commons.lang3.RandomStringUtils;
public class Main {
public static void main(String[] args) {
System.out.println("Hello Jigsaw!! : " + RandomStringUtils.random(10, "0123456789"));
}
}
-
module-info.javaを追加しただけだと、 commons-lang3 を使用しているMain.java側は、次のようにコンパイルエラーになっている
-
import文のエラー部分でAlt+Enterを入力すると、module-info.javaの補完が選択肢として現れる
-
module-info.javaが補完されて、コンパイルエラーが解消される
module sample.jigsaw {
requires org.apache.commons.lang3;
}
application プラグインでモジュール化
モジュール化前
|-build.gradle
`-src/main/java/
`-sample/jigsaw/
`-Main.java
apply plugin: 'application'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
mainClassName = "sample.jigsaw.Main"
jar.baseName = "jigsaw-sample"
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.7'
}
> gradle run
Hello Jigsaw!! : 5790539280
-
applicationプラグインを使うと、runタスクでプログラムを実行することができる - メインクラスは
mainClassNameで指定している
モジュール化後
apply plugin: 'application'
sourceCompatibility = 10
targetCompatibility = 10
compileJava.options.encoding = 'UTF-8'
ext.moduleName = "sample.jigsaw"
mainClassName = "${moduleName}/sample.jigsaw.Main"
jar.baseName = "jigsaw-sample"
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.7'
}
compileJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
run {
doFirst {
jvmArgs = [
"--module-path", classpath.asPath,
"--module", mainClassName
]
classpath = files()
}
}
> gradle run
Hello Jigsaw!! : 9066254426
説明
ext.moduleName = "sample.jigsaw"
mainClassName = "${moduleName}/sample.jigsaw.Main"
compileJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
run {
doFirst {
jvmArgs = [
"--module-path", classpath.asPath,
"--module", mainClassName
]
classpath = files()
}
}
-
compileJavaと同じように、runタスクのオプションを拡張してモジュールに対応させる -
doFirstの中でjvmArgsを変更する - 変更内容は
compileJavaと同じで、--module-pathをclasspathの代わりに設定する - 加えて、メインクラスを指定する
--module(-m) オプションを追加する - ここには
mainClassNameの値を設定している -
mainClassNameは、"${moduleName}"/sample.jigsaw.Mainのようにしてモジュール用のメインクラス指定にしている
この方法の問題点
上述の run タスクを書き換える方法は、公式のドキュメント で紹介されている方法になる。
ただ、この方法には1つ大きな問題がある。
Main クラスを次のように修正すると、問題点が分かりやすくなる。
package sample.jigsaw;
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
System.out.println("args = " + StringUtils.join(args, ","));
}
}
コマンドライン引数を出力している。
これを run タスクで実行すると、次のようになる。
> gradle run
args = -Dfile.encoding=windows-31j,-Duser.country=JP,-Duser.language=ja,-Duser.variant,sample.jigsaw/sample.jigsaw.Main
何も指定していないはずなのに、なんかいっぱい出た。
何が起こったのか?
run {
doFirst {
jvmArgs = [
"--module-path", classpath.asPath,
"--module", mainClassName
]
classpath = files()
// ★追加★
println("[allJvmArgs]")
println(allJvmArgs.join("\n"))
}
}
run タスクの中で allJvmArgs を出力するようにして再度 run を実行する。
> gradle run
[allJvmArgs]
--module-path
...\build\classes\java\main;...\build\resources\main;...\commons-lang3-3.7.jar
--module
sample.jigsaw/sample.jigsaw.Main
-Dfile.encoding=windows-31j
-Duser.country=JP
-Duser.language=ja
-Duser.variant
args = -Dfile.encoding=windows-31j,-Duser.country=JP,-Duser.language=ja,-Duser.variant,sample.jigsaw/sample.jigsaw.Main
jvmArgs で指定したオプションの後ろに、おそらく Gradle が自動的に付けたと思われるオプションがくっついている。
続いて java のヘルプを確認する。
> java --help
使用方法: java [options] <mainclass> [args...]
(クラスを実行する場合)
または java [options] -jar <jarfile> [args...]
(jarファイルを実行する場合)
または java [options] -m <module>[/<mainclass>] [args...]
java [options] --module <module>[/<mainclass>] [args...]
(モジュールのメイン・クラスを実行する場合)
...
-m (--module) でメインクラスを指定する場合、その後ろに続く引数は全てプログラムに渡されるコマンドライン引数として扱われることになっている。
つまり、 jvmArgs で --module を指定してしまうと、その後ろに続く JVM オプションとして渡すはずだった引数が、コマンドライン引数としと main メソッドの引数に渡されてしまうことになる。
さらに、よくよく args に渡された値を見てみると、最後に sample.jigsaw/sample.jigsaw.Main がついていることに気付く。
これは --module 指定でメインクラスを指定しているにも関わらず、 run タスクが mainClassName で指定した値を渡してしまっているためにくっついているものと思われる。
つまり、 run タスクは次のように java コマンドを組み立てようとしているが、
java [jvmArgsで指定した値] [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
|--ここがJVMオプション(のつもり)------------------||--メインクラスの指定--------| |--残りはプログラムに渡す引数.....
jvmArgs で無理やり --module を指定したため、 Java 10 の java コマンドは次のように引数を解釈してしまっている。
java --module-path ......... --module ............. [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
|--ここがJVMオプション--| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数扱い..........................
一旦話を整理する
-
jvmArgsで--module-path,--moduleを指定すれば、一応プログラムは動く - しかし、
--moduleをjvmArgsで指定してしまっているため、そこから後ろに繋げられる本来 JVM オプションなどに指定されるはずだった値が、全てプログラムの引数として扱われてしまっている - これはつまり、引数を利用するプログラムを作って動かそうとしたときに大きな問題となる
どうすべきか?
java --module-path ......... --module ............. [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
|--ここがJVMオプション--| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数扱い..........................
これが、
java --module-path ......... [自動設定されるJVMオプション] --module ............. [argsで指定される値]
|--ここがJVMオプション------------------------------| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数...
こうなるのが理想。
JVM オプションの末尾に任意のパラメータを挿入できないか?
jvmArgs で指定した値は JVM オプションの先頭に渡される。
一方、自動設定されるものも含めた全てのオプションは allJvmArgs プロパティから参照できる。
--module オプションは jvmArgs ではなく、この allJvmArgs の末尾に追加すれば、うまくいくのではなかろうか?
run {
doFirst {
main = "" // main クラスの指定を空にして
jvmArgs = [
"--module-path", classpath.asPath
]
allJvmArgs += ["--module", mainClassName] // allJvmArgs の末尾に --module を追加
classpath = files()
}
}
> gradle run
Execution failed for task ':run'.
> java.lang.UnsupportedOperationException (no error message)
残念ながら、 allJvmArgs は読み取り専用なので書き換えることができなかった。
mainClassName に指定する値に --module を入れられるか?
run {
doFirst {
main = "--module $mainClassName"
jvmArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
main に、 --module を無理やりくっつけた値を渡してみる。
> gradle run
> Task :run FAILED
Unrecognized option: --module sample.jigsaw/sample.jigsaw.Main
どうやら "--module sample.jigsaw/sample.jigsaw.Main" という単一の引数として java コマンドに渡されてしまっているようで、不明なオプションとして処理されてしまった。
args の先頭でメインクラスを指定する
run {
doFirst {
main = ""
jvmArgs = [
"--module-path", classpath.asPath
]
args = ["--module", mainClassName]
args += ["foo", "bar"]
classpath = files()
}
}
main は空にして、 args の先頭で --module を指定する。
> gradle run
args = foo,bar
上手く動いた!
結論
run {
doFirst {
jvmArgs = [
"--module-path", classpath.asPath,
"--module", mainClassName
]
classpath = files()
}
}
run {
doFirst {
main = ""
jvmArgs = [
"--module-path", classpath.asPath
]
args = ["--module", mainClassName]
args += ["foo", "bar"]
classpath = files()
}
}
ファーストクラスでのサポートが待たれる。。。
startScript を修正する
- ここまでの修正だけだと、
runタスクにしか対応できていない -
installDistで生成される起動スクリプトはクラスパス指定のままになっているので、そのままだと起動ができない
...
set CLASSPATH=%APP_HOME%\lib\jigsaw-sample.jar;%APP_HOME%\lib\commons-lang3-3.7.jar
...
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS% -classpath "%CLASSPATH%" sample.jigsaw/sample.jigsaw.Main %CMD_LINE_ARGS%
-
CLASSPATHに jar のパスがセットされ、-classpath指定で java コマンドを実行している - このため、このまま起動しようとしてもモジュールが見つからずにエラーになる
> build\install\jigsaw\bin\jigsaw
エラー: メイン・クラスsample.jigsaw.sample.jigsaw.Mainを検出およびロードできませんでした
原因: java.lang.ClassNotFoundException: sample.jigsaw.sample.jigsaw.Main
- これに対応するために、公式ドキュメントでは次のように
build.gradleを修正する方法が紹介されている
ext.moduleName = "sample.jigsaw"
mainClassName = "${moduleName}/sample.jigsaw.Main"
...
import java.util.regex.Matcher
startScripts {
doFirst {
classpath = files()
defaultJvmOpts = [
"--module-path", "APP_HOME_LIBS",
"--module", mainClassName
]
}
doLast {
File bashFile = new File(outputDir, applicationName)
String bashContent = bashFile.text
bashFile.text = bashContent.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('$APP_HOME/lib'))
File batFile = new File(outputDir, applicationName + ".bat")
String batContent = batFile.text
batFile.text = batContent.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('%APP_HOME%/lib'))
}
}
- これで、起動スクリプトは次のように出力されるようになる
...
set DEFAULT_JVM_OPTS="--module-path" "%APP_HOME%/lib" "--module" "sample.jigsaw/sample.jigsaw.Main"
...
set CLASSPATH=
...
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS% -classpath "%CLASSPATH%" sample.jigsaw/sample.jigsaw.Main %CMD_LINE_ARGS%
- 起動スクリプトを叩くと、問題なく実行できる
> build\install\jigsaw\bin\jigsaw.bat
Hello Jigsaw!! : 9282500878
- ただ、前述した
runタスクの修正と同様の問題が発生している - それについては後述
説明
- なにやら色々修正しているように見えるが、結局やっているのは次の2つだけ
- クラスパスを空にする
- 代わりにモジュールパスを設定する
- つまり、
compileJavaとかでやっていたのと同じ修正をしているにすぎない
startScripts {
doFirst {
classpath = files()
defaultJvmOpts = [
"--module-path", "APP_HOME_LIBS",
"--module", mainClassName
]
}
- まずは、
classpathにfiles()をセットすることでクラスパスを空にしている - 次に
defaultJvmOptsに--module-path(モジュールパス)を設定している- ここではまず
APP_HOME_LIBSという文字列を設定している- この文字列は、あとでスクリプトファイルの種類ごとに置換を行う
- また、
--moduleでメインクラスを指定している
- ここではまず
- この
defaultJvmOptsで指定した値は、スクリプトファイル上ではDEFAULT_JVM_OPTSという変数に格納するように出力される
set DEFAULT_JVM_OPTS="--module-path" "%APP_HOME%/lib" "--module" "sample.jigsaw/sample.jigsaw.Main"
doLast {
File bashFile = new File(outputDir, applicationName)
String bashContent = bashFile.text
bashFile.text = bashContent.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('$APP_HOME/lib'))
File batFile = new File(outputDir, applicationName + ".bat")
String batContent = batFile.text
batFile.text = batContent.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('%APP_HOME%/lib'))
}
-
--module-pathに指定したAPP_HOME_LIBSは、そのままだと何の意味もないただの文字列でしかない - 実際には、ここにモジュールパスに含めたいパスを指定しなければならない
- モジュールパスに指定するパスは、プログラムを構成する jar ファイル(もしくは、その jar が存在するディレクトリへのパス)になる
-
installDistで生成されるフォルダ構成では、<インストールディレクトリ>/libの下に jar ファイルがまとめられている - そして、
<インストールディレクトリ>は起動スクリプト上ではAPP_HOMEという名前の変数で参照できるようになっている - よって、
APP_HOME_LIBSを<APP_HOME>/libに置き換えれば、良い感じにモジュールパスを指定できることになる - ただ、ここで問題なのは、変数の参照方法が
bashファイルとbatファイルとで異なるという点 -
bashは$APP_HOMEで参照し、batでは%APP_HOME%で参照しなければならない -
doLastブロックでは、この2つの違いを、それぞれのスクリプトで分けて実装している - 結果、生成された起動スクリプトは次のようになる
...
set DEFAULT_JVM_OPTS="--module-path" "%APP_HOME%/lib" "--module" "sample.jigsaw/sample.jigsaw.Main"
...
set CLASSPATH=
...
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS% -classpath "%CLASSPATH%" sample.jigsaw/sample.jigsaw.Main %CMD_LINE_ARGS%
引数の問題
...
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS% -classpath "%CLASSPATH%" sample.jigsaw/sample.jigsaw.Main %CMD_LINE_ARGS%
- よく見ると、
runタスクのときと同じように、引数の指定がおかしくなっている - 実際、引数を出力するようにして実行すると次のようになる
> build\install\jigsaw\bin\jigsaw.bat
args = -classpath,,sample.jigsaw/sample.jigsaw.Main
-
-classpath以下が引数として認識されてしまっている - これは、一応次のように
build.gradleを修正すれば改善する
doLast {
File bashFile = new File(outputDir, applicationName)
String bashContent = bashFile.text
bashFile.text = bashContent
.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('$APP_HOME/lib'))
.replace('-classpath "\\"$CLASSPATH\\"" ' + mainClassName, '')
File batFile = new File(outputDir, applicationName + ".bat")
String batContent = batFile.text
batFile.text = batContent
.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('%APP_HOME%/lib'))
.replace('-classpath "%CLASSPATH%" ' + mainClassName, '')
}
- やや無理矢理だが、
-classpath以下の余計な部分を空文字に置換している
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS% %CMD_LINE_ARGS%
> build\install\jigsaw\bin\jigsaw hoge fuga
args = hoge,fuga
単体テストのあるプロジェクトをモジュール化する
まず最初に思いついたの
まず最初に思いついたのは、 test の方にも module-info.java を置いて、 sample.jigsaw.test のように main とは異なるモジュールを定義するという方法だった。
しかし、この方法は次のような問題があるのでダメだった。
- 異なるモジュール間では、同じパッケージを宣言できない
-
HogeクラスのテストクラスHogeTestを、Hogeと同じパッケージで宣言するとエラーになる
-
-
exportsしていないパッケージはテストモジュールから参照できなくなってしまうので、内部 API のテストが書けない
1 の方は異なるパッケージに宣言すれば解決できそうだが、 2 の方が致命的なので、 test を異なるモジュールにするというのは無さそう。
推奨?されている方法
公式ドキュメント では、次のような方法が推奨?されている。
|-build.gradle
`-src/
|-main/java/
| |-module-info.java
| `-sample/jigsaw/
| `-Hoge.java
|
`-test/java/
`-sample/jigsaw/
`-HogeTest.java
-
testの方にはmodule-info.javaを置いていない
...
ext {
moduleName = "sample.jigsaw"
junitModuleName = "org.junit.jupiter.api"
}
...
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
}
...
compileTestJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath,
"--add-modules", junitModuleName,
"--patch-module", "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
"--add-reads", "$moduleName=$junitModuleName"
]
classpath = files()
}
}
package sample.jigsaw;
public class Hoge {
public String method(int a, int b) {
return a + " + " + b + " = " + (a + b);
}
}
package sample.jigsaw;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HogeTest {
@Test
void test() {
Hoge hoge = new Hoge();
String actual = hoge.method(10, 20);
assertEquals("10 + 20 = 30", actual);
}
}
> gradle compileTestJava
BUILD SUCCESSFUL in 3s
説明
なにやら compileTestJava に色々追加しているが、1つずつ説明していく。
compileTestJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath,
"--add-modules", junitModuleName,
"--patch-module", "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
"--add-reads", "$moduleName=$junitModuleName"
]
classpath = files()
}
}
--module-path
"--module-path", classpath.asPath,
- まずは、
compileJavaのときと同じでクラスパスの代わりにモジュールパスを利用することでモジュール化をしている
--add-modules
-
--module-pathを追加しただけだと、次のようなエラーが発生する。
> gradle compileTestJava
...\src\test\java\sample\jigsaw\HogeTest.java:3: エラー: パッケージorg.junit.jupiter.apiは表示不可です
import org.junit.jupiter.api.Test;
^
(パッケージorg.junit.jupiter.apiはモジュールorg.junit.jupiter.apiで宣言されていますが、モジュール・グラフにありません)
-
HogeTestが JUnit のパッケージを使用しようとしているが、 JUnit のモジュールがモジュール・グラフ上にないのでエラーになっている - というのも、
mainの方のmodule-info.javaには JUnit のモジュールを読み込むような記述はないので当然と言えば当然 - そこで、 JUnit のモジュール (
org.junit.jupiter.api) を--add-modulesを使ってルート・モジュールに追加する
"--add-modules", junitModuleName,
--patch-module
- まだエラーは消えない
> gradle compileTestJava
...\src\test\java\sample\jigsaw\HogeTest.java:11: エラー: シンボルを見つけられません
import sample.jigsaw.Hoge;
^
シンボル: クラス Hoge
場所: パッケージ sample.jigsaw
-
HogeTestクラスからHogeクラスを参照できていない - これは、
sample.jigsawモジュールの中にsrc/test/java以下のテストクラス達が含まれていないために起こっているっぽい - この問題を解決するため、
--patch-moduleというオプションが用意されている -
--patch-moduleを使うと、任意のモジュールに対してクラスやパッケージを後から追加できる - 書式は
--patch-module <モジュール名>=<追加するjarやディレクトリ>[;<追加するjarやディレクトリ>...]- パス区切り文字は、 Linux とかならコロン(
:)になる
- パス区切り文字は、 Linux とかならコロン(
"--patch-module", "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
- これで、
sample.jigsawモジュールに対してsrc/test/java以下のクラスがコンパイル時に追加されるようになる - つまり、
HogeTestもsample.jigsawの中に入れてもらえた状態になる
ちなみに、 --patch-module は JEP の説明の中で次のように説明されている。
The --patch-module option is intended only for testing and debugging. Its use in production settings is strongly discouraged.
【訳】
--patch-module オプションはテストやデバッグだけで使われることを意図しています。
本番環境での使用はお勧めしません。
Patching module content | JEP 261: Module System
--add-reads
最後のエラー。
> gradle compileTestJava
...\src\test\java\sample\jigsaw\HogeTest.java:3: エラー: パッケージorg.junit.jupiter.apiは表示不可です
import org.junit.jupiter.api.Test;
^
(パッケージorg.junit.jupiter.apiはモジュールorg.junit.jupiter.apiで宣言されていますが、モジュールsample.jigsawに読み込まれていません)
- これは、
org.junit.jupiter.apiモジュールはルート・モジュールとして読み込まれたが、sample.jigsawモジュールがorg.junit.jupiter.apiをrequiresしていないために発生している - これについても、任意のモジュールが使用するモジュールを追加できるオプションが用意されている
- それが
--add-readsオプション - 書式は
--add-reads <モジュール名>=<追加で使用するモジュール>[,<追加で使用するモジュール>...]
"--add-reads", "$moduleName=$junitModuleName"
-
sample.jigsawモジュールに対して、org.junit.jupiter.apiモジュールを読み込めるように追加している - これでようやくコンパイルが通るようになる
整理
| オプション | 意味・目的 |
|---|---|
--module-path |
モジュールパスを利用してビルドするため |
--add-modules |
JUnit のモジュールをルート・モジュールとして登録 |
--patch-module |
sample.jigsaw モジュールに src/test/java 以下のクラスを追加 |
--add-reads |
sample.jigsaw モジュールから JUnit のモジュールを読み込めるようにする |
テストの実行
test {
useJUnitPlatform()
doFirst {
jvmArgs = [
'--module-path', classpath.asPath,
'--add-modules', 'ALL-MODULE-PATH',
'--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath,
'--add-reads', "$moduleName=$junitModuleName",
'--add-opens', "$moduleName/sample.jigsaw=org.junit.platform.commons"
]
classpath = files()
}
}
またちょっとオプションが増えている。
それぞれ説明していく。
useJUnitPlatform()
test {
useJUnitPlatform()
- こちらは、 Gradle で JUnit 5 を利用するときに必要になる記述
--module-path
'--module-path', classpath.asPath,
- これは、これまで同様モジュールパスを使用するための設定
--add-modules
'--add-modules', 'ALL-MODULE-PATH',
-
compileTestJavaのときとは少し変わっていて、値がALL-MODULE-PATHになっている -
ALL-MODULE-PATHは、名前の通りモジュールパスに設定されているものを全てルート・モジュールとして登録する指定になる - テストの JVM は Gradle のテストランナーによって起動されるが、この JVM は当然
sample.jigsawモジュールなどをモジュール・グラフ上に読み込んではいない- Jigsaw へのファーストクラスサポートもまだなので、 JUnit 類のモジュールも読み込んでいないはず
- なので、
ALL-MODULE-PATHで全てのモジュールパスをルート・モジュールとして読み込んでいる
ちなみに、この設定をしないと次のようなエラーが発生する。
Caused by: java.lang.ClassNotFoundException: sample.jigsaw.HogeTest
...
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)
...
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.loadClass(JUnitPlatformTestClassProcessor.java:103)
Gradle がテストクラス (HogeTest) をロードしようとして、クラスが見つからずエラーになっている。
おそらく、モジュール・グラフ上に存在しないせいで見つかっていないのだと思う、たぶん。
--patch-module
-
--add-modulesを設定しただけだと、実はエラーの内容は変化せず、相変わらずClassNotFoundException: sample.jigsaw.HogeTestが発生する - ただ、これは
compileTestJavaのときと同じでsrc/test/java以下のクラスがsample.jigsawモジュールに含まれていないことが原因っぽいので、--patch-moduleで追加してあげる必要がある
'--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath,
-
ちょっと注意なのが、
compileTestJavaとはちょっとだけ指定値が変わっている -
compileTestJavaのときはsrcDirsだったが、testのときはoutputDirを指定する
--add-reads
'--add-reads', "$moduleName=$junitModuleName",
- これは
compileTestJavaのときと意味は同じなので割愛
--add-opens
- 公式ドキュメント では、この設定は存在しない
- これは、 JUnit 5 を使う場合に必要になるっぽい
- 公式ドキュメントは JUnit 4 での説明が書かれていて、 JUnit 4 ではこの設定が無くてもテストは動作する
- この設定をせずにテストを実行すると、次のようなエラーが発生する
java.lang.reflect.InaccessibleObjectException: Unable to make sample.jigsaw.HogeTest() accessible: module sample.jigsaw does not "opens sample.jigsaw" to module org.junit.platform.commons
- なにやら、 JUnit 側がリフレクションで
HogeTestにアクセスしようとして怒られているっぽい -
sample.jigsawパッケージがorg.junit.platform.commonsモジュールに対してopensになっていないために怒っているらしい-
opensは、コンパイル時は不要だが実行時にリフレクションなどで参照されるような場合に指定する設定で、module-info.javaで定義する - 詳しくはこちらを参照
-
- 一応
sample.jigsawのmodule-info.javaでopens sample.jigsawとすればエラーはなくなる - しかし、テストのための設定を main の
module-info.javaに書くのはちょっとアレ - ということで、実行時に
opensの定義を追加できる--add-opensオプションを指定することで回避している
'--add-opens', "$moduleName/sample.jigsaw=org.junit.platform.commons"
- エラーメッセージに従って、
sample.jigsawパッケージをorg.junit.platform.commonsに対してopensにした- 書式は
--add-opens <モジュール名>/<パッケージ名>=<公開先のモジュール>[,<公開先のモジュール>...]
- 書式は
実行
これで、ようやくテストが実行できるようになる。
> gradle test
BUILD SUCCESSFUL in 6s
整理
| オプション | 意味・目的 |
|---|---|
--module-path |
モジュールパスを利用してビルドするため |
--add-modules |
ALL-MODULE-PATH 指定で全てのモジュールをルートに登録 |
--patch-module |
sample.jigsaw モジュールに src/test/java 以下のクラスを追加 |
--add-reads |
sample.jigsaw モジュールから JUnit のモジュールを読み込めるようにする |
--add-opens |
JUnit のモジュールから sample.jigsaw パッケージにアクセスできるようにする |
マルチプロジェクトをモジュール化する
ここまでの話を全て総合して、マルチプロジェクトをモジュール化すると下のようになった。
|-settings.gradle
|-build.gradle
|
|-foo/
| |-build.gradle
| `-src/main/java/
| |-module-info.java
| `-sample/jigsaw/foo/
| `-Main.java
|
`-bar/
|-build.gradle
`-src/main/java/
|-module-info.java
`-sample/jigsaw/bar/
`-Bar.java
include "foo", "bar"
ext {
junitModuleName = "org.junit.jupiter.api"
}
subprojects {
apply plugin: 'java'
sourceCompatibility = 10
targetCompatibility = 10
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
}
compileJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath
]
classpath = files()
}
}
compileTestJava {
doFirst {
options.compilerArgs = [
"--module-path", classpath.asPath,
"--add-modules", junitModuleName,
"--patch-module", "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
"--add-reads", "$moduleName=$junitModuleName"
]
classpath = files()
}
}
test {
useJUnitPlatform()
doFirst {
jvmArgs = [
'--module-path', classpath.asPath,
'--add-modules', 'ALL-MODULE-PATH',
'--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath,
'--add-reads', "$moduleName=$junitModuleName",
"--add-opens", "$moduleName/$testPackageName=org.junit.platform.commons"
]
classpath = files()
}
}
}
foo プロジェクト
apply plugin: 'application'
ext {
moduleName = 'sample.jigsaw.foo'
testPackageName = 'sample.jigsaw.foo'
}
mainClassName = "${moduleName}/sample.jigsaw.foo.Main"
dependencies {
implementation project(":bar")
}
run {
doFirst {
jvmArgs = [
"--module-path", classpath.asPath
]
main = ""
args = ["--module", mainClassName]
classpath = files()
}
}
import java.util.regex.Matcher
startScripts {
doFirst {
classpath = files()
defaultJvmOpts = [
"--module-path", "APP_HOME_LIBS",
"--module", mainClassName
]
}
doLast {
File bashFile = new File(outputDir, applicationName)
String bashContent = bashFile.text
bashFile.text = bashContent
.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('$APP_HOME/lib'))
.replace('-classpath "\\"$CLASSPATH\\"" ' + mainClassName, '')
File batFile = new File(outputDir, applicationName + ".bat")
String batContent = batFile.text
batFile.text = batContent
.replaceFirst("APP_HOME_LIBS", Matcher.quoteReplacement('%APP_HOME%/lib'))
.replace('-classpath "%CLASSPATH%" ' + mainClassName, '')
}
}
module sample.jigsaw.foo {
requires sample.jigsaw.bar;
}
package sample.jigsaw.foo;
import sample.jigsaw.bar.Bar;
public class Main {
public static void main(String[] args) {
System.out.println(new Bar().method(10));
}
}
barプロジェクト
apply plugin: 'java-library'
ext {
moduleName = 'sample.jigsaw.bar'
testPackageName = 'sample.jigsaw.bar'
}
dependencies {
implementation "org.apache.commons:commons-lang3:3.7"
}
module sample.jigsaw.bar {
exports sample.jigsaw.bar;
requires org.apache.commons.lang3;
}
package sample.jigsaw.bar;
import org.apache.commons.lang3.RandomStringUtils;
public class Bar {
public String method(int size) {
return RandomStringUtils.random(size, "0123456789");
}
}
package sample.jigsaw.bar;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BarTest {
@Test
void testBar() {
Bar bar = new Bar();
String actual = bar.method(5);
assertEquals(actual.length(), 5);
}
}
動作確認
> gradle test
BUILD SUCCESSFUL in 4s
> gradle :foo:run
6569544042
結論
ファーストクラスサポートはよ
※Gradle プラグインのリポジトリを見たらいくつかあった

