こんにちは。サイオステクノロジー技術部 武井(Twitter:@noriyukitakei)です。今回は、ビルドツールのGradleについて、書きたいと思います。
※ ちなみにGradleの活用事例は以下の記事にあります。
多分わかりやすいマイクロサービス入門 〜 マイクロサービスフレームワーク「Azure Service Fabric」でLINE風なチャットアプリを作ろう!! 〜
Gradleとは?
Gradleは、オープンソースのビルドシステムです。似たようなツールにMavenというものがあり、こちらが現在ではデファクトスタンダードですが、Gradleの人気もジワジワ上がりつつあります。で、Gradleが何をしてくれるかというと、例えば、Javaのjarファイルやwarファイルといったものを作ってくれます。
では、Mavenと何が違うのでしょうか?Mavenはビルドの手順を定義するための言語として、XMLを用います。しかし、XMLでは、ぱっと見ただけでは、どのような処理をしているかなかなかわかりにくいです。
しかしながら、GradleはGroovyという言語によってビルドの手順を定義します。つまり、Gradleではビルドの手順を設定ではなく、Groovyという言語によって処理として記述します。Gradleの設定ファイルはDSLなのです。GroovyはJavaと互換性のある言語で、Javaを知っている人であればスラスラと書けるのではないかと思います。
そんなGradleですが、以下のデメリットもあるというのが個人的所感であります。
- Groovyは、シンタックスシュガー(省略した書き方)が多くわかりにくい。
- そもそもGroovyを知らないと理解できない
同じようなことを思っていらっしゃる方のため、本記事では、そのあたりの不便さも含めて、Gradleをわかりやすく解説したいと思います。
インストール方法
Gradleは、sdkmanというパッケージマネージャーを使ってインストールします。まず、そのsdkmanをインストールします。
# curl -s https://get.sdkman.io | bash ... All done! Please open a new terminal, or run the following in the existing one: source "/path/to/.sdkman/bin/sdkman-init.sh" Then issue the following command: sdk help Enjoy!!!
上記のように表示されればOKです。上記の/path/toは、コマンドを実行したユーザーのホームディレクトリと読み替えてください。
# source "/path/to/.sdkman/bin/sdkman-init.sh"
Gradleをインストールします。
# sdk install gradle
以上で、Gradleのインストールは完了です。
まず使ってみる
手っ取り早く、Gradleで、ハロワのJavaアプリケーションをビルドするものを作ってみます。
# mkdir GraddleApp # cd GradleApp
Javaのプロジェクトをビルドするための、テンプレートを作成します。
# gradle init --type java-application
gradle initはテンプレートを作成するためのコマンドで、それに引き続いて、–typeのあとに、アプリケーションのタイプを指定します。今回はJavaなので、java-applicationとしましたが、例えば、Scalaの場合は、scala-libraryと指定します。
上記のコマンドを実行すると、以下のファイル及びディレクトリが作成されます。
GradleApp ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main │ └── java │ └── App.java └── test └── java └── AppTest.java
重要なものが2つあります。
一つはbuild.gradleです。これは、ビルドの手順を定義するファイルで、Groovyで記述します。Mavenでいうところのpom.xmlのようなもので、ビルド対象やビルドしたものの出力先など、ビルドの動作に関する全てを司る非常に重要なファイルです。
もう一つは、srcディレクトリです。ここにビルド対象のソースコードを格納します。このディレクトリ構成、どこかに見覚えのある方もいらっしゃるかもしれませんが、Mavenのソースディレクトリ構成と全く同じです。ここのところ、GradleはビルドツールのデファクトスタンダードであるMavenとの互換性を意識していると言えます。
src/main/java/App.javaのソースコードを見てみます。
public class App { public String getGreeting() { return "Hello world."; } public static void main(String[] args) { System.out.println(new App().getGreeting()); } }
すでにハロワっぽいものが出来上がっています。
まずコンパイルしてみましょう。以下のコマンドを実行します。
# gradle compileJava
するとbuildディレクトリが作成され、コンパイルしたクラスApp.classが以下のディレクトリ構成で作成されます。
build └── classes └── java └── main └── App.class
次に実行してみましょう。以下のコマンドを実行します。
# gradle run > Task :run Hello world. BUILD SUCCESSFUL in 0s 2 actionable tasks: 1 executed, 1 up-to-date
Hello world.と表示れました。
では、次に、jarを作ってみましょう。以下のコマンドを実行します。
# gradle jar
するとbuildディレクトリが作成され、jarファイルが以下のディレクトリ構成で作成されます。
build └── libs └── GradleApp.jar
このjarを実行してみましょう。ただし、Manifestファイルを定義していないので、単体で実行することはできません(Manifestファイルを作成する方法もGradleにはありますが、ここでは割愛します)。なので、以下のようにクラスパスを指定する形で実行します。
# java -classpath build/libs/GradleApp.jar App Hello world.
ヮ(゚д゚)ォ!Hello world.と表示されました。
build.gradleについて
先程手っ取り早くハロワ作ったのですが、その中 で一番大切なファイルbuild.gradleについて見てみたいと思います。
まずプラグインの定義です。
plugins { id 'java' id 'application' }
こちらの記述はプラグインを定義しています。Gradleは様々なプラグインを利用します。例えば「gradle complieJava」として、Javaのソースをコンパイルできたのは、ここで、「java」のプラグインを定義しているからです。また「gradle run」として、Javaアプリケーションを実行できたのは、「application」プラグインのおかげです。
次に依存性の定義です。
dependencies { compile 'com.google.guava:guava:23.0' testCompile 'junit:junit:4.12' }
これは、ビルドするために必要なライブラリを定義しています。アプリケーションをビルドするためには様々なライブラリが必要になります。dependenciesに記載しておけば、ライブラリをインターネット上のリポジトリ(リポジトリについては後述)からダウンロードしてきてくれます。定義の書式がちょっとわかりにくいですが、以下のような形式なんですね。
グループ名:名前:バージョン
グループ名は、そのライブラリを開発したり所有したりしている企業や団体、名前はその名の通りライブラリの名前、バージョンはライブラリのバージョンです。com.google.guava:guava:23.0はcom.google.guavaという団体が所有しているguavaという名前のライブラリで、バージョンは23.0という意味になります。
気になるのが「グループ名:名前:バージョン」の前にある「compile」とか「testCompile」です。
compileは、アプリケーションをコンパイルするときに参照・ダウンロードされます。
testCompileは、テストのときだけ参照・ダウンロードされるライブラリを定義します。ここでは、junitが定義されていますが、これはテストのときだけ必要で、アプリケーションビルド時には不要なので、testCompileとしています。
次に、リポジトリの定義です。
repositories { jcenter() }
これは、JCenterというリポジトリからライブラリをダウンロードしますよという意味です。
repositories { mavenCentral() }
これはMavenセントラルリポジトリというリポジトリからライブラリをダウンロードしますよという意味です。
JCenterとMavenセントラルリポジトリのどちらを使っても問題ありません。自分のほしいライブラリがあるリポジトリを使うとよいでしょう。
タスク
Gradleはタスクというのものを定義できます。タスクは関数やメソッドのようなもので、複数の処理をひとまとめにしたものです。今まで「run」とか「compileJava」など実行してきたものは、みんなタスクと呼ばれるものです。このタスクは自ら定義することもできます。
書式は以下のとおりです。
task タスク名 { 処理 }
基本のハロワからいきましょう。
task hello { println "Hello World!!" }
以下のようにして実行します。
# gradle タスク名
つまりこんな感じです。
# gradle hello > Configure project : Hello World!! BUILD SUCCESSFUL in 0s
ちゃんとハロワが表示されました。
doFirstとdoLast
Gradleでタスクを定義するときは、だいたいdoFirstもしくはdoLastを定義することが多いようです。doFirstはタスクの一番最初の行う処理、doLastはタスクの一番最後に行う処理です。書式は以下のような感じです。
task タスク名 { doFirst { 一番最初に行う処理を書く } doLast { 一番最後に行う処理を書く } }
記述例は以下になります。
task hello { doFirst { println "First!!" } doLast { println "Last!!" } }
実行してみます。
# gradle hello > Task :hello First!! Last!! BUILD SUCCESSFUL in 1s 1 actionable task: 1 executed
最初にFirst!!が表示されて、次にLast!!が表示されました。
以下のように書いても同じ結果になります。
task hello { } hello.doFirst { println "First!!" } hello.doLast { println "Last!!" }
タスク同士の依存関係
あのタスクを実行するときには、必ずこのタスクの実行後でないとダメといった記述も可能です。以下が書式です。タスク2を実行すると、必ずタスク1が実行された後に、タスク2が実行されるようになります。
task タスク1 { 〜処理を書く〜 } task タスク2 (dependsOn:'タスク1') { 〜処理を書く〜 }
実際にやってみましょう。
task greeting1 { println "Good morning!!" } task greeting2 (dependsOn:'greeting1') { println "Good evening!!" }
実行してみます。
# gradle greeting2 > Configure project : Good morning!! Good evening!!
greeting2というタスクを実行すると、先に依存関係にあるgreeting1というタスクが実行された後にgreeting2が実行されているのがわかります。
タスクの中で、自身のタスクの情報を取得
タスクを実行するとき、自身のタスクの情報を取得することができます。例えば、実行しているタスク名を取得するには以下のように定義します。
task hoge { task -> println task.name }
上記のようにタスクのクロージャーの中にtaskを引数としたラムダ式を定義することで実現できます。実行すると以下のような結果になります。
$ gradle -q hoge hoge
ちなみに以下のように書いても同義です。(型を明示的に指定しただけです)
task hoge { Task task -> println task.name }
タスクを定義するときの<<について
タスクを定義するときに、よく<<っていのが出てきます。以下のような感じです。
task hoge << { println "hoge" }
上記の記法は以下と同義なのです。
task hoge { doLast { println "hoge" } }
ふーん、そうなんだって感じです。Groovyはこんなシンタックスシュガーが多くて、ちょっとわかりにくいです(´・ω・`)
ちなみに<<をつけないでタスクを実行すると、タスクの実行を指定してないにもかかわらず、実行されてしまいます。どういうことかといいますと、
task hoge { println "hoge" } task fuga { println "fuga" }
このように定義したタスクを実行してみると、
$ gradle -q hoge hoge fuga
fugaのタスクを指定してないのに、なぜか実行されています。
以下のように書き直してみます。
task hoge << { println "hoge" } task fuga << { println "fuga" }
hogeタスクを実行してみると・・・
$ gradle -q hoge hoge
うん、想定通り、hogeしか実行されてないです。
カスタムタスク
Gradleではタスクを定義する方法がも一つあります。それがこのカスタムタスクです。前述したtask {}というように、クロージャーの中で定義する方法では、外部からプロパティを与えてタスクの挙動を変えるといったことができません。そんなことを実現するのが、このカスタムタスクです。
書式は以下のとおりです。DefaultTaskというクラスを継承して、カスタムタスクを定義した新たなクラスを作成します。
class タスク名 extends DefaultTask { void メソッド名(引数) { 〜処理〜 } @TaskAction def メソッド名(引数) { 〜処理〜 } }
とりあえず実装してみます。あいさつをするカスタムタスクを定義してみます。
class GreetingTask extends DefaultTask { private String greeting void greeting(g) { greeting = g } @TaskAction def greet() { println greeting } }
定義したカスタムタスクを使うためには以下のようにします。
task hello(type: カスタムタスクのクラス名) { メソッド名 引数 }
つまり、こんな感じになります。
task hello(type: GreetingTask) { greeting "Hello" }
実行してみます。
# gradle -q hello Hello
カスタムタスクで定義したメソッド名greetingを呼び出して、引数に定義した文字列が、そのまま標準出力に表示されます。
Gradleにはいくつかこのようなタスクが標準で実装されています。その中の一つであるCopyタスクを使ってみたいと思います。以下のようにbackupディレクトリを作成して、そこにsrc/main/java/App.javaをコピーします。
├── backup ├── build.gradle ├── settings.gradle └── src └── main └── java └── App.java
build.gradleファイルに以下のタスクを追加します。なんとなくわかると思いますが、fromにコピー元、intoにコピー先を指定します。
task copySrc(type: Copy) { from 'src/main/java/App.java' into 'backup' }
以下のように実行すると、backupディレクトリにApp.javaがコピーされます。
# gradle -q copySrc
includeでコピー対象ファイルを指定することもできます。
task copySrc(type: Copy) { from 'src/main/java/App.java' into 'backup' include '*.java' }
excludeでコピー対象から除外することもできます。以下のように書くと、何もコピーされません。
task copySrc(type: Copy) { from 'src/main/java/App.java' into 'backup' exclude '*.java' }
ソースディレクトリと出力先の変更
ソースディレクトリと、ソースをコンパイルしたクラスの出力ディレクトリを変更してみます。以下のような構成のディレクトリを作成します。ソースディレクトリはsrc(デフォルトだとsrc/main/java)、ソースをコンパイルしたクラスの出力ディレクトリはout/classesとします。
GradleApp ├── build.gradle ├── out │ └── classes ├── settings.gradle └── src └── App.java
build.gradleに以下のように記載します。java.srcDirsにソースディレクトリ、output.classesDirにソースをコンパイルしたクラスの出力ディレクトリを指定します。
sourceSets { main { java.srcDirs = ['src'] output.classesDir = 'out/classes' } }
コンパイルしてみますと、out/classesにApp.classが出来上がります。
# gradle compileJava
依存関係の管理
ところで、build.gradleに以下の記述があったのを覚えてますでしょうか?
dependencies { compile 'com.google.guava:guava:23.0' testCompile 'junit:junit:4.12' }
このcompileとかtestCompileというのは、confugrationsオブジェクトというもので管理されています。以下のタスクを実行してみてください。
task showConfigurations { println configurations }
んで、実行してみてください。
# gradle -q showConfigurations [configuration ':annotationProcessor', configuration ':apiElements', configuration ':archives', configuration ':compile', configuration ':compileClasspath', configuration ':compileOnly', configuration ':default', configuration ':implementation', configuration ':runtime', configuration ':runtimeClasspath', configuration ':runtimeElements', configuration ':runtimeOnly', configuration ':testAnnotationProcessor', configuration ':testCompile', configuration ':testCompileClasspath', configuration ':testCompileOnly', configuration ':testImplementation', configuration ':testRuntime', configuration ':testRuntimeClasspath', configuration ':testRuntimeOnly']
なんか色々出てきました。おなじみのcompileとかtestCompileとか出てきました。では、compileオブジェクトの中には何が入っているのでしょうか?以下のタスクをbuild.gradleに書いてください。
task showConfigurations2 { configurations.compile.each { println it } }
実行してみると・・・
$ gradle -q showConfigurations2 /Users/ntakei/.gradle/caches/modules-2/files-2.1/com.google.guava/guava/23.0/c947004bb13d18182be60077ade044099e4f26f1/guava-23.0.jar /Users/ntakei/.gradle/caches/modules-2/files-2.1/com.google.code.findbugs/jsr305/1.3.9/40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf/jsr305-1.3.9.jar /Users/ntakei/.gradle/caches/modules-2/files-2.1/com.google.errorprone/error_prone_annotations/2.0.18/5f65affce1684999e2f4024983835efc3504012e/error_prone_annotations-2.0.18.jar /Users/ntakei/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/976d8d30bebc251db406f2bdb3eb01962b5685b3/j2objc-annotations-1.1.jar /Users/ntakei/.gradle/caches/modules-2/files-2.1/org.codehaus.mojo/animal-sniffer-annotations/1.14/775b7e22fb10026eed3f86e8dc556dfafe35f2d5/animal-sniffer-annotations-1.14.jar
dependenciesのcompileのあとに指定したライブラリ本体及びそれに依存するライブラリのパスが取得できました。つまり、configurationsオブジェクトは依存ライブラリの様々な情報を持っているのです。
ちなみにconfigurationsは自分で定義できたりもします。build.gradleに以下のように書いてください。
configurations { conf1 }
加えて以下のように定義をしてください。
dependencies { compile 'com.google.guava:guava:23.0' conf1 'org.slf4j:slf4j-api:1.7.12' testCompile 'junit:junit:4.12' }
以下のタスクを定義してください。先のcompileの部分をconf1に変更しただけです。
task showConfigurations3 { configurations.conf1.each { println it } }
上記のタスクを実行します。
$ gradle -q showConfigurations3 /Users/ntakei/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.12/8e20852d05222dc286bf1c71d78d0531e177c317/slf4j-api-1.7.12.jar
dependenciesのところで定義したライブラリの情報が表示されました。compileやcompileTestと同様のものを独自に定義できることがわかりました。
ちなみに依存ライブラリを取得してきて、他のディレクトリにコピーすると言ったことも可能です。以下のタスクを定義することで実現できます。compile時に必要なライブラリ(dependenciesのcompileで指定されたライブラリ)をlibディレクトリにコピーします。
task copyDeps(type: Copy,dependsOn: configurations.compile) { configurations.compile.each { from it } into "lib" }
先述したCopyタスクを使っています。「dependsOn: configurations.compile」は、dependenciesのcompileで指定されたライブラリをダウンロードした後に、Copyタスクを実行してくださいという意味です。これを入れないと、当然ながら、ライブラリをダウンロードする前にコピーが走ってしまい、何も起こりません。
実行可能なjarを作ってみる
「まず使ってみる」でもjarを作りましたが、単体では実行できませんでした。今回は単体で実行出来るjarを作ります。
apply plugin: 'java' repositories { jcenter() } dependencies { compile 'org.apache.commons:commons-lang3:3.7' } jar { from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } manifest { attributes('Main-Class': 'App') baseName "App" } } defaultTasks 'clean', 'jar'
先程のハロワをビルドすることを想定しています。一番最初にご紹介したハロワをビルドするスクリプトと違う点は、jar{}の部分ですね。一つずつ説明します。
from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
jarに含める依存ライブラリを指定します。つまりdependenciesで指定したライブラリをjarに含めたい場合に指定します。configurations.compileは、dependenciesのcompileで指定した依存ライブラリのリストが含まれています。collectメソッドでそれを一つずつ取り出し、続くクロージャーのの中で処理を記載します。
クロージャーの中のitはconfigurations.compile.collectで取り出した依存ライブラリのフルパスが入っています。以下の書き方と同義になります。
from configurations.compile.collect { it -> it.isDirectory() ? it : zipTree(it) }
引数を省略すると、itという変数にリストの中身が入ってくるようです。
it.isDirectory() ? it : zipTree(it)は、よくある三項演算子で、もしディレクトリだったら何もせず、ディレクトリでなかったら展開する、つまりjarの中身をバラすということになります。
manifest { attributes('Main-Class': 'App') baseName "App" }
manifestでは、jarのマニフェストファイルに記載する内容を定義します。attributes(‘Main-Class’: ‘App’)はマニフェストファイルの中にMain-Class: Appと記載するのと同じ処理になります。つまりjarを実行したときにmainメソッドがあるクラスを定義しています。
baseName “App”は出来上がるjarファイルの名前を定義します。この場合、App.jarというjarファイルが出来上がります。いつものようにgradle buildしてみますとApp.jarが出来上がります。そのApp.jarを解凍してみると、以下のようなディレクトリ構成になっています。
. ├── App.class ├── App.jar ├── META-INF │ ├── LICENSE.txt │ ├── MANIFEST.MF │ ├── NOTICE.txt │ └── maven │ └── org.apache.commons │ └── commons-lang3 │ ├── pom.properties │ └── pom.xml └── org └── apache └── commons └── lang3 ├── AnnotationUtils$1.class ├── AnnotationUtils.class ...omit...
なるほど、dependenciesで指定したcommons-lang3の中身がバラされています。
MANIFEST.MFの中身を見てみると、
Manifest-Version: 1.0 Main-Class: App
確かにMain-Classが指定されています。実行してみるとたしかにハロワが表示されます。
$ java -jar App.jar Hello world.
マルチプロジェクト
MavenではおなじみのマルチプロジェクトをGradleでもやってみます。以下のような構成を考えてみます。
親プロジェクト ├── api (子プロジェクト) └── common (子プロジェクト)
ビルドを統合する親プロジェクト、APIの処理が実装してあるapiプロジェクト、全てのプロジェクトが参照する共通処理が実装してあるcommonプロジェクトという、よくある構成をビルドしてみます。
ファイルの構成はこんな感じです。
親プロジェクト ├── api │ ├── build.gradle │ └── src │ └── main │ └── java │ └── App.java ├── common │ ├── build.gradle │ └── src │ └── main │ └── java │ └── Common.java ├── build.gradle └── settings.gradle
まずは、apiプロジェクトのApp.javaから見ていきます。
public class App { public static void main(String[] args) { Common.hoge(); } }
commonプロジェクトにあるhogeメソッドを呼び出しています。
次に、commonプロジェクトのCommon.javaを見てみます。
public class Common { public static void hoge () { System.out.println("Hello Java World !"); } }
apiプロジェクトが参照しているhogeメソッドは、Hello Java World !を標準出力に表示します。
親プロジェクトのbuild.gradleを見てみます。
subprojects { repositories { mavenCentral() } }
subproject内に記載された処理は、全てのサブプロジェクトに反映されます。つまり、これは、apiプロジェクトとcommonプロジェクトは、依存関係ライブラリの参照先リポジトリとして、Maven Central Repositoryを見に行きなさいということを表しています。
親プロジェクトのsettings.gradleを見てみます。
include 'common', 'api'
サブプロジェクトとして、commonプロジェクトとapiプロジェクトを参照するという設定です。この記述によって、プロジェクトの親子関係を表しています。ちなみにここで指定するのは、サブプロジェクトのTOPのディレクトリ名です。
apiプロジェクトのbuild.gradleを見てみます。
apply plugin: 'java' dependencies { compile project(':common') }
dependenciesの中でcompile project(‘:common’)という記述があります。これは、コンパイル時に、commonプロジェクトを参照しますよという設定です。これをしないと、APIプロジェクトのApp.javaでCommon.hoge()としたときに、メソッドが見つからないというエラーになります。
commonプロジェクトのbuild.gradleを見てみます。
apply plugin: 'java'
特にこのプロジェクトは特別なことしないので、これだけです。
では、親プロジェクトのディレクトリに移動して、gradle buildしてみましょう。
$ gradle build BUILD SUCCESSFUL in 1s 4 actionable tasks: 4 up-to-date
無事ビルドできました。各プロジェクトのbuild/libsの下にjarができていると思います。
親プロジェクト ├── api │ ├── build │ │ ├── classes │ │ │ └── java │ │ │ └── main │ │ │ └── App.class │ │ └── libs │ │ └── api.jar │ ├── build.gradle │ ├── settings.gradle │ └── src │ └── main │ └── java │ └── App.java ├── common │ ├── build │ │ ├── classes │ │ │ └── java │ │ │ └── main │ │ │ └── Common.class │ │ └── libs │ │ └── common.jar │ ├── build.gradle │ ├── settings.gradle │ └── src │ └── main │ └── java │ └── Common.java ├── build.gradle └── settings.gradle
実行してみましょう。
$ java -classpath api/build/libs/api.jar:common/build/libs/common.jar App Hello Java World !
おおヮ(゚д゚)ォ!無事ハロワが表示されました(`・ω・´)シャキーン
最後に
いかがでしょうか?GradleよりもやっぱMavenがいいかなって思うときも、ときどきありますが、きっとそれは私がうまく使いこなせてないからなんです。もっと精進しようと思います。
※ ちなみにGradleの活用事例は以下の記事にあります。
多分わかりやすいマイクロサービス入門 〜 マイクロサービスフレームワーク「Azure Service Fabric」でLINE風なチャットアプリを作ろう!! 〜