raahii.meのブログのロゴ画像

ウェブログ

GradleでConvention Pluginを使い設定や依存を共通化する

前回書いた Gradleでテストの依存を共通化する を少し一般化したような内容です。GradeでKotlin & Spring Bootのミニマムなプロジェクトをつくる で作成したプロジェクトを拡張してマルチプロジェクト化しつつ、Gradleプラグインの設定の共通化方法を整理します。

前提

あまりないかと思いますが、今回は例として、Spring Web MVCを使用するアプリケーションとWebFluxを使用するアプリケーションが同じプロジェクトにあると仮定します。Spring周りの依存やKotlinのコンパイル設定を共通化しつつ、それぞれのappのsubprojectでは独自の設定だけを持つ、という風に変えていきたいと思います。

初期のディレクトリ構成はこのような形。apps 以下に webmvcwebflux の2つのsub projectがあります。

.
├── apps
│   ├── webflux
│   │   ├── build.gradle.kts
│   │   └── src
│   │       └── main
│   │           └── kotlin
│   │               └── App.kt
│   └── webmvc
│       ├── build.gradle.kts
│       └── src
│           └── main
│               └── kotlin
│                   └── App.kt
├── buildSrc
│   └── src
│       └── main
│           └── kotlin
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
└── settings.gradle.kts
  • apps/webflux/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.spring)
    alias(libs.plugins.springboot)
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform(libs.springboot.bom))
    implementation(libs.springboot.starter.webflux)
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
  • apps/webflux/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.spring)
    alias(libs.plugins.springboot)
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform(libs.springboot.bom))
    implementation(libs.springboot.starter.web)
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

ref. https://github.com/raahii/kotlin-spring-boot-multi-projects/tree/ef6de8ca7873d3f6146480ea37b0cfef95f5d95e

それぞれのappは ”Hello, World!" を返すコントローラーだけを持っています。webmvcではtomcatが、webfluxではnettyを使用してappが動いていることを確認しておきます。

  • webflux
❯ ./gradlew :apps:webflux:bootRun
...
> Task :apps:webflux:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.2)

2025-03-23T18:55:30.455+09:00  INFO 15264 --- [           main] org.example.webflux.AppKt                : Starting AppKt using Java 17.0.7 with PID 15264 (/Users/raahii/repos/src/github.com/raahii/kotlin-spring-boot-multi-projects/apps/webflux/build/classes/kotlin/main started by raahii in /Users/raahii/repos/src/github.com/raahii/kotlin-spring-boot-multi-projects/apps/webflux)
2025-03-23T18:55:30.471+09:00  INFO 15264 --- [           main] org.example.webflux.AppKt                : No active profile set, falling back to 1 default profile: "default"
2025-03-23T18:55:31.652+09:00  INFO 15264 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080 (http)
2025-03-23T18:55:31.673+09:00  INFO 15264 --- [           main] org.example.webflux.AppKt                : Started AppKt in 1.667 seconds (process running for 2.039)
  • webmvc
❯ ./gradlew :apps:webmvc:bootRun
...

> Task :apps:webmvc:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.2)

2025-03-23T18:56:18.984+09:00  INFO 15393 --- [           main] org.example.webmvc.AppKt                 : Starting AppKt using Java 17.0.7 with PID 15393 (/Users/raahii/repos/src/github.com/raahii/kotlin-spring-boot-multi-projects/apps/webmvc/build/classes/kotlin/main started by raahii in /Users/raahii/repos/src/github.com/raahii/kotlin-spring-boot-multi-projects/apps/webmvc)
2025-03-23T18:56:18.987+09:00  INFO 15393 --- [           main] org.example.webmvc.AppKt                 : No active profile set, falling back to 1 default profile: "default"
2025-03-23T18:56:19.883+09:00  INFO 15393 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-03-23T18:56:19.896+09:00  INFO 15393 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-03-23T18:56:19.896+09:00  INFO 15393 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.34]
2025-03-23T18:56:19.937+09:00  INFO 15393 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-03-23T18:56:19.939+09:00  INFO 15393 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 870 ms
2025-03-23T18:56:20.257+09:00  INFO 15393 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-03-23T18:56:20.269+09:00  INFO 15393 --- [           main] org.example.webmvc.AppKt                 : Started AppKt in 1.733 seconds (process running for 2.095)

OKです。

それでは、これらの2つのアプリケーションの設定を Convention Plugin という仕組みで共通化していきます。

Convention Plugin とは

通常Gradle Pluginはremote repositoryから取得して利用しますが、Convention Pluginと呼ばれる仕組みを使うとローカルディレクトリに配置したビルドスクリプト(*.gradle.kts)をpluginとしてimportできるようになります。今回のように、あるプロジェクト内でビルドロジック(Gradleの設定)を共通化したい場合に便利です。

convention pluginは任意のディレクトリに配置できます。プロジェクトルートの buildSrc はGradle専用のディレクトリであるため、ここに配置する場合は明示的な設定なしに利用できます。他のディレクトリに配置しても、settings.gradle.kts 内で includeBuild() を宣言することによって、convention pluginとして読み込むことが出来ます(これはComposite Buildと呼ばれている、と思います)。

