Gradle で Jigsaw メモ

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 のモジュールについての**ファーストクラスでのサポートはまだありません**。
> このガイドでは、サポートが完了するまでの間にどのようにしたらモジュールを試せるのかについて説明します。

[Building Java 9 Modules](https://guides.gradle.org/building-java-9-modules/?_ga=2.50399337.1325336598.1527511393-492720480.1523802875)

ということで、 2018 年 6 月現在（version 4.7）、 Gradle は Jigsaw のファーストクラスでのサポートはまだ対応していない。

ただし、これはコンパイルができないわけではない。
従来のタスクのオプションをややハック気味にいじることによって、 Jigsaw に対応したプロジェクトをビルドすることはできるようになっている。

Jigsaw に特化したオプションなどは存在しないということで、**ファーストクラスでのサポートはない**という状況。

## いつ完全にサポートされるようになる？
この「ファーストクラスでのサポートはまだない」という説明は、少なくとも Java 9 が出た当初から書かれているのを見ている。

Java 10 が出た今もまだ対応しておらず、ちょっと気になったので調べたら、 GitHub に次のような Issue があるのを見つけた。

[[Epic] Java 9 Jigsaw support · Issue #890 · gradle/gradle](https://github.com/gradle/gradle/issues/890)

2016 年の 11 月に作られた Issue で、 Milestone が **Unscheduled** になっている。。。
2018 年になってからの書き込みもあるが、みんな Jigsaw のサポートがいつになるのか気になっているようだ。。。


# まずは普通のプロジェクトをモジュール化する
## モジュール化前
```text:プロジェクト構成
|-build.gradle
`-src/main/java/
  `-sample/jigsaw/
    `-Main.java
```

```groovy:build.gradle
apply plugin: 'java'

sourceCompatibility = 10
targetCompatibility = 10

compileJava.options.encoding = 'UTF-8'

jar.baseName = "jigsaw-sample"
```

```java:Main.java
package sample.jigsaw;

public class Main {

    public static void main(String[] args) {
        System.out.println("Hello Jigsaw!!");
    }
}
```

```text:ビルドして実行
> gradle jar

> java -cp build\libs\jigsaw-sample.jar sample.jigsaw.Main
Hello Jigsaw!!
```

## モジュール化後
モジュール化するといっても、 `module-info.java` をルートに置くだけ。

```text:プロジェクト構成
|-build.gradle
`-src/main/java/
  |-module-info.java <<NEW>>
  `-sample/jigsaw/
    `-Main.java
```

```java:module-info.java
module sample.jigsaw {
}
```

```text:ビルドして実行
> 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 のモジュール版みたいなことはできないっぽい？
```groovy:build.gradle
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 を指定しておく

```text:--main-classを指定してjarを作成する
> 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` に書き込まれる

```text:module-info.classの中をjavapで確認
> javap -v module-info.class
...

ModuleMainClass: #8                     // sample.jigsaw.Main
ModulePackages:
  #11                                     // sample.jigsaw
```

- `ModuleMainClass` とかいうのが追加されているのが確認できる
- これを Gradle のビルドの際に指定できるのか試したが、結果としてはダメそうな感じがした
- Gradle の jar タスクのクラスを見てみると [zip を作成するタスクを継承して作られているのがわかる](https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/Jar.html)
- つまり、 `jar` コマンドを呼び出しているとかいうわけではなく、自力で jar 構造のアーカイブを作っているっぽい気がする
    - おそらくなので、間違ってたらすみません
- `jar` コマンドを呼んでいないとなると、 `--main-class` オプションを挟む余地は現状ないということになる


# 依存ライブラリのあるプロジェクトをモジュール化する
## モジュール化前
```text:プロジェクト構成
|-build.gradle
|-commons-lang3-3.7.jar
`-src/main/java/
  `-sample/jigsaw/
    `-Main.java
```

```groovy:build.gradle
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'
}
```

```java:Main.java
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"));
    }
}
```

```text:ビルドして実行
> gradle jar

> java -cp build\libs\jigsaw-sample.jar;commons-lang3-3.7.jar sample.jigsaw.Main
Hello Jigsaw!! : 5176080436
```

- 依存関係として commons-lang3 を追加したプロジェクト


## モジュール化後
```text:プロジェクト構成
|-build.gradle
|-commons-lang3-3.7.jar
`-src/main/java/
  |-module-info.java <<NEW>>
  `-sample/jigsaw/
    `-Main.java
```

