1
/
5

なんとなく使わないGradle

はじめに

最近スパイスカレーを食べるのはもちろん、作るのにもハマっている小林(@mako-makok)です。
近所のお気に入りのお店の閉店が決まってしまい、悲しみに暮れていますが頑張ってアドカレの記事を書きました。

この記事は株式会社ログラスProductチームの2022年12/18(日)の記事です。
株式会社ログラス Product チーム のカレンダー | Advent Calendar 2022 - Qiita

なぜ今更Gradleかというと、最近社内で構築しているSheetlinというライブラリがあります。ニッチな話になりますが、Sheetlin のインターフェース設計に関する話をKotlin Fest Reject Conference 2022でしてきたので、よろしければこちらもご覧ください。

そんなSheetlinですが、ビルドツールはGradleを利用しています。
私も雰囲気でGradleを書いていたのですが、今回ビルドロジックを1から書くにあたって基礎とモダンなプラクティスについてキャッチアップしました。
この記事を読まれているということは少しでもGradleに興味を持っている、業務で使っている方ではないでしょうか。

  • 普段は雰囲気でGradle使っているけどいつかはしっかり理解したいと思っている方
  • Gradleって色々書き方あるけど最近の書き方がわからない方

という方向けに、基礎的な部分から、最近のGradle事情をかいつまんでご紹介します。

Gradle とは

Gradleはオープンソースで開発されているビルド自動化ツールです。
GradleはJVM上で実行されるため、Java, Kotlin, Scalaなど、JVM系の言語で書かれたモジュールのビルドツールとしてしばしば利用されます。
JVM上で実行されるだけで、実はTypeScriptやGoのモジュールもプラグインを利用することでビルドできたりします。

実態は依存関係に基づくタスクランナーであり、コンパイルやビルド、実行などは個々のタスクでしかありません。例えばSpring BootプロジェクトでbootRun を実行すると自動的にアプリが立ち上がります。これはコンパイルやビルドといったタスクがbootRunに紐付いており、依存関係が整理された上で順序どおり実行されています。

より詳しい話は公式ドキュメントにBuild Lifecycleの項にまとまっています。

難しく感じるかもしれませんが、実際は自分でゴリゴリ書くシーンは少ないです。
基本的にはデフォルトで用意されているタスクや後続で紹介するプラグインを入れて少しの設定をするだけで簡単に様々なタスクを実行してくれる非常に便利なツールです。

Gradle の概念

プロジェクト

Gradleのビルド対象のことをプロジェクトと呼びます。

Spring Initializr でGradleを選択してDLすると、ダウンロードされたフォルダのルートにbuild.gradleというファイルがあります。

このファイルがあるディレクトリ = 1プロジェクトという形になります。build.gradleにプラグインの設定やタスクのスクリプトを記載していきます。
プロジェクトはネストさせたり(ネストさせたプロジェクトをサブプロジェクトと呼ぶ)、依存関係を自信で定義してモジュール化する(マルチモジュール化)ことも可能です。

Gradleでは、DSLとしてGroovyもしくはKotlinを利用できます。

タスク

タスクは

  • ビルド
  • アプリケーション実行
  • テスト実行
  • デプロイ

といったアプリケーション開発における何かしらのタスクを実行するものです。
デフォルトでも様々なタスクが組み込まれており、一般的な最低限のビルドのニーズを満たすようなものはだいたいあります。
より高度なことを実現する場合は、後続で説明するプラグインを利用するか、DSLで自作のタスクを定義します。

プラグイン

プラグインを利用すると便利なタスクを追加できたり、ビルド自動化を超えたタスクを実行できます。

一部の例ですが、弊社ではGradleのプラグインを利用して以下のようなタスクを実行しています。

  • Ktlintを使ってktファイルにフォーマットをかける
  • SonarQubeで静的解析を行なう
  • サブプロジェクト毎に生成されるテストレポート・カバレッジをひとまとめにして閲覧できるようにする

公開されているプラグインを利用することで、ビルドの範疇外のタスクを行なうことができます。
また、プラグインは自作できます。
例えばサププロジェクトA, B, Cがあったとします。
AとBだけで自作したこのタスクを使えるようにしたいけどCでは利用させたくない、という状況で、タスクをプラグイン化してサブプロジェクトで読み込む設定をすることで解決できます。

まとめ

  • プロジェクト単位でタスクやプラグインの設定を行なえる
  • 設定は基本的にbuild.gradleに書いていく
  • プラグインは自作できる

Gradle のプラクティス

Gradleの概念としては以上が基本となります。
あとはどれだけ書き方を知っているか、メソッドを知っているかの勝負です。
メソッドは無数にあるので、全部を紹介できませんが、いくつかピックしてご紹介します。

Kotlin DSL を使う