ちなみに余談ですが、前回の Gradleでテストの依存を共通化する の記事で書いたように、設定を共通化するときにルートの build.gradle.kts を使うという、極めてストレートなやり方もあります。しかしこの方法だと、sub project Aには適用したいが、sub project Bには適用したくないといった設定が増えるに連れ、ビルドスクリプトが複雑化します。また、変更差分から影響のある箇所だけをビルドし直すincremental buildの恩恵を受けらなくなり、ビルドパフォーマンスが悪化してしまいます。そのため、ルートの build.gradle.kts には、単に全体に適用して良いものだけを記述するようにするのが良いです。 ref. Do not use cross-project configuration

Convention Plugin でビルドロジックを共通化する

今回は buildSrc ディレクトリにpluginを配置していきます。 まず、build.gradle.kts を定義し、kotlin-dsl プラグインを使用します。ktsファイルをpluginとして認識するために必要なものです。

  • buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

次に、settings.gradle.ktstypesafe-conventions plugin を使うようにします。

  • buildSrc/settings.gradle.kts
plugins {
    id("dev.panuszewski.typesafe-conventions") version "0.5.1"
}

typesafeConventions {
    // enable or disable support for version catalog typesafe accessors in plugins block of a convention plugin
    accessorsInPluginsBlock = true

    // enable or disable auto dependency for every alias(...) plugin declaration in a convention plugin
    // set it to 'false' if you prefer to add plugin marker dependencies manually (you can use the pluginMarker helper method for that)
    autoPluginDependencies = true
}

これは必須ではないのですが、無いと buildSrc内でversion catalogを使うことができないです。入れることで import libs するだけでversion catalogに定義したライブラリを参照できるようになり、しかもちゃんと plugins ブロックでも dependencies ブロックでも使えます(実はここがミソだったりします。詳しく知りたい方は この辺りのissue をご覧ください)。

そうしたらあとはプラグインを定義していきます。まずは kotlin.gradle.kts を作成します。

  • buildSrc/src/main/kotlin/kotlin.gradle.kts
package convention

import libs

plugins {
     alias(libs.plugins.kotlin.jvm)
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

共通化の粒度はお好みですが、このファイルではkotlin jvmプラグインを利用する宣言と、コンパイル周りの設定だけを記述するファイル、ということにします。

さらに app.gradle.kts でwebmvcとwebfluxのアプリケーションも共通化します。

  • buildSrc/src/main/kotlin/app.gradle.kts
package convention

import libs

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.spring)
    alias(libs.plugins.springboot)

    id("convention.kotlin")
}

dependencies {
    implementation(platform(libs.springboot.bom))
}

spring関連のプラグインとしてkotlin springとspringbootプラグインを宣言します。さらに、先程作成した kotlin.gradle.kts を宣言します。pluginsブロック内で id(“${package名}.${拡張子抜きファイル名}") と書くことでconvention pluginを利用できます。このように、convention pluginからconvention pluginを呼び出すことも可能です。また、共通の依存であるspring bootのBOMの宣言も共通化しています。

subprojectでConvention Plugin を使う

最後に、各アプリケーションでは、id(“convention.app) を使うことで設定をスリムにできます。

  • apps/webflux/build.gradle.kts
 plugins {
-    alias(libs.plugins.kotlin.jvm)
-    alias(libs.plugins.kotlin.spring)
-    alias(libs.plugins.springboot)
+    id("convention.app")
 }

 ...

 dependencies {
-    implementation(platform(libs.springboot.bom))
     implementation(libs.springboot.starter.webflux)
 }

-java {
-    toolchain {
-        languageVersion = JavaLanguageVersion.of(17)
-    }
-}
  • apps/webflux/build.gradle.kts
 plugins {
-    alias(libs.plugins.kotlin.jvm)
-    alias(libs.plugins.kotlin.spring)
-    alias(libs.plugins.springboot)
+    id("convention.app")
 }

 ...

 dependencies {
-    implementation(platform(libs.springboot.bom))
     implementation(libs.springboot.starter.web)
 }

-java {
-    toolchain {
-        languageVersion = JavaLanguageVersion.of(17)
-    }
-}

設定を共通化したあとも、各アプリケーションが問題なく立ち上がることを確認できました。

今回はかなり単純化された設定ですが、他にもtestの設定や3rd partyプラグインの設定なども適宜ファイルを分けながら綺麗に管理できます。また、一部のsub projectにしか適用しない設定があっても、convention pluginを切って必要なsub projectでのみ利用することで、適用範囲も明確に分けることが出来るようになりますね!

最後に

「Convention Plugin とは」の節で説明したように、convention pluginはcomposite buildを使うことで任意のディレクトリに配置できます。それは素晴らしいことなのですが、現状(2025/03)では、buildSrctypesafe-conventions pluginを組み合わせなければ、convention plugin内のpluginsブロックでversion catalogが利用できない(はず)です。なので、buildSrc に置くのが安牌かなと思っています。あるいは、convention pluginによってGradle pluginの宣言が集約されることを考えると、pluginに関してはversion catalogを諦めるのも手かもしれません。まずこれが1つ。

あとは、Gradle周りの設定はこれくらいシンプルな所から整理して始めるのがおすすめです。私の苦手意識が強いだけかもしれませんが、Gradleはbuild scriptの設定によって、build script内で宣言可能なブロックや利用可能なタスクが増えたりします。要するに、ただ設定を宣言しているのではなく、設定をプログラミングしているわけです(programable configuraionって言うのかな…)。なので、build script自体のビルドがコケだすと訳わからなくなったりするんですよね。これはconvention pluginを使う時に限った話ではないのですが、日頃感じているのでポロッと最後に書いてみました。

参考