```java:module-info.java
module sample.jigsaw {
    requires org.apache.commons.lang3;
}
```

```groovy:build.gradle
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` は変更なし

```text:ビルドして実行
> 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` を何も修正しないと、依存関係をクラスパスにいれてビルドしようとする
- このため、モジュールが解決できずに次のようなエラーになる

```text:デフォルトのままビルドしたときに発生するエラー
> 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` のオプションを変更する必要がある

```groovy:build.gradle
compileJava {
    doFirst {
        options.compilerArgs = [
            "--module-path", classpath.asPath
        ]
        classpath = files()
    }
}
```

- `compileJava` の `doFirst` ブロックでオプションを変更する
- `options.compileArgs` で、クラスパスに入っている依存関係(`classpath.asPath`)をモジュールパスに設定する(`--module-path`)
- そして、逆に `classpath` には `files()` をセットすることでクラスパスを空にしておく
- これでモジュールパスを使ったビルドが行われるようになる

# 依存ライブラリのモジュール名を調べる
依存ライブラリを使う場合は、 `module-info.java` で `requires` 指定で対象のモジュールを読み込む必要がある。

そのためには、依存ライブラリのモジュール名を知る必要がある。

しかし、依存ライブラリのモジュール名は優先度の高い順に次のいずれかに決まるので、調べるのがやや面倒。

1. `module-info.class` で定義されているモジュール名
2. `MANIFEST.MF` の `Automatic-Module-Name` で指定されたモジュール名
3. jar ファイル名から自動決定されるモジュール名

特に 2018 年現在は世のライブラリもモジュール化の過渡期にあるので、ライブラリによっては `Automatic-Module-Name` の指定が入れられているものもあれば、まったく入っていないものもあったりと様々な状況になっている。

ちなみに、 commons-lang3 は最新版(`3.3.7`)では `Automatic-Module-Name` が設定されているので、そこで定義されているモジュール名を指定する必要がある。

## jdeps コマンドで調べる
- Java 8 から JDK に同梱されている jdeps というコマンドを使うと、依存するモジュールと、その名前を調べることができる

```text:プロジェクト構成
|-build.gradle
`-src/main/java/
  `-sample/jigsaw/
    `-Main.java
```

```groovy:build.gradle
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
    )
}
```

```text:実行
> 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 は未調査だが、たぶん同等の機能があると信じてる）

```java:module-info.java
module sample.jigsaw {
}
```

```java:Main.java
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` 側は、次のようにコンパイルエラーになっている

![jigsaw.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/214b2840-67d6-28fd-5b80-aea2911c395a.jpeg)

- `import` 文のエラー部分で `Alt` + `Enter` を入力すると、 `module-info.java` の補完が選択肢として現れる

![jigsaw.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/2d323134-bef9-e4fa-0c30-39e93ef8c135.jpeg)

- `module-info.java` が補完されて、コンパイルエラーが解消される

```java:module-info.java
module sample.jigsaw {
    requires org.apache.commons.lang3;
}
```

# application プラグインでモジュール化
## モジュール化前
```text:プロジェクト構成
|-build.gradle
`-src/main/java/
  `-sample/jigsaw/
    `-Main.java