Gradleの設定を記載するときはGroovyを使うことが主流でしたが、ここ数年で公式ドキュメント・IDEサポートの拡充によりKotlinで書くことが増えてきています。
Kotlinが導入されているプロジェクトの場合、そのままKotlinで書けてしまうので導入しない手はありません。
導入されていない場合でも、Kotlin DSLはbuild.gradleの拡張子に.ktsをつけるだけで書き始めることができます。

KotlinはJavaライクに書けつつ、Javaよりも比較的簡潔な記述をできることが多いです。
Javaのスキルセットがあればそこまで困らず書くことができます。

また、いくつか拡張関数が定義されており、Gradleの設定自体を簡潔に行なうようなAPIを利用できます。
本格的に移行しようとなった場合は、Androidは公式ドキュメントでKotlin DSLへの移行ガイドがあります。

jvmToolchain の使用

Gradle 6.7で追加されたオプションです。
https://docs.gradle.org/current/userguide/toolchains.html

どのJavaのバージョンで動かそう、となったとき、もともとjvmTargetというプロパティに8や11のような数値を指定することでJavaのバージョンを指定していました。

これだけでも便利なのですが、jvmToolchainは更に便利です。
ビルドする時に指定のバージョンのJavaが入っていない場合、自動的にダウンロードしてきてそのバージョンのJavaを利用してビルドを行います。

kotlin {
    jvmToolchain {
        (this).languageVersion.set(JavaLanguageVersion.of(11))
    }
}

jvmTargetも個別に指定できますが、特に何も書かない場合jvmToolchainで指定したバージョンがjvmTargetになります。

非推奨となっている書き方

Gradleは進化が早く、非推奨となっている書き方がいくつかあります。
古い情報と新しい情報で書き方が異なって混乱することがありますが、そういった違和感を感じた際は公式ドキュメントで探すと良いです。
今回は個人的に混乱してしまった事例を2つ紹介します。

プラグイン導入には apply plugin を利用しない

apply pluginはレガシーです。
https://docs.gradle.org/current/userguide/plugins.html#sec:old_plugin_applicationapply plugin

プラグインの利用はplugin DSLを利用します。

plugins {
    id("com.jfrog.bintray") version "1.8.5"
}

マルチモジュール構成で subprojects{}, allprojects{} を使わない

Gradleでマルチモジュールなプロジェクトを作ろう、となったとき書き方が色々あるのですが、その1つにsubprojects{ … } ,allprojects{ … }という書き方があります。
これはなにかというと、ブロック内に設定を書いていくとそれが子のプロジェクトにも適用されるようになります。
例えば以下のような構成になっているプロジェクトがあったとします。

root-project
├── build.gragle
├── project-a
│   ├── build.gradle
├── project-b
    ├── build.gradle

root-projectにsubprojects{ … } ,allprojects{ … }で設定を書いていくと、project-aとproject-bにもその設定が適用されるという継承のような挙動をします。

この書き方は公式では非推奨となっています。

  • モジュールが複雑化してくると分岐が発生して複雑になる
  • 依存関係の整理が難しくなっていき、不要なライブラリを読み込んだりバージョンの管理が難しくなる無駄なものを読み込んだ場合、ビルドのパフォーマンスは下がる

構文的にはかなり読みやすいので、モジュール構成が複雑にならないというのが確定していれば導入してもさほど問題ないにはなりません。
逆に、モジュラモノリス構成のようなものをやり始めるとビルドロジックや依存が途端に複雑化するので。

ではどうするのが良いかというと、

  • 共通の設定はbuildSrcというディレクトリを作成しそこに記載する
  • 各プロジェクトはその設定をプラグインで明示的に読み込む

という方法が紹介されています。

We recommend putting source code and tests for the convention plugins in the special buildSrc directory in the root directory of the project. For more information about buildSrc, consult Using buildSrc to organize build logic.

公式で大規模なマルチモジュール構成を構築する方法のドキュメントもあるので、興味ある方は覗いてみてください。

https://docs.gradle.org/7.5.1/userguide/structuring_software_products.html

ハンズオン

ここまでGradleの様々な機能を紹介しましたが、論よりコードということで実際にGradleのコードを書いていきたいと思います。
ここでは、Spring InitializrでGroovyベースのbuild.gradleを、Kotlin DSLで書き直してマルチモジュール化していきます。

Spring Initinalizr で Spring Boot プロジェクトの雛形を DL

構成

  • Gradle - Groovy
  • Language: Java
  • Java 17
  • DependenciesSpring Web
    Spring Web Service

1. API を作成する

APIの内容は本質では無いので適当ですが、apiとdomainは後でマルチモジュール化するので分けておきます。(執筆日は頑張ったご褒美にすきやきを食べました)
今回domainに関しては、依存関係を作りたかったので、 Project <- ToDoという依存を作っています。

まずはドメインとコントローラーを2つずつ作成します。

