📑

なんとなく使わないGradle

2022/12/18に公開

はじめに

最近スパイスカレーを食べるのはもちろん、作るのにもハマっている小林(@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_application

プラグインの利用は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
  • Dependencies
    • Spring 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の固有の文法がわからないという方はこちら
  • ブラケットで囲まれている部分なに

  • 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のクラスパスに追加されます。

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

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

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

最終的にできたものはこちらになります。
https://github.com/mako-makok/spring-boot-multi-module-sample/tree/main

終わりに

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

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

参考リンク

GitHubで編集を提案
株式会社ログラス テックブログ

Discussion

ログインするとコメントできます