Gradle再入門
はじめに
Javaプロジェクトの依存関係解決やjarのビルドなど、Java固有のビルドツールというイメージのあるGradleですが、
一般的なファイル操作やコマンド呼び出しなども容易に行える汎用性を備えています。
Gradleの解説は、どうしてもJavaのビルドに必要な「おまじない」が話の中心になってしまい、
そこから少しでもレールを外れるとどうすればいいかわからなくなりがちです。
ここでは、Gradleのフレームワークとしての輪郭をおさえつつ
やりたいことをやるための部品と、その組み上げ方について開発者があたりをつけられるような記事を目指しています。
前提知識
-
プロになるJavaをひととおり読んだことがあるぐらいの知識を想定しています
- Javaで数値・文字列の演算、ファイルの読み書きをしたことがある
- Javaでオブジェクト指向に触れている
- javac, javaコマンドを叩いたことがある
- ごく小さなGradleのスクリプト(build.gradle)を書いたことがある、あるいはテンプレをコピペして実行したことがある
- build.gradle以外にGroovyは書いたことがない
1. 準備
Gradleを動かすため、次のものをインストールしておきます。
- JDK
- 17.0.3
- win
- 直接ローカルにインストール(JAVA_HOMEを設定)
- https://adoptium.net/temurin/archive
- mac / linux
- sdkman!というパッケージマネージャ経由がおすすめ
-
sdk install java 17.0.3-tem
でjdkをインストール
- win
- 17.0.3
- Gradle
- 7.4.2
- win
- 直接ローカルにインストール
- https://gradle.org/install/
- mac / linux
- これもsdkman!経由がおすすめ
-
sdk install gradle 7.4.2
でgradleをインストール
- win
- 7.4.2
2. 少しずつ書いてみる
Gradleは汎用的なビルドスクリプトのためのフレームワークです。
ビルドスクリプトの記述には次の言語を選択できます
- Groovy
- 動的型言語。関数(クロージャ)を第一級オブジェクトとして扱える
- Javaも一緒に書ける
- 基礎
-
build.gradle
というファイル名で記述
- Kotlin
- 静的型言語。関数を第一級オブジェクトとして扱える
- Javaも一緒に書ける
- 基礎
-
build.gradle.kts
というファイル名で記述
最近はKotlin版がファーストチョイスになると思いますが、新しめの機能をKotlinで書くと細かいハマりどころがあるため、とりあえず歴史の古いGroovyを使って説明します。
(落ち着いたらKotlinで書き直します)
JavaのスクリプトとしてのGradle
まずはGradleのフレームワークが提供する機能、Groovy言語の機能を使わず、Javaだけで書いてみます。
build.gradle
というファイルを作り、
System.out.println("Hello World!");
と書いてターミナルから gradle build
を同ディレクトリで実行すると、GradleのログとともにHello World!
が出力されます。
このように、Javaをスクリプトとして実行しているかのようにコードが書けてしまいます。
しかも最近のJavaはファイル操作やコマンド実行が簡単にできるようになっているので、例えば
import java.nio.file.Path;
import java.nio.file.Files;
Files.writeString(Path.of("./abc.txt"), """
aaa
bbb
ccc
""");
var str = Files.readString(Path.of("./abc.txt"));
System.out.println(str);
Files.copy(Path.of("./abc.txt"), Path.of("./abc2.txt"))
Files.delete(Path.of("./abc.txt"))
// プロセス起動(https://github.com/gradle/gradle/issues/16716)
new ProcessBuilder("java", "-version").inheritIO().start().waitFor();
と書くことで、普段シェルスクリプトでやっているような作業をJavaで実現出来ます。
ただし、Java9で追加されたjshellを使えばgradleを使わずとも同じことが出来てしまいます。
実際に上のコードに対してjshell build.gradle
を実行すると、gradle build
と同じ結果になることがわかります。
GroovyスクリプトとしてのGradle
次に、Groovyを使ってbuild.gradleを書いてみます。
ここでは最低限の紹介しかしませんが、大まかに次の特徴があります。
defで変数を定義する
def abc = "abc"
関数の括弧を省略できる
println("Hello World!")
println "Hello World!"
ただし、引数のない関数の場合は省略できません。
// ok
println System.getenv()
// ng
println System.getenv
シングルクォート, ダブルクォートどちらでも文字列が扱える
シングルクォート, ダブルクォートどちらもStringとして評価されます。
// java.lang.String
println "World"
println "World".getClass().getName()
// java.lang.String
println 'World'
println 'World'.getClass().getName()
ただし、ダブルクォートに${}
が含まれる場合 GString というGroovy独自の型に評価され、${}
内の変数が置換されます。
def greet = 'Hello'
// java.lang.String
println "%s World".formatted(greet)
println "%s World".formatted(greet).getClass().getName()
// org.codehaus.groovy.runtime.GStringImpl
println "${greet} World"
println "${greet} World".getClass().getName()
Groovyのクラス
Groovyでは、フィールドとメソッドはすべてpublic固定です。
class TestClass {
def hoge = 'abc'
def fuga() {
println hoge
}
}
def obj = new TestClass()
println obj.hoge
obj.hoge = 'cdf'
obj.fuga()
getter, setterを用意した場合、フィールドアクセスがgetter, setter呼び出しに読み替えられます。
class TestClass {
def hoge = 'abc'
def getHoge() {
println 'getHoge() called'
return this.hoge
}
def setHoge(String str) {
println 'setHoge() called'
this.hoge = str
}
}
def obj = new TestClass()
obj.hoge // getHoge() called
obj.hoge = 'def' // setHoge() called
波括弧{}で囲んだ領域はクロージャになる
Groovyのクロージャは、本来の「環境つき関数」としての意味合いと、「無名関数」「ラムダ」としての意味合いを併せ持ちます。
// 本来のクロージャ
def c0 = {
def local = 1
return { x -> local += x; println local }
}
def closure = c0()
closure(100) // 101
closure(100) // 201
closure(100) // 301
// 無名関数・ラムダとしてのクロージャ
def c1 = { a, b -> println a + b }
c1(1, 2)
def c2 = { a -> println a }
[10, 20, 30].each(c2)
// 引数が1つだけの場合、記述を省略できる(引数がitとして予約される)
def c3 = { println it }
c3(1000)
def c4 = { it() }
c4 {
println 'abc'
}
引数の最後がクロージャである場合、外側に記述できる
Gradleで多用される書き方なので、覚えておくと理解が捗ります。
def c = { a, closure ->
closure(a)
}
// A
c('abc', { println it })
// B
c('abc') {
println it
}
// AとBは同じ結果になる
クロージャのデリゲート
Gradleで多用される書き方なので、覚えておくと理解が捗ります。
def c = {
hoge(123)
fuga('abc')
}
class HogeFuga {
def hoge(int num) {
println num
}
def fuga(String str) {
println str
}
}
def hf = new HogeFuga()
c.delegate = hf
c()
クロージャ上にhoge、fugaは定義しておらず、delegateするオブジェクト次第で如何様にも動作が変わります。
後述しますが、Gradleのフレームワークはbuild.gradle上のスクリプトを1つの大きなクロージャとして扱い、
Gradle組み込みのオブジェクトをdelegateに与え実行することで動作しています。
Gradleの関数を使いながらGroovy(+Java)で書く
Gradleのフレームワークに浸る前に、Gradleが提供する便利な関数を使ってみます。
JavaのFilesユーティリティ, Groovyのjdk enhancementも使えますが、ファイル操作やコマンド実行は通常これらの関数を組み合わせて記述します。
file / files / fileTree
// file()でFileオブジェクト(Java)が生成できる
file('./dir1').mkdir()
file('./dir1/a.txt').createNewFile()
file('./dir1/b.txt').createNewFile()
file('./dir1/dir2').mkdir()
file('./dir1/dir2/c.txt').createNewFile()
// files()でFileCollectionオブジェクト(Gradle固有)が生成できる
// - Iterable<File>の実装クラス
// - https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileCollection.html
def list = files('c.json', 'a.txt', 'a.csv', 'a.json', 'b.csv', 'b.json')
list.filter({ f -> f.getName().endsWith('.json') }).sort().each({ println it.getName() })
// ネストしてもフラットなリストとして扱える
def list2 = files(files('c.json', 'a.txt', 'a.csv'), files('a.json', 'b.csv', 'b.json'))
list2.filter({ f -> f.getName().endsWith('.csv') }).each({ println it.getName() })
// fileTreeでFileTreeオブジェクト(Gradle固有)が生成できる
// - FileCollectionのサブクラス
// - https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileTree.html
fileTree('./dir1').visit({ println it.getPath().toString() })
file('./dir1/dir2/c.txt').delete()
file('./dir1/dir2').delete()
file('./dir1/b.txt').delete()
file('./dir1/a.txt').delete()
file('./dir').delete()
delete
ファイル、ディレクトリを再帰的に削除できます。
file('./tmp').mkdir()
file('./tmp/abc.txt').createNewFile()
delete './tmp'
copy
引数にクロージャを渡すことで、様々なカスタマイズができる関数です。
ファイル、ディレクトリを再帰的にコピーできます。
delete './from'
delete './into'
file('./from').mkdir()
file('./from/a.txt').createNewFile()
file('./from/dir').mkdir()
file('./from/dir/b.txt').createNewFile()
file('./into').mkdir()
copy {
from './from'
into './into'
}
include, excludeでコピー対象を制御できます。
// a.txtだけ
copy {
from './from'
include '**/a.txt'
into './into'
}
// a.txt, ./dir だけ
copy {
from './from'
exclude '**/b.txt'
into './into'
}
指定した名前でコピーできます。
copy {
from './from/a.txt'
into './into'
rename { name -> 'aaa.txt' }
}
コピー時にファイル内のテキストを変数置換できます。
Files.writeString(Path.of('./from/hogefuga.txt'), '''
Hello, ${hoge} !
Hello, ${fuga} !
''');
copy {
from './from/hogefuga.txt'
into './into'
expand [
hoge: 'aaa',
fuga: 'bbb'
]
}
なお、このクロージャが実装するインターフェースはCopySpecといって、Gradleの様々な場所で登場します。
同じ作法で設定が書けるため、覚えておくことをお勧めします。
{
from()
into()
include()
exclude()
rename()
expand()
...
}
exec
外部コマンドを呼び出すことができます。
exec {
commandLine 'java', '-version'
}
// 環境変数の指定
// - mac/linux
exec {
environment [ 'test1': 'aaa' , 'test2': 'bbb', 'test3': 'ccc' ]
commandLine 'sh', '-c', 'echo $test1 $test2 $test3'
}
// - win
exec {
environment [ 'test1': 'aaa' , 'test2': 'bbb', 'test3': 'ccc' ]
commandLine 'cmd', '/c', 'echo %test1% %test% %test3%'
}
// カレントディレクトリの変更
file('./tmp').mkdir()
file('./tmp/a.txt').createNewFile()
exec {
workingDir './tmp'
commandLine 'ls', '-la'
}
delete './tmp'
// リダイレクト
exec {
standardOutput new FileOutputStream(file('./out.txt'))
commandLine 'ls', '-la'
}
ちなみに、このクロージャはExecSpecというGradleのインターフェースを実装しています。
{
environment()
args()
standardInput()
standardOutput()
errorOutput()
commandLine()
...
}
javaexec
javaコマンドを呼び出すことができます。
下のスクリプトは、exec
でコンパイルしたクラスファイルをjavaexec
で実行する例です。
import java.nio.file.Path;
import java.nio.file.Files;
delete('./Main.java')
delete('./Main.class')
Files.writeString(Path.of('./Main.java'), '''
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}
''');
exec {
commandLine 'javac', 'Main.java'
}
javaexec {
main = 'Main'
classpath = files('.')
args = ['a', 'b', 'c']
}
ちなみに、このクロージャはJavaExecSpecというGradleのインターフェースを実装しています。
{
main()
args()
classpath()
...
}
3. TaskとConfiguration
ここからGradleのフレームワークに触れていきます。
Gradleの中心的な機能は、TaskとConfigurationという2種類の依存関係解決の仕組みにあります。
Task
Taskを定義しておくと gradle タスク名
としてコマンドから直接呼び出すことができます。
tasks.register('a') {
doLast {
println 'aaa'
}
}
gradle a
を実行すると、次のようにログ出力されるはずです。
> Task :a
aaa
BUILD SUCCESSFUL
1 actionable task: 1 executed
ところで、doLast
の部分は何を表しているのでしょうか。
Taskは actions という名前のプロパティを持っていて、それは Action インターフェースのリストになっているのですが、 doLast
は引数として与えられたクロージャをActionのオブジェクトに変換し、actionsの末尾に追加するはたらきをします。
そしてgradle タスク名
を実行すると、タスクが作られたのち actions に登録された処理が先頭から順番に実行されていきます。
tasks.register('a') {
doLast {
println 'aaa'
}
doLast {
println 'bbb'
}
doLast {
println 'ccc'
}
}
// aaa -> bbb -> ccc の順で出力される
実は、doLast
を使った上のコードは下のコードのシンタックスシュガーになっています。
tasks.register("a") {
actions.add(new Action<Task>() {
@Override
public void execute(Task task) {
println("aaa")
}
})
actions.add(new Action<Task>() {
@Override
public void execute(Task task) {
println("bbb")
}
})
actions.add(new Action<Task>() {
@Override
public void execute(Task task) {
println("ccc")
}
})
}
(ちなみにdoLast
だけでなくdoFirst
などもあります。詳しくは TaskのJavadoc を参照してください。)
もう少しdoLast
で遊んでみます。一度 register したタスクはこのように
tasks.register('a') {
doLast{
println 'aaa'
}
}
a {
doLast {
println'bbb'
}
}
直接タスク名で呼び出すことができ、同様に actions へ処理を追加することが可能です。
これは、たとえばマルチプロジェクト構成をとったとき同じタスク名で前処理と本体を分割するのに役立ちます(タスク定義によるプロパティの追加、マルチプロジェクトについては後の章で説明)。
// 親プロジェクトのbuild.gradle
tasks.register('a') {
doLast{
println '前処理'
}
}
// サブプロジェクトのbuild.gradle
a {
doLast {
println'本体'
}
}
しかし、特に理由がなければ下のコードのように
tasks.register('a') {
doLast{
println 'aaa'
}
doLast{
println 'bbb'
}
doLast{
println 'ccc'
}
}
1つのタスク内で細かく分割することはせず、
tasks.register('a') {
doLast{
println 'aaa'
println 'bbb'
println 'ccc'
}
}
1つのdoLast
にまとめたほうが良いと思います。
設定フェーズと実行フェーズ
もう少しだけdoLast
の話をします。
もしdoLast
を書かなかった場合はどうなるのでしょうか。試しに次のコードをbuild.gradleとして保存しgradle a
を実行してみてください。
tasks.register('a') {
println 'a'
}
tasks.register('b') {
println 'b'
}
(出力結果)
> Configure project :
a
b
BUILD SUCCESSFUL
a
だけでなくb
も表示されてしまいました。
Gradleのフレームワークにおいて、build.gradleの実行は設定フェーズと実行フェーズに分けられます。
doLast
の外に書いた内容はすべて設定フェーズ時に処理され、実行フェーズでは指定したタスク内のdoLast
の中身のみが処理されます。
例えば
tasks.register('a') {
println 'config a' // --- (1)
doLast {
println 'exec a' // --- (2)
}
}
tasks.register('b') {
println 'config b' // --- (3)
doLast {
println 'exec b' // --- (4)
}
}
上のbuild.gradleに対してgradle a
を実行すると
> Configure project :
config a
config b
> Task :a
exec a
BUILD SUCCESSFUL
1 actionable task: 1 executed
「Configure project」=設定フェーズで(1)(3)が呼ばれ、「Task :a」= 実行フェーズで(2)が呼ばれています。
このように、呼び出したTaskに関わらず 全Taskの設定が設定フェーズで呼ばれることがわかりました。
Taskの依存関係
Task同士に依存関係を定義することができます
tasks.register('a') {
doLast {
println 'aaa'
}
}
tasks.register('b') {
dependsOn 'a'
doLast {
println 'bbb'
}
}
この状態でgradle b
を実行すると、aが呼ばれた後にbが呼ばれます。
> Task :a
aaa
> Task :b
bbb
BUILD SUCCESSFUL
2 actionable tasks: 2 executed
では、次のケースはどうでしょうか。
tasks.register('a') {
doLast {
println 'aaa'
}
}
tasks.register('b') {
dependsOn 'a'
doLast {
println 'bbb'
}
}
tasks.register('c') {
dependsOn 'a'
dependsOn 'b'
doLast {
println 'ccc'
}
}
依存関係はこのような構造です。
c
├── a
└── b
└── a
gradle c
を実行すると
> Task :a
aaa
> Task :b
bbb
> Task :c
ccc
BUILD SUCCESSFUL
3 actionable tasks: 3 executed
a -> a -> b -> c というようにはならず a -> b -> c の順序でTaskが呼び出されます。
つまり、依存関係が解決される過程で重複が排除されることがわかります。
組み込みTask
出来合いのタスク=組み込みタスクを継承することで、記述を減らすことができます。
組み込みタスクはすべてdoLast
を使わずに設定を行います。
できるだけ組み込みタスクを使った記述にすることで、宣言的でメンテナンスしやすいビルドスクリプトになるはずです。
Copyタスク
- copy()同様の動作です
- CopySpecに従うクロージャを指定します
delete './from'
delete './into'
file('./from').mkdir()
file('./from/a.txt').createNewFile()
file('./from/dir').mkdir()
file('./from/dir/b.txt').createNewFile()
tasks.register('copy', Copy) {
from './from'
into './into'
}
Execタスク
- exec()同様の動作です
- ExecSpecに従うクロージャを指定します
tasks.register('exec', ExecSpec) {
environment [ 'test1': 'aaa' , 'test2': 'bbb', 'test3': 'ccc' ]
commandLine 'sh', '-c', 'echo $test1 $test2 $test3'
}
JavaExecタスク
- javaexec()同様の動作です
- JavaExecSpecに従うクロージャを指定します
import java.nio.file.Path;
import java.nio.file.Files;
delete('./Main.java')
delete('./Main.class')
Files.writeString(Path.of('./Main.java'), '''
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}
''');
tasks.register('javac', Exec) {
commandLine 'javac', 'Main.java'
}
tasks.register('javaExec', JavaExec) {
dependsOn 'javac'
main = 'Main'
classpath = files('.')
args = ['a', 'b', 'c']
}
Zipタスク
- 文字通りzipファイルを生成するタスクです
- CopySpecに従って、zip対象を制御します
- AbstractArchiveTaskのプロパティをセットして、zipファイルの保存先を制御します
- archiveFileName, destinationDirectory, ...
- https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/AbstractArchiveTask.html
- 後述しますが、AbstractArchiveTaskを継承するタスクのため直接publish対象にすることができます
delete './from'
delete './into'
file('./from').mkdir()
file('./from/a.txt').createNewFile()
file('./from/dir').mkdir()
file('./from/dir/b.txt').createNewFile()
tasks.register('zip', Zip) {
from './from'
archiveFileName.set('test.zip')
destinationDirectory.set(layout.projectDirectory) // build.gradleがあるディレクトリ
}
Tarタスク
- tar.gzファイルを生成するタスクです
- 設定その他はZipタスク同様になります
delete './from'
delete './into'
file('./from').mkdir()
file('./from/a.txt').createNewFile()
file('./from/dir').mkdir()
file('./from/dir/b.txt').createNewFile()
tasks.register('tar', Tar) {
from './from'
archiveFileName.set('test.tar.gz')
destinationDirectory.set(layout.projectDirectory) // build.gradleがあるディレクトリ
}
Taskのインターフェース
こちらを参照してください。
Configuration
Configurationはファイルやライブラリの依存関係をグループ化する仕組みです。
ローカルのファイルシステムからファイルの取得
configurations {
conf
}
dependencies {
conf files('./a.txt')
conf files('./b.txt')
conf files('./c.txt')
}
copy {
from configurations.conf
into './dir'
}
上のスクリプトをbuild.gradleとして保存し、gradle build
を実行すると./dir
配下にa.txt, b.txt, c.txtがコピーされます。
このように、configurationsとdependenciesを組み合わせてローカルのファイルをグループ化することが可能です。
configurations.conf
の実体はFileCollectionのインスタンスですので、上の例については次のように書いても同じです。
def conf = files './a.txt', './b.txt', './c.txt'
copy {
from conf
into './dir'
}
セントラルリポジトリからjarの取得
configurations {
conf
}
dependencies {
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.25
conf 'org.slf4j:slf4j-api:1.7.25'
}
repositories {
mavenCentral()
}
copy {
from configurations.conf
into './lib'
}
上のbuild.gradleに対してgradle build
を実行すると、./lib
にslf4j-api-1.7.25.jar
が保存されます。
このように、configurations・dependencies・repositoriesを組み合わせてリモート(maven repository)にあるjarをグループ化することが可能です。
また、それらのjarがあたかもローカルにあるファイルであるかのように扱うことができます。
推移的依存関係の解決
configurations {
conf
}
dependencies {
conf 'org.slf4j:slf4j-api:1.7.25'
conf 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}
repositories {
mavenCentral()
}
copy {
from configurations.conf
into './lib'
}
repositoriesにリポジトリを指定した場合、グループに含まれるjarは依存関係を辿った全jarが含まれ、gradle dependencies
コマンドでその内容を確認できます。
conf
+--- org.slf4j:slf4j-api:1.7.25
\--- org.apache.logging.log4j:log4j-slf4j-impl:2.17.2
+--- org.slf4j:slf4j-api:1.7.25
+--- org.apache.logging.log4j:log4j-api:2.17.2
\--- org.apache.logging.log4j:log4j-core:2.17.2
\--- org.apache.logging.log4j:log4j-api:2.17.2
重複が排除された結果、上のスクリプトで./lib
に配置されるライブラリは
log4j-api-2.17.2.jar
log4j-core-2.17.2.jar
log4j-slf4j-impl-2.17.2.jar
slf4j-api-1.7.25.jar
の4つになります。
Latest戦略
Gradleでは、jarのバージョンが食い違った場合に最新のバージョンが採用されます(=Latest戦略)。
configurations {
conf
}
dependencies {
conf 'org.slf4j:slf4j-api:1.6.6'
conf 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}
repositories {
mavenCentral()
}
copy {
from configurations.conf
into './lib'
}
gradle dependencies
の結果は次の通りです。
conf
+--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
\--- org.apache.logging.log4j:log4j-slf4j-impl:2.17.2
+--- org.slf4j:slf4j-api:1.7.25
+--- org.apache.logging.log4j:log4j-api:2.17.2
\--- org.apache.logging.log4j:log4j-core:2.17.2
\--- org.apache.logging.log4j:log4j-api:2.17.2
重複が排除&最新が選択された結果、gradle build
で./lib
に配置されるライブラリは
log4j-api-2.17.2.jar
log4j-core-2.17.2.jar
log4j-slf4j-impl-2.17.2.jar
slf4j-api-1.7.25.jar
の4つになります。
推移的関係の無効化
掲題の通り、推移的な依存関係を無効化できます。
configurations {
conf {
transitive false
}
}
dependencies {
conf 'org.slf4j:slf4j-api:1.6.6'
conf 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}
repositories {
mavenCentral()
}
copy {
from configurations.conf
into './lib'
}
gradle dependencies
の結果は次の通りです。
conf
+--- org.slf4j:slf4j-api:1.6.6
\--- org.apache.logging.log4j:log4j-slf4j-impl:2.17.2
gradle build
したときに./lib
へ配置されるjarもこれら2つになります。
推移的関係のexclude
部分的に依存関係を無効化することができます。
configurations {
conf {
exclude module: 'log4j-core'
}
}
dependencies {
conf 'org.slf4j:slf4j-api:1.6.6'
conf 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}
repositories {
mavenCentral()
}
copy {
from configurations.conf
into './lib'
}
gradle dependencies
の結果は次の通りです。
conf
+--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
\--- org.apache.logging.log4j:log4j-slf4j-impl:2.17.2
+--- org.slf4j:slf4j-api:1.7.25
\--- org.apache.logging.log4j:log4j-api:2.17.2
結果、gradle build
で./lib
に配置されるライブラリは
log4j-api-2.17.2.jar
log4j-slf4j-impl-2.17.2.jar
slf4j-api-1.7.25.jar
の3つになります。
Configurationのインターフェース
こちらを参照してください。
TaskとConfigurationを組み合わせてjarを作る
これまでの知識をもとに、Configurationを使ってライブラリの依存関係解決をしつつ、Taskを使ってjavac, jarコマンドを実行してみます。
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
logger.info("Main start.");
System.out.println(Arrays.toString(args));
logger.info("Main end.");
}
}
(↑./Main.java
として保存)
configurations {
conf
}
dependencies {
conf 'org.slf4j:slf4j-api:1.6.6'
conf 'org.slf4j:slf4j-simple:1.7.25'
}
repositories {
mavenCentral()
}
tasks.register('resolveDependencies', Copy) {
from configurations.conf
into './lib'
}
tasks.register('javac', Exec) {
dependsOn resolveDependencies
delete './Main.class'
commandLine 'javac', 'Main.java', '-cp', './lib/*', '-encoding', 'UTF-8', '-source', '17', '-target', '17', '-d', './classes'
}
tasks.register('jar', Exec) {
dependsOn javac
delete './jar'
file('./jar').mkdirs()
commandLine 'jar', 'cfe', './jar/main.jar', 'Main', '-C', './classes', '.'
}
上のスクリプトをbuild.gradle
として保存し、gradle jar
を実行すると./jar/main.jar
としてjarが生成されます。
試しに次のコマンドを実行すると
java -cp './lib/*:./jar/main.jar' Main a b c
このような出力になるはずです。
[main] INFO Main - Main start.
[a, b, c]
[main] INFO Main - Main end.
4. Gradleの手続きと機能拡張のしくみ
クロージャのデリゲートで少し触れましたが、build.gradle
に書く内容は1つの大きなクロージャとして扱われ、
そのクロージャのdelegateにGradleフレームワークのオブジェクトが指定されることでGradleが動作します。
(フレームワーク側がライブラリ側に渡されることで動くという、主従が逆転していて面白い仕組みだと思います。)
これまで登場したconfiguration
, dependencies
, repositories
, tasks
といったワードは
Gradleのフレームワーク内部にあるProjectオブジェクトのメソッドおよびプロパティに対応しています。
つまり、build.gradle
は Project オブジェクトのメソッドやプロパティを呼び出すコードとして動くということがわかります。
例えば
configurations {
conf {
}
}
dependencies {
conf 'org.slf4j:slf4j-api:1.7.25'
conf 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}
repositories {
mavenCentral()
}
tasks.register('test') {
doLast {
println 'aaa'
}
}
上のbuild.gradle
は
Project project = getProject();
project.getConfigurations().create("conf")
project.getDependencies().add("conf", "org.slf4j:slf4j-api:1.7.25");
project.getDependencies().add("conf", "org.apache.logging.log4j:log4j-slf4j-impl:2.17.2");
project.getRepositories().mavenCentral();
project.getTasks().register("test",
new Action<Task>() {
@Override
public void execute(Task task) {
task.doLast {
println 'aaa'
}
}
}
);
こちらのコードと同じ動作になります(試してみてください)。
3つのフェーズ
build.gradle
実行時、次の3つのフェーズを経てProjectオブジェクトが初期化・実行されていきます。
Projectオブジェクトは
-
初期化フェーズ
- Projectオブジェクト生成
-
設定フェーズ
-
build.gradle
に書かれた設定用の内容を実行(以下は例)-
Project#configuration 実行
- configurationブロックで指定したクロージャが、Projectオブジェクト配下にあるConfigurationContainerに処理をデリゲートします
-
Project#dependencies 実行
- dependenciesブロックで指定したクロージャがDependencyHandlerに処理をデリゲートします
-
Project#repositories 実行
- repositoriesブロックで指定したクロージャがRepositoryHandlerに処理をデリゲートします
-
Projectオブジェクトのtasksプロパティを初期化し、各Taskを追加
- tasksプロパティはTaskContainerというインターフェースを実装し、registerなどのメソッドをもちます
-
Project#configuration 実行
-
-
実行フェーズ
- gradleコマンドの引数に対応するTaskのactionsを実行
フェーズ内の処理は他にも沢山ありますが、おおまかに上のような流れで進むことを覚えておいてください。
プラグインによるProjectの拡張
GradleはPluginを通して機能を拡張することができます。
プラグイン内部でProjectオブジェクトを操作することによって、さまざまなTaskやConfigurationを事前に用意することができます。
Javaのプロジェクトに必須な機能もすべてGradle本体から切り離し可能な形で実装されており、
それがjavaプラグイン, applicationプラグインなどお馴染みの組み込みプラグインとして提供されています。
このように、Gradleの中心はあくまで TaskとConfiguration をはじめとした抽象的な仕組みにあって、
用途に応じたプラグインを組み込むことによりビルドツールとして様々なユースケースに応えるフレームワークになっています。
プラグインを適用すると、先ほどの手続きは次のようになります。
- 初期化フェーズ
- Projectオブジェクト生成
- 設定フェーズ
-
build.gradle
に書かれた設定用の内容を実行(以下は例)- Project#configuration 実行
- Project#dependencies 実行
- Project#repositories 実行
- Projectオブジェクトのtasksプロパティを初期化し、各Taskを追加
-
プラグインの登録(Projectオブジェクトを受けとり、任意の操作を行う)
- Project#configuration 実行
- ProjectオブジェクトのtasksプロパティにTaskを追加
- ...
-
- 実行フェーズ
- gradleコマンドの引数に対応するTaskのactionsを実行
拡張の対象
プラグインによって拡張される機能は次の3種類です。
- Task
- Configuration
- Extention
動くコードを例にして説明します。
まず次のコードをbuild.gradle
として保存してください。
apply from: 'test_plugin.gradle'
printABC {
doLast {
println 'ABC'
}
}
tasks.register('printConf') {
doLast {
configurations.testConf.forEach({ println it })
}
}
tasks.register('printProps') {
doLast {
testExt.printProps()
}
}
続いて次のコードをtest_plugin.gradle
として同じディレクトリに保存してください。
class TestPlugin implements Plugin<Project> {
void apply(Project project) {
project.getConfigurations().create("testConf")
project.getDependencies().add("testConf", "org.slf4j:slf4j-api:1.7.25");
project.getDependencies().add("testConf", "org.apache.logging.log4j:log4j-slf4j-impl:2.17.2");
project.getRepositories().mavenCentral();
project.getTasks().register("printABC",
new Action<Task>() {
@Override
public void execute(Task task) {
println 'abc'
}
}
);
ExtensionContainer extensions = project.getExtensions();
extensions.add('testExt', TestExtension.class)
}
}
class TestExtension {
String p1 = 'aaa'
String p2 = 'bbb'
void printProps() {
println p1 + ', ' + p2
}
}
apply plugin: TestPlugin
上のスクリプトによって、3つのタスクが次のように動作するはずです。
gradle printABC
#
# abc
#
# BUILD SUCCESSFUL
gradle printConf
#
# > Task :printConf
# /Users/xxx/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-slf4j-impl/2.17.2/183f7c95fc981f3e97d008b363341343508848e/log4j-slf4j-impl-2.17.2.jar
# /Users/xxx/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.25/da76ca59f6a57ee3102f8f9bd9cee742973efa8a/slf4j-api-1.7.25.jar
# /Users/xxx/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-core/2.17.2/fa43ba4467f5300b16d1e0742934149bfc5ac564/log4j-core-2.17.2.jar
# /Users/xxx/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.17.2/f42d6afa111b4dec5d2aea0fe2197240749a4ea6/log4j-api-2.17.2.jar
#
# BUILD SUCCESSFUL
# 1 actionable task: 1 executed
gradle printExt
#
# > Task :printExt
# aaa, bbb
#
# BUILD SUCCESSFUL
# 1 actionable task: 1 executed
このように、プラグインを通して
- Taskを事前に追加することができます
- 上の例では、事前に定義された printABC に対して処理を追加しています
- Configurationを事前に追加することができます
- 上の例では、事前に定義された conf を printConf で利用しています
-
Extention という種類のプロパティを事前に追加することができます
- 上の例では、事前に定義された testExt を printExt で利用しています
javaプラグイン, applicationプラグインなどの組み込みプラグインによって使える機能は、これら Task, Configuration, Extention の拡張によって実現されています。
長くなったので別記事にします。
できれば以下のような流れで書いていきたいです。
- (4.の続き)
- 組み込みプラグイン
- javaプラグイン
- applicationプラグイン
- 組み込みプラグイン
-
- アーティファクト管理
- アーティファクトのpublish, 参照
- アーカイブタスクを利用したpublish
- 直接ファイルを指定したpublish
-
- マルチプロジェクト
- マルチプロジェクト構成
- settings.gradle
- プロジェクト参照
- buildSrc
- バージョンカタログ
- キャッシュ
- 7.開発環境
- gradleラッパー
- プロジェクトプロパティ
Discussion