mkdir -p src/main/java/com/example/demo/{domain,api}
touch src/main/java/com/example/demo/domain/{ToDo.java,Project.java}
touch src/main/java/com/example/demo/api/{ToDoController.java,ProjectController.java}

ToDo.java

package com.example.demo.domain;

public record ToDo(String id, String description) {

}

Project.java

package com.example.demo.domain.project;

import com.example.demo.todo.ToDo;

import java.util.List;

public record Project(String id, String name, List<ToDo> toDos) {
}

ToDoController.java

package com.example.demo.api;

import com.example.demo.domain.ToDo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
class ToDoController {

    @GetMapping
    public List<ToDo> getTodos() {
        return List.of(
            new ToDo("1", "すきやきを食べる"),
            new ToDo("2", "洗剤を買う")
        );
    }
}


ProjectController.java

package com.example.demo.api;

import com.example.demo.domain.Project;
import com.example.demo.domain.ToDo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("api/projects")
class ProjectController {

    @GetMapping
    public List<Project> list() {
        return List.of(
            new Project(
                "1",
                "プロジェクトA",
                List.of(
                    new ToDo("1", "すきやきを食べる")
                )
            ),
            new Project(
                "2",
                "プロジェクトB",
                List.of(
                    new ToDo("2", "洗剤を買う")
                )
            )
        );
    }
}


一旦起動してみて、APIが叩けることを確認します。
gradleはJavaが実行できる環境であれば実行できます。

# Linux, Mac
./gradlew bootRun

# Windous
gradle.bat bootRun


起動したらAPIを叩いて結果が返ってくれば成功です。

curl http://localhost:8080/api/todos
[{"id":"1","description":"すきやきを食べる"},{"id":"2","description":"洗剤を買う"}]

2. build.gradle を Kotlin DSL で書き換える

次はKotlin DSLで既存のbuild.gradleを書き換えていきます。
以下のようなbuild.gradleがあります。

// pluginの読み込み。apply pluginではなく plugin DSLを使って書かれている
plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.6'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

// ビルドの情報。sourceCompatibilityはコンパイルに利用するJavaのバージョンを記載する
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

// リポジトリの設定。外部ライブラリを利用する場合はどこからそれをダウンロードしてくるかの設定が必要
// 今回はmavanが管理しているリポジトリを利用している
repositories {
	mavenCentral()
}

// 外部ライブラリの読み込み。dependenciesブロック内にパッケージ名トライブラリ名を記載する
// implementationは全パッケージで、testImplementationはtest配下のパッケージで利用できる
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-web-services'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

// もともと存在する「test」というタスクを拡張している
// org.springframework.boot:spring-boot-starter-testを入れると、useJUnitPlatform()というメソッドが利用できるようになる
// useJUnitPlatform()はjUnit5で実行するよという意味
tasks.named('test') {
	useJUnitPlatform()
}


ちなみに、1の項で何気なく./gradlew bootRunができましたが、org.springframework.boot
というプラグインが入っていることによって、bootRunというタスクが追加されSpring Bootの起動ができるようになっています。

本題からずれましたが、このファイルをKotlinで書き換えていきます。

まずはファイルをbuild.gradlebuild.gradle.ktsにリネームします。
この状態で./gradlew bootRunを実行してみるとコンパイルエラーで落ちます。

build.gradle.ktsを以下のように書き換えます。

  • シングルクォートをダブルクォートに変換
  • id, implementation, testImplementationはかっこで括るようにする
  • sourceCompatibilityの前にjava.をつける
  • task.named('test')tasks.withType<Test>に変更する
plugins {
	id("java")
	id("org.springframework.boot") version "2.7.6"
	id("io.spring.dependency-management") version "1.0.15.RELEASE"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-web-services")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
	useJUnitPlatform()
}

Kotlinを使っている方からすると、pluginsidはメソッド、sourceCompatibilityてjavaのプロパティなんだ、ということが分かってくるかと思います。

大きな変更点としてはtasks.named('test')withTypeに変えています。
これは拡張関数として定義されており、ジェネリクスでタスクを絞り込んで拡張していくことが可能になっています。
https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:locating_tasks


Kotlinの固有の文法がわからないという方はこちら

  • ブラケットで囲まれている部分なに高階関数の呼び出し時、functionの部分はブラケットで囲って書くことができます
    https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/lambdas.html
  • withType、Groovyで書こうとすると無いんだけど
  • Kotlinには拡張関数という機能があります
  • 以下のように既存のクラスに対してメソッドを足すような書き方ができます。
```kotlin
fun String.hello() {
  return "hello"
}

print("hoge".hello()) // hello
```

マルチモジュール化

まずはモジュールを配置するディレクトリを作成し、その中にそれぞれbuild.gradle.ktsを作成していきます。