```

```groovy:build.gradle
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'
}
```

```text:実行する
> gradle run
Hello Jigsaw!! : 5790539280
```

- `application` プラグインを使うと、 `run` タスクでプログラムを実行することができる
- メインクラスは `mainClassName` で指定している

## モジュール化後
```groovy:build.gradle
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()
    }
}
```

```text:実行
> gradle run
Hello Jigsaw!! : 9066254426
```

### 説明
```groovy:build.gradle
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` タスクを書き換える方法は、[公式のドキュメント](https://guides.gradle.org/building-java-9-modules/?_ga=2.50399337.1325336598.1527511393-492720480.1523802875#modify_the_code_run_code_task_to_use_modules) で紹介されている方法になる。

ただ、この方法には１つ大きな問題がある。

`Main` クラスを次のように修正すると、問題点が分かりやすくなる。

```java:Main.java
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
```

何も指定していないはずなのに、なんかいっぱい出た。

### 何が起こったのか？
```groovy:build.gradle
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` のヘルプを確認する。

```text: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` コマンドを組み立てようとしているが、

```text:runタスクが組み立てるコマンド
java [jvmArgsで指定した値] [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
    |--ここがJVMオプション(のつもり)------------------||--メインクラスの指定--------| |--残りはプログラムに渡す引数.....
```

`jvmArgs` で無理やり `--module` を指定したため、 Java 10 の `java` コマンドは次のように引数を解釈してしまっている。

```text:javaコマンドが解釈した引数の構成
java --module-path .........  --module ............. [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
     |--ここがJVMオプション--| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数扱い..........................
```

### 一旦話を整理する
- `jvmArgs` で `--module-path`, `--module` を指定すれば、一応プログラムは動く
- しかし、 `--module` を `jvmArgs` で指定してしまっているため、そこから後ろに繋げられる本来 JVM オプションなどに指定されるはずだった値が、全てプログラムの引数として扱われてしまっている
- これはつまり、引数を利用するプログラムを作って動かそうとしたときに大きな問題となる

### どうすべきか？
```text:問題のある状態
java --module-path .........  --module ............. [自動設定されるJVMオプション] [mainClassNameで指定された値] [argsで指定される値]
     |--ここがJVMオプション--| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数扱い..........................
```

これが、

```text:求めている状態
java --module-path .........  [自動設定されるJVMオプション] --module .............  [argsで指定される値]
     |--ここがJVMオプション------------------------------| |--メインクラスの指定--| |--残りは全部プログラムに渡す引数...
```

こうなるのが理想。

### JVM オプションの末尾に任意のパラメータを挿入できないか？
`jvmArgs` で指定した値は JVM オプションの先頭に渡される。

一方、自動設定されるものも含めた全てのオプションは `allJvmArgs` プロパティから参照できる。
`--module` オプションは `jvmArgs` ではなく、この `allJvmArgs` の末尾に追加すれば、うまくいくのではなかろうか？

```groovy:build.gradle
run {
    doFirst {
        main = "" // main クラスの指定を空にして
        jvmArgs = [
            "--module-path", classpath.asPath
        ]
        allJvmArgs += ["--module", mainClassName] // allJvmArgs の末尾に --module を追加
        classpath = files()
    }
}
```

```text:実行結果
> gradle run
Execution failed for task ':run'.
> java.lang.UnsupportedOperationException (no error message)
```

残念ながら、 `allJvmArgs` は読み取り専用なので書き換えることができなかった。

### mainClassName に指定する値に --module を入れられるか？
```groovy:build.gradle
run {
    doFirst {
        main = "--module $mainClassName"
        jvmArgs = [
            "--module-path", classpath.asPath
        ]
        classpath = files()
    }
}
```

`main` に、 `--module` を無理やりくっつけた値を渡してみる。

```text:実行結果
> gradle run
> Task :run FAILED
Unrecognized option: --module sample.jigsaw/sample.jigsaw.Main
```

どうやら `"--module sample.jigsaw/sample.jigsaw.Main"` という単一の引数として `java` コマンドに渡されてしまっているようで、不明なオプションとして処理されてしまった。

### args の先頭でメインクラスを指定する
```groovy:build.gradle
run {
    doFirst {
        main = ""
        jvmArgs = [
            "--module-path", classpath.asPath
        ]
        args = ["--module", mainClassName]
        args += ["foo", "bar"]
        classpath = files()
    }
}
```

`main` は空にして、 `args` の先頭で `--module` を指定する。

```text:実行結果
> gradle run

args = foo,bar
```

上手く動いた！

### 結論
```groovy:コマンドライン引数が不要
run {
    doFirst {
        jvmArgs = [
            "--module-path", classpath.asPath,
            "--module", mainClassName
        ]
        classpath = files()
    }
}
```

```groovy:コマンドライン引数が必要
run {
    doFirst {
        main = ""
        jvmArgs = [
            "--module-path", classpath.asPath
        ]
        args = ["--module", mainClassName]
        args += ["foo", "bar"]
        classpath = files()
    }
}
```

ファーストクラスでのサポートが待たれる。。。


## startScript を修正する
- ここまでの修正だけだと、 `run` タスクにしか対応できていない
- `installDist` で生成される起動スクリプトはクラスパス指定のままになっているので、そのままだと起動ができない

```bat:現状で出力される起動スクリプト(bat)
...
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 コマンドを実行している
- このため、このまま起動しようとしてもモジュールが見つからずにエラーになる

```text:この状態で実行するとエラー
> build\install\jigsaw\bin\jigsaw
エラー: メイン・クラスsample.jigsaw.sample.jigsaw.Mainを検出およびロードできませんでした
原因: java.lang.ClassNotFoundException: sample.jigsaw.sample.jigsaw.Main
```

- これに対応するために、[公式ドキュメント](https://guides.gradle.org/building-java-9-modules/?_ga=2.50399337.1325336598.1527511393-492720480.1523802875#modify_the_code_startscript_code_task_for_both_nix_and_windows_to_use_modules)では次のように `build.gradle` を修正する方法が紹介されている

```groovy: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'))
    }
}
```

- これで、起動スクリプトは次のように出力されるようになる

```bat:修正後の起動スクリプト(bat)
...
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%
```

- 起動スクリプトを叩くと、問題なく実行できる

```text:実行する
> build\install\jigsaw\bin\jigsaw.bat
Hello Jigsaw!! : 9282500878
```

- ただ、前述した `run` タスクの修正と同様の問題が発生している
- それについては後述

### 説明
- なにやら色々修正しているように見えるが、結局やっているのは次の２つだけ
    1. クラスパスを空にする
    2. 代わりにモジュールパスを設定する
- つまり、 `compileJava` とかでやっていたのと同じ修正をしているにすぎない

```groovy:build.gradle
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` という変数に格納するように出力される

```bat:defaultJvmOptsの出力結果
set DEFAULT_JVM_OPTS="--module-path" "%APP_HOME%/lib" "--module" "sample.jigsaw/sample.jigsaw.Main"
```

```groovy: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'))

        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` ブロックでは、この２つの違いを、それぞれのスクリプトで分けて実装している
- 結果、生成された起動スクリプトは次のようになる

```bat:出力された起動スクリプト(bat)
...
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%
```

### 引数の問題
```bat:出力された起動スクリプト(bat)
...
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS%  -classpath "%CLASSPATH%" sample.jigsaw/sample.jigsaw.Main %CMD_LINE_ARGS%
```

- よく見ると、 `run` タスクのときと同じように、引数の指定がおかしくなっている
- 実際、引数を出力するようにして実行すると次のようになる

```text
> build\install\jigsaw\bin\jigsaw.bat
args = -classpath,,sample.jigsaw/sample.jigsaw.Main
```

- `-classpath` 以下が引数として認識されてしまっている
- これは、一応次のように `build.gradle` を修正すれば改善する

```groovy: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` 以下の余計な部分を空文字に置換している

```bat:出力された起動スクリプト
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %JIGSAW_OPTS%   %CMD_LINE_ARGS%
```

```text:実行結果
> build\install\jigsaw\bin\jigsaw hoge fuga
args = hoge,fuga
```

# 単体テストのあるプロジェクトをモジュール化する
## まず最初に思いついたの
まず最初に思いついたのは、 `test` の方にも `module-info.java` を置いて、 `sample.jigsaw.test` のように `main` とは異なるモジュールを定義するという方法だった。

しかし、この方法は次のような問題があるのでダメだった。

1. 異なるモジュール間では、同じパッケージを宣言できない
    - `Hoge` クラスのテストクラス `HogeTest` を、 `Hoge` と同じパッケージで宣言するとエラーになる
2. `exports` していないパッケージはテストモジュールから参照できなくなってしまうので、内部 API のテストが書けない

1 の方は異なるパッケージに宣言すれば解決できそうだが、 2 の方が致命的なので、 `test` を異なるモジュールにするというのは無さそう。

## 推奨？されている方法
[公式ドキュメント](https://guides.gradle.org/building-java-9-modules/?_ga=2.50399337.1325336598.1527511393-492720480.1523802875#modify_the_code_compiletestjava_code_task_to_locally_alter_the_module) では、次のような方法が推奨？されている。

```text:プロジェクト構成
|-build.gradle
`-src/
  |-main/java/
  | |-module-info.java
  | `-sample/jigsaw/
  |   `-Hoge.java
  |
  `-test/java/
    `-sample/jigsaw/
      `-HogeTest.java
```

- `test` の方には `module-info.java` を置いていない

```groovy:build.gradle
...

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()
    }
}
```

```java:Hoge.java
package sample.jigsaw;

public class Hoge {
    
    public String method(int a, int b) {
        return a + " + " + b + " = " + (a + b);
    }
}
```

```java:HogeTest.java
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);
    }
}
```

```text:とりあえずコンパイルが通るか確認
> gradle compileTestJava
BUILD SUCCESSFUL in 3s
```


### 説明
なにやら `compileTestJava` に色々追加しているが、１つずつ説明していく。

```groovy:build.gradle
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
```groovy:build.gradle
"--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` を使ってルート・モジュールに追加する

```groovy:build.gradle
"--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 とかならコロン(`:`)になる

```groovy:build.gradle
"--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](http://openjdk.java.net/jeps/261#Patching-module-content)

#### --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 <モジュール名>=<追加で使用するモジュール>[,<追加で使用するモジュール>...]`

```groovy:build.gradle
"--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 のモジュールを読み込めるようにする|

## テストの実行
```groovy:build.gradle
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()
```groovy:build.gradle
test {
    useJUnitPlatform()
```

- こちらは、 Gradle で JUnit 5 を利用するときに必要になる記述

### --module-path
```groovy:build.gradle
'--module-path', classpath.asPath,
```

- これは、これまで同様モジュールパスを使用するための設定

### --add-modules
```groovy:build.gradle
'--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` で追加してあげる必要がある

```groovy:build.gradle
'--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath,
```

- **ちょっと注意**なのが、 `compileTestJava` とはちょっとだけ指定値が変わっている
- `compileTestJava` のときは `srcDirs` だったが、 `test` のときは `outputDir` を指定する

### --add-reads
```groovy:build.gradle
'--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` で定義する
    - 詳しくは[こちら](https://qiita.com/opengl-8080/items/1007c2b2543c2fe0d7d5#open)を参照
- 一応 `sample.jigsaw` の `module-info.java` で `opens sample.jigsaw` とすればエラーはなくなる
- しかし、テストのための設定を main の `module-info.java` に書くのはちょっとアレ
- ということで、実行時に `opens` の定義を追加できる `--add-opens` オプションを指定することで回避している

```groovy:build.gradle
'--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` パッケージにアクセスできるようにする|


# マルチプロジェクトをモジュール化する
ここまでの話を全て総合して、マルチプロジェクトをモジュール化すると下のようになった。

```text:プロジェクト構成
|-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
```

```groovy:/settings.gradle
include "foo", "bar"
```

```groovy:/build.gradle
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 プロジェクト**

```groovy:/foo/build.gradle
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, '')
    }
}
```

```java:/foo/src/main/java/module-info.java
module sample.jigsaw.foo {
    requires sample.jigsaw.bar;
}
```

```java:Main.java
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プロジェクト**

```groovy:/bar/build.gradle
apply plugin: 'java-library'

ext {
    moduleName = 'sample.jigsaw.bar'
    testPackageName = 'sample.jigsaw.bar'
}

dependencies {
    implementation "org.apache.commons:commons-lang3:3.7"
}
```

```java:/bar/src/main/java/module-info.java
module sample.jigsaw.bar {
    exports sample.jigsaw.bar;
    requires org.apache.commons.lang3;
}
```

```java:Bar.java
package sample.jigsaw.bar;

import org.apache.commons.lang3.RandomStringUtils;

public class Bar {
    
    public String method(int size) {
        return RandomStringUtils.random(size, "0123456789");
    }
}
```

```java:BarTest.java
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);
    }
}
```

------

**動作確認**

```text:テスト
> gradle test

BUILD SUCCESSFUL in 4s
```

```text:run
> gradle :foo:run

6569544042
```

# 結論
ファーストクラスサポートはよ

※[Gradle プラグインのリポジトリを見たらいくつかあった](https://plugins.gradle.org/search?term=jigsaw)

# 参考
- [Building Java 9 Modules](https://guides.gradle.org/building-java-9-modules/?_ga=2.47850215.1325336598.1527511393-492720480.1523802875)
- [Java SE 9の紹介: モジュール・システムを中心に](https://www.slideshare.net/miyakawataku/introduction-of-java-se-9-and-the-module-system)
- [Five Command Line Options To Hack The Java 9 Module System - blog@CodeFX](https://blog.codefx.org/java/five-command-line-options-to-hack-the-java-9-module-system/)
- [JEP 261: Module System](http://openjdk.java.net/jeps/261)