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 プラグインのリポジトリを見たらいくつかあった