mkdir -p api/src/main/java/com/example/demo/controller
mkdir -p domain/{todo,project}/src/main/java/com/example/demo
mkdir -p buildSrc/src/main/kotlin

touch api/build.gradle.kts
touch domain/{todo,project}/build.gradle.kts
touch buildSrc/build.gradle.kts

次に、1で作成したクラスたちを移動させていきます。

mv src/main/java/com/example/demo/api/* api/src/main/java/com/example/demo/controller/
mv src/main/java/com/example/demo/domain/todo/ToDo.java domain/todo/src/main/java/com/example/demo/
mv src/main/java/com/example/demo/domain/project/ToDo.java domain/project/src/main/java/com/example/demo/


setting.gradle の修正

ルートにsetting.gradleがあるので、kts化して以下のように記載します。

rootProject.name = "gradle-sample"
include(":domain:todo", ":domain:project", "api")

作成するモジュールはincludeにモジュールの名称をStringの配列で渡します。
モジュールが階層化されている場合、: で区切って記載します。

buildSrc について

このディレクトリは特別なものとなっています。
Gradleは実行時にbuildSrcを探して、このモジュールは自動的にビルドされ、build scriptのクラスパスに追加されます。

要はここに置いたものはプラグイン化されます。
プラグインなので、他のプロジェクトから読み込むことが可能です。


今回作っていくプラグインもKotlinのコードなので、そのコードをビルド・実行できるようにする設定を書きます。今回はJavaプロジェクト全体で使いそうなプラグインと、Spring Bootを利用するプラグインの二種類を作っていきます。
まずはbuildSrc配下にあるbuild.gradle.ktsを修正していきます。

buildSrc/build.gradle.kts

// ktsを使って設定を書いていくので、kotlin-dslプラグインを利用する
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

// Spring Bootのバージョンはここで指定する
dependencies {
    implementation("org.springframework.boot:spring-boot-gradle-plugin:2.7.6")
}

Spring Bootの依存を抜いたプラグインを作成します。
また必須では無いですが、ツールチェインを利用するようにします。

buildSrc/src/main/java/java-common-conventions.kts

plugins {
    java
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

// Javaの設定をtoolchainに寄せる
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

続いて、Spring Bootの依存を別ファイルに記述します。

buildSrc/src/main/java/spring-conventions.kts

plugins {
    // 作成したローカルプラグインは通常のプラグインと同様にid()で読み込める
    id("java-common-conventions")
    id("org.springframework.boot")
    id("io.spring.dependency-management")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-web-services")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

以上でbuildSrcの設定は終わりです。

作成したローカルプラグインを domain, api で読み込む

プラグインができたので、次は利用する側の設定をします。

apiとそれぞれのdomainでモジュールを切り、細かく依存を定義してあげることでモジュラモノリス構成を目指します。

domain/todo/build.gradle.kts

plugins {
    id("java-common-conventions")
}

domain/project/build.gradle.kts

plugins {
    id("java-common-conventions")
}

// モジュールはproject()で読み込むことができる
dependencies {
    implementation(project(":domain:todo"))
}


api/build.gradle.kts

plugins {
    id("spring-conventions")
}

dependencies {
    implementation(project(":domain:project"))
    implementation(project(":domain:todo"))
}

ポイントは以下です。

  • domainはSpring Bootへの依存はいらないのでjavaの共通設定だけをプラグインで読み込む
  • TodoというdomainはProjectのことを知らなくても良いので現状どこにも依存していない
  • 逆にProjectというdomainはToDoに依存しているのでモジュールとしてTodoをロードする
  • apiはひとまとめにしているので基本的に全部読み込み

今回は簡略化のためにapiからdomainを直接読み込む構成にしていますが、api ← application ← domain ← infrastructureのようにオニオンアーキテクチャチックに依存を定義することももちろん可能です。

動かしてみる

ビルドロジックの構築は以上となります。
同様に動かしてみて、レスポンスが返ってくれば成功です。

./gradlew bootRun

curl http://localhost:8080/api/todos
curl http://localhost:8080/api/projects

最終的にできたものはこちらになります。

GitHub - mako-makok/spring-boot-multi-module-sample: spring-boot-multi-module-sample
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.
https://github.com/mako-makok/spring-boot-multi-module-sample/tree/main


終わりに

Gradleの紹介と実際にマルチモジュール化のハンズオンを行いました。
Gradleは他にもたくさんの機能があり、ビルドやCI/CDを高速化・効率化させることができます。

紹介したもの以外にも様々なプラクティスがありますので、この記事を期にGradleに興味を持っていただけると嬉しいです。
Gradleは公式ドキュメントがとても充実しているので、困ったときは是非参考にしてください。


参考リンク

株式会社ログラスでは一緒に働く仲間を募集しています
同じタグの記事
今週のランキング
株式会社ログラスからお誘い
この話題に共感したら、メンバーと話してみませんか?