みなさんこんにちは、電通国際情報サービス(ISID)コーポレート本部 システム推進部の佐藤太一です。
本日は最新のGradle(2022/08現在)を使いこなしながらKotlinでJavaのアプリケーションをビルドするスクリプトを書く際に、知っておくと便利なノウハウをまとめてご紹介します。
はじめに
この記事では、ソースコードをユーザーが利用できる環境にデプロイできるアプリケーションに変換する工程をビルドプロセスと呼ぶこととします。
Javaで実装されたアプリケーションをビルドするためのツールとしては、古くはmakeから始まり、Antを経由して現代ではMavenやGradleといったツールが広く利用されています。
この記事では、自分がコントロールできる範囲では宣言的に記述でき、必要に応じてアドホックなコードを書いていけるGradleを使ったビルドプロセスにおけるノウハウを紹介します。
記事の執筆環境
この記事が前提とする環境について軽く説明しておきましょう。
まず、OSはWindows10 Proで、バージョンは21H2です。 Windows環境でアプリケーションをインストールするために、パッケージマネージャーとしてscoopを使っています。 そして、Java 17を使います。
記事の主題はGradleの使い方ですので、Windows以外のOSでも有効なものです。 Windows以外の環境をお使いなら次に続く環境のセットアップ手順については読み飛ばしてください。
scoopのセットアップ
scoopはpowershellだけで様々なツールをインストールできるパッケージマネージャーの一種です。 この記事で紹介するツールはscoopを使ってインストールしますので、もし未導入なら導入をお願いします。
まずは、PowerShellの実行ポリシーを変えます。
scoopのインストールのためには、インターネット上にあるサーバからシェルスクリプトをダウンロードしてきて実行できるようにします。cf. about_Execution_Policies
PowerShellを起動して以下のコマンドを実行します。
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
次にインストールスクリプトをダウンロードしつつ実行します。
irm get.scoop.sh | iex
これでscoopのインストールは終わりです。
Javaのセットアップ
scoopでJavaをインストールする前にバケットを追加します。
scoop bucket add java
このバケットには様々なOpenJDKのビルドが入っているのですけども、ここではopenjdk17を使います。
scoop install openjdk17
openjdkを指定すると、記事執筆時点ではJava 18がインストールされましたのでメジャーバージョンを明示的に指定しています。
Gradleのセットアップ
次はGradleをインストールしましょう。
scoop instlal gradle
Gradleのインストールが完了したらセットアップは終了です。
サンプルアプリケーションについて
この記事ではユーザーインターフェースのあるウェブアプリケーションと、ユーザーインターフェースのないバッチアプリケーションが、単一のgitリポジトリ内に共存しているものを想定しています。
典型的な構造ですが、様々なやり方でビルドスクリプトを記述できます。この記事では私が実施しているプロジェクトにおける構成において得られた知見をもとに説明します。
ルートプロジェクトの実装
まずは、プロジェクト全体を格納するためのディレクトリを作成しましょう。
ここからは、この記事内でシェルコマンドを実行するよう説明している部分では、必ずこのルートディレクトリで実行してください。
作るプロジェクトは、説明のために demo-project
とします。作ったdemo-projectディレクトリの中で、以下のコマンドを実行して最小限のプロジェクトを作成します。
gradle init --type basic --dsl kotlin --project-name demo-project --incubating
最小限とはいえ、Gradle Wrapperとなるシェルスクリプトやgit用の設定ファイルが生成されていますね。
その中から、不要な build.gradle.kts
を削除してください。
ウェブアプリケーションプロジェクトの実装
次はSpring Bootを使ったウェブアプリケーションをビルドする環境を作ってみましょう。
ビルドスクリプトの作成
demo-projectディレクトリにweb
ディレクトリを作成します。次に、以下の内容でbuild.gradle.kts
を作成します。
plugins { // 1. id("java") id("org.springframework.boot") version "2.7.2" id("io.spring.dependency-management") version "1.0.13.RELEASE" } group = "com.example" // 2. version = "0.0.1-SNAPSHOT" // 3. repositories.mavenCentral() // 4. dependencies { // 5. implementation("org.springframework.boot:spring-boot-starter") } java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) // 6. tasks.withType<JavaCompile>().configureEach { // 7. options.encoding = "UTF-8" }
このビルドスクリプトについて説明しておきましょう。
- このプロジェクトで利用するプラグインを列挙しています。
- ビルド成果物として出力されるアーティファクト(JARファイル)のグループIDとなる部分の宣言です。基本的には会社のドメイン名を逆にしたものを使っておきましょう。
- ビルド成果物のバージョン番号を記述しています。バージョン番号の管理についてはこの後、もう少し詳しく説明します。
- 依存ライブラリをダウンロードする先を宣言しています。ここではMavenのデフォルトアーティファクトリポジトリであるMaven Central Repositoryを指定しています。
- このプロジェクトが依存するライブラリを列挙しています。
io.spring.dependency-management
によって機能性が拡張されているため、バージョン番号の指定がありません。org.springframework.boot:spring-boot-starter
がSpring Bootの本体です。
- このビルドスクリプトで利用するコンパイラやランタイムのバージョン番号を指定しています。
- このビルドスクリプト内に定義されているタスクを型指定で検索しています。
このビルドスクリプトがルートプロジェクトの一部であることを記述しておきましょう。
demo-project直下にあるsettings.gradle.kts
にinclude("web")
を追記します。
その結果以下のようになるでしょう。
/* * This file was generated by the Gradle 'init' task. * * The settings file is used to specify which projects to include in your build. * * Detailed information about configuring a multi-project build in Gradle can be found * in the user manual at https://docs.gradle.org/7.4.2/userguide/multi_project_builds.html * This project uses @Incubating APIs which are subject to change. */ rootProject.name = "demo-project" include("web") // ルートプロジェクトに含めるために追記した部分
コメントアウトされている部分に意味はないので消してしまって構いません。
ビルドスクリプトが正しく記述できているか確認しておきましょう。以下のコマンドを実行します。
gradle tasks --group=application
Application tasksとして bootRun
が出力されていればビルドスクリプトは上手く構成されています。
サンプルアプリケーションの実装
ビルドスクリプトを配置したらソースコードを格納するためのディレクトリを作成しましょう。以下のコマンドを実行します。
mkdir web/src/main/java/com/example/web
以下の内容でdemo-project/web/src/main/java/com/example/web
ディレクトリにMain.java
を作成します。
package com.example.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
詳細に説明することはしませんが、これはSpring Bootアプリケーションを起動するコードです。
アプリケーションをビルドしてアーティファクトを生成してみましょう。以下のコマンドを実行します。
gradle build
ビルドが成功したら demo-project/web/build/libs
ディレクトリの中に web-0.0.1-SNAPSHOT.jar
が出力されます。
バージョニング
ビルドプロセスを検討するにあたって、生成物に対してバージョン番号をどのように付与するのかは悩ましい課題です。
開発者のローカル環境でビルドしているなら、リリース作業をする前にbuild.gradle.kts
にあるversion
の値をエディタで書き換えてからビルドすればいいでしょう。作業が成功したら変更差分としてコミットしておけば問題ありません。
しかし、再現性のあるビルドを行うならGitHub ActionsのようなCIサーバ上でビルドするのが望ましいでしょう。そうした時、build.gradle.kts
にあるversion
の値を書き換えるのは少々やっかいです。
僕が好んで使っているやり方は、gitのタグをプッシュして、その名前をバージョン番号を決める際のパラメータとする方法です。 このやり方をサポートしてくれるGradleのプラグインがme.qoomon.git-versioningです。
このプラグインを使うとタグ名やブランチ名をパラメータにしてバージョン番号を簡単に決められます。具体例をお見せしましょう。
plugins { id("java") id("org.springframework.boot") version "2.7.2" id("io.spring.dependency-management") version "1.0.13.RELEASE" id("me.qoomon.git-versioning") version "6.3.0" // 1. } group = "com.example" version = "0.0.1-SNAPSHOT" gitVersioning.apply { // 2. refs { considerTagsOnBranches = true // 3. tag("v(?<version>\\d+[.]\\d+[.]\\d+)") { // 4. version = "\${ref.version}" // 5. } } } repositories.mavenCentral() dependencies { implementation("org.springframework.boot:spring-boot-starter") } java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
先ほどのコードに対してバージョニングするためのプラグインを組み込んでいます。
me.qoomon.git-versioning
がバージョニングするためのプラグインIDです。- バージョン番号を決める
version
変数の直後に、gitVersioning.applyブロックを宣言しています。- これによって、バージョニングプラグインが動作しなかった時は、最初に定義したバージョン番号が利用されます。
considerTagsOnBranches
をtrueにすることで常にタグを考慮してバージョン番号を決定します。- ここで指定されている正規表現にタグがマッチしている場合にタグをバージョン番号として使います。
- バージョン番号をマッチするための正規表現の中にある名前を使って具体的にバージョン番号として採用する範囲を決めています。
ref.version
では専用の式言語を使って正規表現と一致した部分を取り出しています。$
記号の前に\
があるのは、Kotlinの文字列テンプレートとしては解釈されないようにするためです。
プラグインが正しく構成されているか確認してみましょう。以下のコマンドを実行します。
gradle web:version
この時点ではタグを打っていないので、version変数に代入されている0.0.1-SNAPSHOT
がバージョン番号として出力されます。
では、以下のコマンドを実行してタグを打った上で、同じようにバージョン番号を出力してみましょう。
git tag v0.1.2 gradle web:version
バージョン番号として 0.1.2
が出力されましたね。
その他のバージョニングプラグイン
タグプッシュを起点にビルドする以外の方式でバージョン番号を決めたい場合には、以下のプラグインについて検討するといいでしょう。
バッチプロジェクトの実装
次はバッチアプリケーションを作ってみましょう。
demo-projectディレクトリにbatch
ディレクトリを作成します。次に、以下の内容でbuild.gradle.kts
を作成します。
plugins { id("application") // 1. id("me.qoomon.git-versioning") version "6.3.0" } group = "com.example" version = "0.0.1-SNAPSHOT" gitVersioning.apply { // 2. refs { considerTagsOnBranches = true tag("v(?<version>\\d+[.]\\d+[.]\\d+)") { version = "\${ref.version}" } } } repositories.mavenCentral() dependencies { implementation("com.google.guava:guava:30.1.1-jre") } application { mainClass.set("com.example.batch.Main") // 3. } java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
ウェブアプリケーションのものと似ていますが、このビルドスクリプト固有の部分について説明しておきましょう。
- applicationプラグインは、JavaでCLIアプリケーションを実装するための定義を自動的に実施します。
- ウェブアプリケーションと同じバージョニングポリシーを採用するので、
me.qoomon.git-versioning
を導入します。 - ここでは、CLIアプリケーションとして起動する際に使うmainメソッドのあるクラスを指定しています。
demo-project直下にあるsettings.gradle.kts
にinclude("batch")
を追記します。
その結果以下のようになるでしょう。
rootProject.name = "demo-project" include("web") include("batch") // ルートプロジェクトに含めるために追記した部分
バッチアプリケーションの実装
ビルドスクリプトを配置したらソースコードを格納するためのディレクトリを作成しましょう。以下のコマンドを実行します。
mkdir batch/src/main/java/com/example/batch
以下の内容でdemo-project/batch/src/main/java/com/example/batch
ディレクトリにMain.java
を作成します。
package com.example.batch; import com.google.common.base.Strings; public class Main { public static void main(String[] args) { System.out.println(Strings.repeat("Hello, ", 2) + "World."); } }
詳細に説明することはしませんが、CLIアプリケーションとして起動するコードです。 この後で説明するFat Jarの動作を確認できるようにGuavaのメソッドを使っています。
Fat/Uber Jarの作り方
Javaの実行バイナリの形式として、依存ライブラリを全て一つのJARファイルに含めてしまう形式の事を、Fat Jar や Uber Jarと呼びます。 ビルド生成物を単一のファイルにすると、その後の取り回しが単純化するのでデプロイプロセスの安定性が高まります。
Gradleでは Gradle Shadow プラグインを使うと簡単にFat/Uber Jarがつくれます。
既に書いたバッチアプリケーションのビルドスクリプトに、このプラグインを導入してみましょう。
plugins { id("application") id("me.qoomon.git-versioning") version "6.3.0" id("com.github.johnrengelman.shadow") version "7.1.2" // プラグイン導入 } group = "com.example" version = "0.0.1-SNAPSHOT" gitVersioning.apply { refs { considerTagsOnBranches = true tag("v(?<version>\\d+[.]\\d+[.]\\d+)") { version = "\${ref.version}" } } } repositories.mavenCentral() dependencies { implementation("com.google.guava:guava:30.1.1-jre") } application { mainClass.set("com.example.batch.Main") } java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
このビルドスクリプトでは、既にapplicationプラグインが導入されているので、shadowプラグインはそれに相乗りする形で動作します。つまり、プラグインの導入を宣言するだけでFat/Uber Jarが生成されるようになるのです。
では、以下のコマンドでビルドしてみましょう。
gradle batch:build
ビルドが正常終了したら、以下のコマンドを実行して生成された成果物を確認してみましょう。
ls batch/build/libs
大きく膨らんだbatch-0.0.1-SNAPSHOT-all.jar
がFat/Uber Jarです。今回は、この中にGuavaおよびその依存ライブラリが全て含まれています。
ビルドにおける共通処理の切り出し
ここまで、ウェブアプリケーションとバッチアプリケーションのビルドスクリプトを作ってきたわけですが、かなりの重複があることにお気づきでしょうか?
オレンジ色の枠で囲った部分が二つのビルドスクリプトにおける重複部分です。 これらはビルド対象プロジェクトが増えるたびに重複していくでしょう。それは、望ましくありません。
ここからは、Gradleのローカルプラグインを使ってビルドスクリプトの共通部分を切り出す方法について説明します。
共通部分をプラグイン化することによって、それぞれのビルドスクリプトはより小さくて分かり易いものになります。また、プロジェクトにおけるバージョニングなどのポリシーを一か所に書くことで、ポリシーの変更があった際に抜け漏れなく対応できます。
ローカルプラグインの作り方
それでは、ローカルプラグインの作り方を説明しましょう。
demo-projectディレクトリにbuildSrc
ディレクトリを作成します。次に、以下の内容でbuild.gradle.kts
を作成します。
plugins { `kotlin-dsl` // 1. } repositories.gradlePluginPortal() // 2.
Gradleはルートプロジェクト直下にあるbuildSrc
ディレクトリをローカルプラグインが実装されているプロジェクトであるとみなします。少し奇妙だと感じるかもしれませんが、慣れてしまいましょう。
- このプロジェクトは
kotlin-dsl
を使ってプラグインを記述します。 - このプロジェクトはプラグインプロジェクトになるので、依存ライブラリをGradle - Pluginsからダウンロードします。
ローカルプラグインの実装
ビルドスクリプトを配置したらソースコードを格納するためのディレクトリを作成しましょう。以下のコマンドを実行します。
mkdir buildSrc/src/main/kotlin
以下の内容でdemo-project/buildSrc/src/main/kotlin
ディレクトリにcom.example.commons.gradle.kts
を作成します。
plugins { id("java") } group = "com.example" version = "0.0.1-SNAPSHOT" repositories.mavenCentral() java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
ここでは、Gradleの標準機能だけを使って共通化しています。
これで、com.example.commons
をプラグインIDとして参照できるローカルプラグインの実装は終わりです。非常に簡単ですね。
ローカルプラグインの使い方
では、ローカルプラグインを使ってウェブアプリケーションのビルドスクリプトを簡略化してみましょう。
まずは、ウェブアプリケーションのビルドスクリプトにローカルプラグインを適用します。
plugins { id("com.example.commons") // ローカルプラグインの導入 id("org.springframework.boot") version "2.7.2" id("io.spring.dependency-management") version "1.0.13.RELEASE" id("me.qoomon.git-versioning") version "6.3.0" } gitVersioning.apply { refs { considerTagsOnBranches = true tag("v(?<version>\\d+[.]\\d+[.]\\d+)") { version = "\${ref.version}" } } } dependencies { implementation("org.springframework.boot:spring-boot-starter") }
差分を確認してみましょう。
かなり小さくなりましたね。同じことをバッチアプリケーションのビルドスクリプトでもやっておきましょう。
ローカルプラグインにGradleプラグインを組み込む
次は、バージョニングプラグインの適用もローカルプラグインの中でやってみましょう。
まずは、誤った例をお見せします。
この状態でローカルプラグインがビルドされると以下のようなエラーが出力されます。
Invalid plugin request [id: 'me.qoomon.git-versioning', version: '6.3.0']. Plugin requests from precompiled scripts must not include a version number. Please remove the version from the offending request and make sure the module containing the requested plugin 'me.qoomon.git-versioning' is an implementation dependency of project ':buildSrc'.
これは、ローカルプラグインの実装でプラグインを指定する場合には、冒頭のpluginsブロックの中でバージョン番号を指定できないというエラーです。このローカルプラグインからはバージョン番号を削除した上で、buildSrcプロジェクトにおける依存性としてプラグインを宣言するように指示されています。
具体的にどうするのかというと、buildSrcディレクトリ内にあるbuild.gradle.ktsを以下のように修正します。
plugins { `kotlin-dsl` } repositories.gradlePluginPortal() dependencies { implementation("me.qoomon:gradle-git-versioning-plugin:6.3.0") // 依存性を追加 }
ここで追加する依存性はアーティファクトID(me.qoomon:gradle-git-versioning-plugin:6.3.0
)を指定します。プラグインID(me.qoomon.git-versioning
)ではありませんのでご注意ください。
次は、ローカルプラグイン内にバージョニングプラグインを適用します。
plugins { id("java") id("me.qoomon.git-versioning") // 1. } group = "com.example" version = "0.0.1-SNAPSHOT" gitVersioning.apply { // 2. refs { considerTagsOnBranches = true tag("v(?<version>\\d+[.]\\d+[.]\\d+)") { version = "\${ref.version}" } } } repositories.mavenCentral() java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
注意深く見てほしい2か所について説明します。
ウェブアプリケーションのビルドスクリプトからバージョン番号の採番を消せました。これで、さらに小さくて読みやすくなりましたね。
plugins { id("com.example.commons") id("org.springframework.boot") version "2.7.2" id("io.spring.dependency-management") version "1.0.13.RELEASE" } dependencies { implementation("org.springframework.boot:spring-boot-starter") }
プラグインで変数や関数を定義する前に
最後は複数のプロジェクトで使いまわせる関数や変数をローカルプラグインで定義してみましょう。
定義の前にGradleにおけるプラグインの動作と変数スコープについて少し説明させてください。
まず、Gradleのビルドスクリプトにおける一番外側のスコープ、つまり暗黙のインスタンスはProject
です。
plugins
やdependencies
といったコードブロックは、Project
クラスのインスタンスに定義されたメソッドをレシーバを指定せずに呼び出しているのです。
そして、GradleのプラグインはProjectインスタンスのメソッドを呼び出したり、動的にメンバを追加することで機能を実現しています。
つまり、複数のプロジェクトで使いまわせる関数や変数をローカルプラグインで定義するというのは、動的なメンバをProjectインスタンスに追加するという事になります。もちろん、dependencies
やtasks
といったスコープに変数や関数を追加することも出来ます。
そして、Gradleプラグインでは動的にメンバを編集する仕組みをExtensionと呼んでいます。
Extensionの実装
それでは、Extensionを使ってプロジェクト固有の変数を追加してみましょう。
以下の内容でdemo-project/buildSrc/src/main/kotlin
ディレクトリにExtension.kt
を作成します。
import org.gradle.api.Project val Project.env: String get() = if (System.getenv("ENV") != null) { System.getenv("ENV").toLowerCase() } else "dev"
Extensionというファイル名はGradleプラグインにおける命名規則によるものです。プラグイン名が自明な場合には、プラグイン名のサフィックスとしてExtension
を指定するのが一般的なようです。
今回は明示的なプラグイン名がないので、単にExtension
としています。
ここでは、プロジェクトスコープにenv
という変数を追加してみました。
これは環境変数のENV
を読み取った上で中身があれば、その値を返します。それが無ければdev
を返すという処理になっています。
これをバッチアプリケーションのビルドスクリプトで使ってみましょう。
plugins { id("com.example.commons") id("application") id("com.github.johnrengelman.shadow") version "7.1.2" } dependencies { implementation("com.google.guava:guava:30.1.1-jre") } application { mainClass.set("com.example.batch.Main") } tasks { shadowJar { archiveClassifier.set("all-" + env) // ファイル名のサフィックスとしてenvの値を使う。 } }
ここでは、shadowプラグインによって出力されるアーティファクト名の一部として、envの値を使うように変更しました。
これによって、ファイル名の一部として環境変数であるENVの値を部分的に使うようになります。
この状態でビルドするとbatch/build/libs/batch-0.0.1-SNAPSHOT-all-dev.jar
というJARファイルが出力されるのを確認できます。
環境変数ENVの中身をprod
に変えてビルドしてください。ビルド結果としてbatch/build/libs/batch-0.0.1-SNAPSHOT-all-prod.jar
というJARファイルが出力されるのを確認できます。
まとめ
このエントリでは、Kotlinを使ってGradleのビルドスクリプトを書く際に知っていると便利なTipsを紹介してきました。ここで紹介したGradleの標準機能や、いくつかのプラグインは私がビルドスクリプトを書く際には必ず利用しているものです。
また、ローカルプラグインにプラグインを追加する際のエラーについては検索エンジンにインデックスされるよう少し冗長に書いています。冗長に書いた理由は、同じエラーに遭遇した誰かに届いてほしいからです。私自身は、このエラーを上手く解決できなくてかなり苦しみましたので、そういう方が一人でも減ってほしいという気持ちがあります。
この長文を最後まで読んでいただき本当にありがとうございます。
私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募をお待ちしています。
執筆:@sato.taichi、レビュー:@higa (Shodoで執筆されました)