Gradle3におけるJavaプロジェクトのビルド入門(2) #gradle

前回は、JVM component modelによるJavaプロジェクトのビルドの入門をおこなった。
これまでのjavaプラグインに比べると記述量が増えてしまうが、遜色ないビルドができることは示せたと思う。
複数ライブラリー(コンポーネント)
前回、簡単にするためにmainというライブラリーだけに限ってプロジェクトのビルドを行いましたが、複数のライブラリー(コンポーネント)を指定することも可能でした。そこで、今回はまず複数のライブラリーを作成するプロジェクトを構築してみたいと思います。
前回用いたMainクラスとResクラスをそれぞれ別のライブラリーに分けることにします。
| クラス | ライブラリー |
|---|---|
sample.Main |
main |
sample.Res |
res |
また、resources/app.propertiesファイルはmainライブラリーに含めるものとします。
まず、mainというライブラリーとresというライブラリーの二つのコンポーネントを持ちますので、build.gradleのDSLは次のように書くことができます。
build.gradle
plugins {
id 'jvm-component'
id 'java-lang'
}
model {
components {
main(JvmLibrarySpec)
res(JvmLibrarySpec)
}
}
この状態で、componentsタスクを実行すると、前回に実行した時のように二つのライブラリーを持つコンポーネントが準備されていることがわかります。
$ gradle components
:components
------------------------------------------------------------
Root project
------------------------------------------------------------
JVM library 'main'
------------------
Source sets
Java source 'main:java'
srcDir: src/main/java
JVM resources 'main:resources'
srcDir: src/main/resources
Binaries
Jar 'mainJar'
build using task: :mainJar
targetPlatform: Java SE 8
tool chain: JDK 8 (1.8)
Jar file: build/jars/mainJar/main.jar
JVM library 'res'
-----------------
Source sets
Java source 'res:java'
srcDir: src/res/java
JVM resources 'res:resources'
srcDir: src/res/resources
Binaries
Jar 'resJar'
build using task: :resJar
targetPlatform: Java SE 8
tool chain: JDK 8 (1.8)
Jar file: build/jars/resJar/res.jar
Note: currently not all plugins register their components, so some components may not be visible here.
BUILD SUCCESSFUL
Total time: 0.756 secs
$
また、このコンポーネント構成を満たすためのディレクトリーは次のようになります。
$ tree -d src
src
├── main
│ ├── java
│ └── resources
└── res
├── java
└── resources
6 directories
$
Javaファイルの作成
前回作成したファイルをそれぞれ次の位置に配置し直します。
| 元のファイル | 新しいファイル |
|---|---|
src/main/java/sample/Main.java |
変更なし |
src/main/java/sample/Res.java |
src/res/java/sample/Res.java |
src/main/resources/app.properties |
変更なし |
この時のディレクトリー構成は次のようになります。
$ tree
src
├── main
│ ├── java
│ │ └── sample
│ │ └── Main.java
│ └── resources
│ └── app.properties
└── res
├── java
│ └── sample
│ └── Res.java
└── resources
8 directories, 3 files
$
ビルドの実行
ではビルドを実行します。
$ gradle build
:compileMainJarMainJava
/Users/mike/IdeaProjects/gradle-projects/gradle-self-training/g3-java-intro/src/main/java/sample/Main.java:23: エラー: シンボルを見つけられません
Res res = new Res();
^
シンボル: クラス Res
場所: クラス Main
/Users/mike/IdeaProjects/gradle-projects/gradle-self-training/g3-java-intro/src/main/java/sample/Main.java:23: エラー: シンボルを見つけられません
Res res = new Res();
^
シンボル: クラス Res
場所: クラス Main
エラー2個
:compileMainJarMainJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileMainJarMainJava'.
> 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.848 secs
$
ライブラリーを分けた瞬間にMainクラスが参照することのできた、Resクラスを参照することができなくなり、コンパイルエラーとなってしまいました。
コンポーネント間の依存性
これは、JVM component modelにおける、コンポーネント間のAPI依存性です。例えば、ライブラリーfooがライブラリーbarに依存する場合、fooをコンパイルするためにはbarをコンパイルする必要があるというものです。
Gradle2.8現在、コンポーネント依存性解決については以下の二つのタイプをサポートしています。
- 同一プロジェクトのライブラリー間の依存性解決
- 同一プロジェクトを構成するサブプロジェクトのライブラリー間の依存性解決
現在のところ、外部ライブラリー(例えばslf4jとかtwitter4jのようなライブラリー)の参照はサポートされていないようです。
外部ライブラリー依存解決についてGradle Forumに質問してみたところ、現在のコードベース(リリースされていない)においては、実装および簡単なテストはあるようですが、リリースについてはGradle2.10頃になるのではないかとのことです。もし、外部ライブラリーを利用したい方はナイトリービルドを用いるなどするとよいでしょう。
依存性の解決方法
依存性解決のDSLについては、SourceSetsDSLのように記述をおこないます。
今のこのサンプルにおいては、mainライブラリーがresライブラリーに依存しているために、次のように記述します。
plugins {
id 'jvm-component'
id 'java-lang'
}
model {
components {
main(JvmLibrarySpec) {
sources {
java {
dependencies {
library 'res'
}
}
}
}
res(JvmLibrarySpec)
}
}
では、このスクリプトの状態で、componentsタスクを実行してみます。
$ gradle components
:components
------------------------------------------------------------
Root project
------------------------------------------------------------
JVM library 'main'
------------------
Source sets
Java source 'main:java'
srcDir: src/main/java
dependencies:
library 'res'
JVM resources 'main:resources'
srcDir: src/main/resources
Binaries
Jar 'mainJar'
build using task: :mainJar
targetPlatform: Java SE 8
tool chain: JDK 8 (1.8)
Jar file: build/jars/mainJar/main.jar
JVM library 'res'
-----------------
Source sets
Java source 'res:java'
srcDir: src/res/java
JVM resources 'res:resources'
srcDir: src/res/resources
Binaries
Jar 'resJar'
build using task: :resJar
targetPlatform: Java SE 8
tool chain: JDK 8 (1.8)
Jar file: build/jars/resJar/res.jar
Note: currently not all plugins register their components, so some components may not be visible here.
BUILD SUCCESSFUL
Total time: 0.759 secs
$
mainライブラリーのコンポーネント情報のところに、resに依存するという情報が追加されました。
ちなみに、dependencies {}ブロックのdelegateオブジェクトはDependencySpecContainerであり、そこに指定できるのは単なるStringなので、次のような記述もできます。
main(JvmLibrarySpec) {
sources {
java {
dependencies {
library 'res'
library 'foo'
}
}
}
}
まあ、できることはできますが、指定されたfooライブラリーは存在しないので、コンパイル時にエラーになるだけです。評価フェーズにて特に検証を行っていないので、ライブラリー名のtypoには気をつけてください。
依存性解決プラスアルファ
サブプロジェクトへの依存性解決
例えばmainライブラリーが、サブプロジェクトsubのライブラリーfooに依存するライブラリーの場合は次のように記述します(これは次々回に詳しくやりましょう)。
main(JvmLibrarySpec) {
sources {
java {
dependencies {
library 'res'
project ':sub' library 'foo'
}
}
}
}
外部依存性の解決
先ほど、外部依存ライブラリーの解決はサポートされていないと書きましたが、教えてもらったテストケースを読むと次のように記述するようです。ただし、incubatingな機能なので、実際にリリースされる際には変更されている可能性があります。その点についてはご容赦ください。
main(JvmLibrarySpec) {
sources {
java {
dependencies {
library 'res'
library 'org.slf4j:slf4j-api:1.7.13'
}
}
}
}
transitive dependencies(推移的依存性解決)
ライブラリー間の依存性解決については、transitive dependencies(推移的依存性解決)にも対応していません。したがって、次のような依存関係を持つライブラリーがあるとします。
fooライブラリーはbarライブラリーに依存しているbarライブラリーはbazライブラリーに依存している
このような状況においては、fooライブラリーの依存性にbazライブラリーを加えておく必要があります。
ビルド
さて、依存性の解決も行ったことですし、ビルドをしてみましょう。
$ gradle build :compileResJarResJava :processResJarResResources :createResJar :resJar :compileMainJarMainJava :processMainJarMainResources :createMainJar :mainJar :assemble :check UP-TO-DATE :build BUILD SUCCESSFUL Total time: 0.859 secs $
mainライブラリーがresライブラリーに依存しているという情報を解析して、resライブラリー→mainライブラリーの順番でビルドされていることがわかります。
では、実際にjarファイルが作成されていることもあわせて確認してみましょう。
$ tree build/jars/
build/jars/
├── mainJar
│ └── main.jar
└── resJar
└── res.jar
2 directories, 2 files
$
実行
おそらくMANIFEST.MFファイルにMain-Class属性はないものと思われますので、-cpオプションでjarを読み込んで、mainクラスを自分で指定して、実行しましょう。また、まだ、incubatingな機能ですから、クラスのリンクを書き出す機能などないでしょうし、resライブラリーの方も、-cpオプションに入れ忘れないように気をつけます。
$ java -cp build/jars/mainJar/main.jar:build/jars/resJar/res.jar sample.Main こんにちわ mikeさん 僕はGradle $
前回と同じ結果が出力されました。
とはいえ、毎回、こんな長いコマンドを書くのも面倒ですね。というわけで、カスタムタスクを作ってしまいましょう。
task runApp(type: JavaExec) {
def jar = org.gradle.jvm.tasks.Jar
tasks.withType(jar) {
classpath += it.outputs.files
}
main = 'sample.Main'
}
このタスクを実行すると次のようになります。
$ gradle runApp :compileResJarResJava UP-TO-DATE :processResJarResResources UP-TO-DATE :createResJar UP-TO-DATE :resJar UP-TO-DATE :compileMainJarMainJava UP-TO-DATE :processMainJarMainResources UP-TO-DATE :createMainJar UP-TO-DATE :runApp こんにちわ mikeさん 僕はGradle BUILD SUCCESSFUL Total time: 1.095 secs $
見事、アプリケーションを実行してくれました。
注意
スクリプト中でJarの型をorg.gradle.jvm.tasks.Jarと書きました。Gradle2.8ではJarタイプのタスクの型はorg.gradle.api.tasks.bundling.Jarですが、ドキュメントのほうが若干先行していてorg.gradle.jvm.tasks.Jarとなっています(少し(5分くらい)ハマりました)。現在、JavaまわりのAPIが過渡期なので注意してください。
終わり
なんか、Gradle2.9rc-1のリリースノートを読み直したら、JavaOneに合わせてか、Jigsaw関連のexportモジュール関連のリリースノートが追加されていました。いや、もしかしたら、僕がJigsawわかってなかったから意識的に読まなかったのかもしれませんが…
ここまで、読んでこられて、かつ、Java9の事情にある程度詳しい皆様なら薄々感づいていらっしゃると思いますが、JVM component modelはGradleのJava9対応でもあるわけです。そのあたりのところを次回(あるのか????)やっていきたいとおもいます。