Gradleでマルチプロジェクトを実行可能JARにする

# この記事の目的

ビルドツールGradleを使ってマルチプロジェクトを管理し、成果物として実行可能JARを作成する手順の備忘録です。

- マルチプロジェクトの管理
- 外部JARに依存する実行可能JARの作成（依存JARをすべて内包したJAR、fat-JARにはしない）

がおもなテーマです。





## 記事作成の背景

JavaFX[^JavaFX]によってGUIアプリケーションを作る機会がありました。
アプリケーションは最終的にjavapackager[^javapackager]というツールによってEXE形式で配布するのですが、その前段階として、ダブルクリックで起動可能なJARを生成しておく必要がありました。

[^JavaFX]: 1 JavaFXの概要(リリース8) (https://docs.oracle.com/javase/jp/8/javafx/get-started-tutorial/jfx-overview.htm)
[^javapackager]: 自己完結型アプリケーションのパッケージ化 (https://docs.oracle.com/javase/jp/8/docs/technotes/guides/deploy/self-contained-packaging.html)

またアプリケーションは、内部処理がほとんど共通で、細かい挙動の異なる複数の種類を作成する必要がありました。
共通部分はcommon.jarとし、入り口となるJARをapp1.jar、app2.jar（それぞれcommon.jarに依存）などとしたかったのですが、社内のJenkinsを覗いて似た構成のマルチプロジェクトを探してみても、Gradleだけで完結したスクリプトがない状態でした。
commonのbuild.gradleを呼び、次いでapp1のbuild.gradleを呼ぶというようなシェルスクリプトがあるだけだったのです。

そのシェルスクリプトを真似すれば目的は果たせたのですが、学習のため、Gradleで完結するスクリプトを作成することとしました。





## 検証環境

- macOS 10.12.1 (Sierra)
- Java 1.8.0_102
- Gradle 3.2.1






# マルチプロジェクトを作成し、実行可能JARを作成する

単純化のため、appプロジェクトは1つだけで始めてみます。





## 最小限のマルチプロジェクトで動作確認する

まずは最小限のマルチプロジェクトを作成し、動作確認のため`hello`タスクを呼び出します。

作業ディレクトリー直下にapp, common, masterというディレクトリーを作り、master直下にbuild.gradleとsettings.gradleを作成します[^Macでtreeコマンド]：

[^Macでtreeコマンド]: Macでtreeコマンド - Qiita (http://qiita.com/kanuma1984/items/c158162adfeb6b217973)

```bash
master$ tree ../
../
├── app
├── common
└── master
    ├── build.gradle
    └── settings.gradle
```

settings.gradleには、サブプロジェクトとして認識させたいディレクトリーを指定します。
ルートプロジェクトの指定がありませんが、masterという名前のディレクトリーが自動でルートプロジェクトとして認識されるので、指定していません[^Gradleでマルチプロジェクト]：

[^Gradleでマルチプロジェクト]: Gradleでマルチプロジェクト - Qiita (http://qiita.com/shiena/items/371fe817c8fb6be2bb1e)

```groovy:settings.gradle
includeFlat 'app', 'common'
```

build.gradleでは、`allprojects`ブロック内で`hello`タスクを定義します[^doLastを使う理由]：

[^doLastを使う理由]: `leftShift`メソッドと`<<`演算子は非推奨となりました。(https://docs.gradle.org/3.2/release-notes#the-left-shift-operator-on-the-task-interface)

```groovy:build.gradle
allprojects {
    task hello {
        doLast {
            println "I`m ${project.name}."
        }
    }
}
```

ここまでできたら、masterディレクトリーで`hello`タスクを呼びます[^qの意味]。
以下のように表示されればOKです：

[^qの意味]: `println`の結果を見やすくするため、`-q`オプションを使っています。

```bash
master$ gradle -q hello
I`m master.
I`m app.
I`m common.
```

ちなみに、commonディレクトリーで`hello`タスクを呼ぶと、commonのタスクだけが呼ばれます。
この性質を使えば、特定プロジェクトのみのビルドも可能です：

```bash
common$ gradle -q hello
I`m common.
```





## appとcommonをEclipseのプロジェクトにする

サブプロジェクトにEclipseプラグインを追加します。
以下を`allprojects`ブロックの後ろに追記します：

```groovy:build.gradle
…

subprojects {
    apply {
        plugin 'eclipse'
    }
}
```

これだけでも、`eclipse`タスクが使えるようになって、Eclipseのプロジェクトを作ることが可能です：

```bash
master$ gradle eclipse
:app:eclipseProject
:app:eclipse
:common:eclipseProject
:common:eclipse

BUILD SUCCESSFUL

Total time: 2.067 secs
```

ただ、実際にはsrcディレクトリーも必要なので、gradleタスクで生成してみます[^initタスクを使わない理由]。
Javaプラグインを追加し、`initSrcDirs`タスクを下記のように作成します。

[^initタスクを使わない理由]: Gradleには`init`タスクがあらかじめ用意されていますが、余計なファイルを作成したくないので、自前で定義しました。

```groovy:build.gradle
…

subprojects {
    apply {
        plugin 'eclipse'
        plugin 'java'
    }

    task initSrcDirs {
        doLast {
            sourceSets.all {
                java.srcDirs*.mkdirs()
                resources.srcDirs*.mkdirs()
            }
        }
    }
}
```

`initSrcDirs`タスクを実行すると、src/main/javaやsrc/main/resourcesなどが作成されます。
それらのパスにすでにファイルが存在していても、空になることはないので、安全に使えます：

```bash
master$ gradle initSrcDirs
:app:initSrcDirs
:common:initSrcDirs

BUILD SUCCESSFUL

Total time: 0.909 secs

master$ tree ../
../
├── app
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           ├── java
│           └── resources
├── common
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           ├── java
│           └── resources
└── master
    ├── build.gradle
    └── settings.gradle
```

`initSrcDirs`タスクを定義する際に現れた変数について説明します。

- `sourceSets`は、Javaプラグインが定義するsrc/main/javaなどのディレクトリー体系です。
- `mkdirs()`は、`srcDirs`がjava.io.Fileのリストなので、java.io.Fileのmkdirsメソッドを呼び出しています。

それ以外の構文（たとえばイテレーションの`*.`）が気になる方はGroovyの文法を調べてみてください。

srcディレクトリーができた状態で再度`eclipse`タスクを呼ぶと、きちんとクラスパスも通ります（先ほどはなかった`eclipseClaspath`タスクが呼ばれています）：

```bash
master$ gradle eclipse
:app:eclipseClasspath
:app:eclipseJdt
:app:eclipseProject
:app:eclipse
:common:eclipseClasspath
:common:eclipseJdt
:common:eclipseProject
:common:eclipse

BUILD SUCCESSFUL

Total time: 0.994 secs
```





## appプロジェクトをcommonプロジェクトへ依存させる

マルチプロジェクト管理で実現したかったことの本質に取りかかります。
appをcommonへ依存させます。




### 失敗するケース

まずは、失敗ケースを再現するため、build.gradleを変更せずに以下の2クラスを作成します。
Constant.javaはcommon配下へ、Main.javaはapp配下へ作成します。
当然ながら、Eclipseでも互いの依存関係は認識できていないので、補完できないうえエラーも出ます：

```java:Constant.java
package com.github.kazuma1989.dec10_2016.common;

public class Constant {
    public static final String NAME = "common";
}
```

```java:Main.java
package com.github.kazuma1989.dec10_2016.app;

import com.github.kazuma1989.dec10_2016.common.Constant;

public class Main {
    public static void main(String[] args) {
        System.out.println(Constant.NAME);
    }
}
```

この状態で`jar`タスクを実行すると、期待どおりにビルド失敗です。
`jar`タスクの前段階の`compileJava`タスクで失敗しています：

```bash
master$ gradle jar
:app:compileJava
/Users/kazuma/qiita/2016/dec10/app/src/main/java/com/github/kazuma1989/dec10_2016/app/Main.java:3: エラー: パッケージcom.github.kazuma1989.dec10_2016.commonは存在しません
import com.github.kazuma1989.dec10_2016.common.Constant;
                                              ^
/Users/kazuma/qiita/2016/dec10/app/src/main/java/com/github/kazuma1989/dec10_2016/app/Main.java:7: エラー: シンボルを見つけられません
        System.out.println(Constant.NAME);
                           ^
  シンボル:   変数 Constant
  場所: クラス Main
エラー2個
:app:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.966 secs
```




### 成功するケース

次に、ビルドを成功させます。
`subprojects`ブロックの最後へ以下のように追記して、commonへの依存関係を定義します：

```groovy:build.gradle
…

subprojects {
    …

    // common以外の設定
    if (project.name in ['common']) return

    dependencies {
        compile project(':common')
    }
}
```

今度は`jar`タスクが成功し、作成したJARも問題なく動作します。
プロジェクト名の昇順ではなく、commonのJAR化が先に実行されています：

```bash
master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 1.154 secs

master$ java -cp ../app/build/libs/app.jar com.github.kazuma1989.dec10_2016.app.Main
common
```

`jar`タスクは必ずしもmasterから呼ぶ必要はなく、appのものだけを呼んでも構いません。
その場合も、commonをコンパイルしてからappがJAR化されるので、きちんと成功します：

```bash
master$ gradle clean :app:jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.938 secs

master$ cd ../app/

app$ gradle clean jar
:app:clean
:common:compileJava UP-TO-DATE
:common:processResources UP-TO-DATE
:common:classes UP-TO-DATE
:common:jar UP-TO-DATE
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.922 secs
```

依存関係を設定後、再度`eclipse`タスクを実行すれば、Eclipseでのエラーも解消し、コード補完も使えるようになります。





## マニフェストを追加してappを実行可能JARにする

`jar`タスクにマニフェストの設定をし、実行可能JARを生成します。




### うまくいくこともあるが、基本は失敗するケース

build.gradleの最後に最低限の記述だけを追加して、とにかく`jar`タスクを実行してみます：

```groovy:build.gradle
…

project(':app') {
    jar {
        manifest {
            attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
        }
    }
}
```

```bash
master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 3.177 secs
```

作成したJARを`java`コマンドで呼び出すと、Mainクラスが呼ばれます：

```bash
master$ java -jar ../app/build/libs/app.jar 
common
```

しかし、これが動作したのはたまたまです。
common.jarへの依存情報をマニフェストに記載しないと、Constantクラスが見つからないからです。
今回は、MainクラスがConstant文字列定数しか参照していないため、コンパイル時にその値がMainクラスに保持されてしまったと考えられます。




### common.jarへの依存情報を設定し、常にうまくいくケース

通常はNoClassDefFoundErrorとなります。
コンパイルは成功するが、実行時に失敗するということです。

たとえば以下は、commonに基底クラス`AbstractApplication`を作成し、appで継承して利用した場合です：

```bash
master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 1.188 secs

master$ java -jar ../app/build/libs/app.jar 
Exception in thread "main" java.lang.NoClassDefFoundError: com/github/kazuma1989/dec10_2016/common/AbstractApplication
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at com.github.kazuma1989.dec10_2016.app.Main.main(Main.java:9)
Caused by: java.lang.ClassNotFoundException: com.github.kazuma1989.dec10_2016.common.AbstractApplication
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 13 more

master$ tree ../app/build/libs/
../app/build/libs/
└── app.jar
```

これを解決するため、`jar`ブロック内を以下のように変更します。
マニフェストには`attributes 'Class-Path'`が、タスクには`copy`操作が追加されています：

```groovy:build.gradle
…

project(':app') {
    jar {
        manifest {
            attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
            attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
        }

        doLast {
            copy {
                from configurations.runtime
                into destinationDir
            }
        }
    }
}
```

`configurations.runtime`は、appプロジェクトの実行時依存についての情報を持っています。
ここには、`dependencies`ブロック内で`compile`として指定したcommonプロジェクトの情報も含まれています。
コンパイル依存は実行時依存でもあるからです。

`attributes 'Class-Path'`のところでは、`configurations.runtime`の内容をスペース区切りで文字列連結しています。
なのでマニフェストの中身は`Class-Path: common.jar`となります（common以外にも依存関係を追加していた場合は、`Class-Path: common.jar another.jar extra.jar`のようになる）。

マニフェストに`Class-Path: common.jar`と書いてあるJARは、自分と同じディレクトリーのcommon.jarを探しにいくので、`copy`ブロックでapp.jarと同じディレクトリーにcommon.jarをコピーしています。

マニフェストの追記と依存JARのコピーによって、今度はJARの実行が成功します。
libsディレクトリーを持ち運べば、アプリケーションが完結する状態になっています：

```bash
master$ gradle clean jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.952 secs

master$ java -jar ../app/build/libs/app.jar 
common

master$ tree ../app/build/libs/
../app/build/libs/
├── app.jar
└── common.jar
```






# プロジェクトごとの設定値をルートプロジェクトから分離し、新しいプロジェクトを追加しやすくする

ここまでで、ひとまずやりたかったことはできるようになりました。
次はapp2プロジェクトを追加し、現実の例に近づけたいところですが、まずはスクリプトの整理をします。





## appのみの設定をapp/build.gradleに分離する

ひとまず動かしたかったので、appプロジェクトの設定をmasterプロジェクトのbuild.gradleに記載していました。
ですが、このままでは、サブプロジェクトが増えるたびにmaster/build.gradleに似たような記述が増えていってしまいます。

サブプロジェクトを増やしても、変更はsettings.gradleのみに留めたいところです。
そのため、appプロジェクトの設定はappディレクトリーのbuild.gradleに分離します：

```bash
master$ tree ../ -L 2
../
├── app
│   ├── build.gradle
│   └── src
├── common
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle
```




### 失敗するケース

次のようにすると、一見成功しそうですが、失敗します。

app/build.gradleには、プロジェクトごとに異なるメインクラスのみを指定します。
master/build.gradleでは、`project(':app')`ブロックに書いていた内容を`subprojects`ブロックに移します：

```groovy:app/build.gradle
jar {
    manifest {
        attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
    }
}
```

```groovy:master/build.gradle
…

subprojects {
    …

    jar {
        manifest {
            attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
        }

        doLast {
            copy {
                from configurations.runtime
                into libsDir
            }
        }
    }
}

// project(':app') {
// }
```

33行目（`attributes 'Class-Path'`の箇所）でエラーが発生します：

```bash
master$ gradle clean jar

FAILURE: Build failed with an exception.

* Where:
Build file '/Users/kazuma/qiita/2016/dec10/master/build.gradle' line: 33

* What went wrong:
A problem occurred evaluating root project 'master'.
> Could not resolve all dependencies for configuration ':app:runtime'.
   > Project :app declares a dependency from configuration 'compile' to configuration 'default' which is not declared in the descriptor for project :common.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.837 secs
```

appで利用している`configurations.runtime`が解決できないためエラーとなったようです。
これは、commonプロジェクトでJavaプラグインを呼び出す前に、appプロジェクトの設定が実行されたためです。

```groovy:master/build.gradle（app/build.gradle分離前）
subprojects {
    // app -> commonの順でプロジェクトの設定をしているが（下に続く）
    apply {
        plugin 'java'
        …
    }

    …
}

project(':app') {
    // このブロックはcommonへのJavaプラグイン適用が済んだ後に実行されるため、問題なかった。
    jar {
        …
    }
}
```

```groovy:master/build.gradle（app/build.gradle分離後）
subprojects {
    // app -> commonの順でプロジェクトの設定をしている。
    apply {
        plugin 'java'
        …
    }

    // project(':app')にあった処理をsubprojectsブロックに持ってきたため、commonへのJavaプラグイン適用より前に実行されることとなり、エラー。
    jar {
        …
    }
}
```




### 成功するケース

`subprojects`ブロックを、「commonとappを含めたすべてのサブプロジェクト」と「common以外のサブプロジェクト」で分けることにより、うまくいくようになります。
ブロックの意味もはっきりするので、可読性としてもこちらのほうがよいでしょう：

```groovy:master/build.gradle
// commonとappを含めたすべてのサブプロジェクト
subprojects {
    apply {
        plugin 'java'
        …
    }

    …
}

// common以外のサブプロジェクト
subprojects {
    if (project.name in ['common']) return

    dependencies {
        …
    }

    jar {
        …
    }
}
```

```bash
master$ gradle clean jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.94 secs

master$ java -jar ../app/build/libs/app.jar 
common
```





## app/build.gradleをgradle.propertiesに置き換える

プロジェクトごとの設定が値だけの違いしかないので、app/build.gradleをgradle.propertiesに置き換えてしまいます。
gradle.propertiesは、build.gradle同様、プロジェクトディレクトリー直下に存在すると自動で読み込まれるプロパティーファイルです：

```bash
master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties
│   └── src
├── common
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle
```

```properties:app/gradle.properties
jar.manifest.attributes.Main-Class=com.github.kazuma1989.dec10_2016.app.Main
```

build.gradleからは、`getProperty`メソッドによって値を取得します。

注意点は、`afterEvaluate`ブロックを使う必要があることです。
プロジェクトは、masterプロジェクト、appプロジェクトの順で読み込まれるため、master/build.gradleの時点ではapp/gradle.propertiesがまだ読み込まれていないからです。
`afterEvaluate`ブロック内は、各プロジェクトの評価後（プロジェクトの設定を完了した後）に実行されるため、うまくいきます：

```groovy:master/build.gradle
…

subprojects {
    …

    afterEvaluate {
        jar {
            manifest {
                attributes 'Main-Class': getProperty('jar.manifest.attributes.Main-Class')
                attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
            }

            …
        }
    }
}
```






# まとめ

最終的に、マルチプロジェクトの構成は次のようになりました：

```bash
master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties   // 各プロジェクトの設定値を書く
│   └── src                 // 各アプリケーションのソースコード。commonに依存する
├── common
│   └── src                 // 共通部品
└── master
    ├── build.gradle        // 全プロジェクトで利用するタスクを定義する
    └── settings.gradle     // マルチプロジェクトに含めるディレクトリーを指定する
```

masterディレクトリーで`gradle jar`とすればすべてのJARが生成されます。
各プロジェクトのディレクトリーで`gradle jar`とするか、`gradle :common:jar`とすることで個別のJARを生成できます。
生成したJARは、libsディレクトリーに依存するJARとまとまっているため、ディレクトリーごと持ち運べばそのまま使えます。

今後、さらにプロジェクトを増やしていくと、以下のようになります：

```bash
master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties
│   └── src
├── app2
│   ├── gradle.properties
│   └── src
├── app3
│   ├── build.gradle        // 特別な設定が必要なら、スクリプトを配置してもよい
│   ├── gradle.properties
│   └── src
├── common
│   ├── build.gradle        // commonが依存するライブラリーなどを指定する
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle
```

以上です。