サーバーサイドKotlinでマルチモジュールをやる2022

(もともとあった)マルチモジュール構成を更にリファクタしたときにドハマリしたので備忘がてらメモ。

環境

  • JVM 16.0.2
  • Kotlin 1.4.31
  • Gradle 7.0.2

完成形

これを

├── appA
│   ├── presentation
│   ├── application
│   ├── domain
│   ├── infrastructure
│   └── src
└── appB
    ├── presentation
    ├── application
    ├── domain
    ├── infrastructure
    └── src

こうした

├── appA
│   ├── presentation
│   └── src
├── appB
│   ├── presentation
│   └── src
└── modules
    ├── application
    ├── domain
    └── infrastructure

パッケージの最適な配置がどうなるか手探りな状態でappAの開発を始めたけど、appBも順調に伸びてきて、そろそろ共通で使えるようにしようかな、みたいなところが動機。
CQRSの考え方を取り入れていて、module/domain, module/infrastructureの下は更にcommandまたはqueryでサブモジュールになっている。(フラグ)

tips: モジュールのおまとめにディレクトリを使う

親のディレクトリはモジュールでなくていいとき
マルチモジュールのすゝめのkts版

// settings.gradle.kts
val modulesDir = File("modules")
project(":application").projectDir = File(modulesDir, "application")

参照時は :modules:application ではなく :application となる点に注意

tips: モジュールお引越しは先にpackageをrenameする

新しくディレクトリを切って、そこにsrc以下をmvすると依存する側が自動で追ってくれないので、IntelliJのProjectペインから該当packageを右クリック→refactor→renameでパッケージを変更してからmvすると多少楽だった。
importが追随しきれていないこともあるので、その場合は一括置換をかけてrebuildした。

ハマった: ネストしたモジュールがimplementationできない

A: 事前に親モジュールを評価しておく

// modules/application/build.gradle.kts
evaluationDependsOn(":domain:foo")

dependencies {
    implementation(project(":domain:foo:query"))
}

ハマった: jar filenameのデフォルトが最下位のサブモジュール名

:domain:foo:query :infrastructure:foo:query と最下位モジュール名が同じ場合に、 docker compose build が以下メッセージで失敗した。

Entry BOOT-INF/lib/query-plain.jar is a duplicate but no duplicate handling strategy has been set.

IntelliJでのビルド、実行はできる状態。はて…?

ググるGradle7へのお気持ちtasks.Copy:duplicatesStrategy をEXCLUDEなりに設定しろというのが出てくるけど、EXCLUDE(重複した場合に取り除く)でもINCLUDE(重複した場合に上書きする)でもWARNでも、設定すると依存先のクラスがないと言われてエラーになる。

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'XXX' defined in URL [jar:file:/app.jar!/BOOT-INF/lib/presentation-plain.jar!/path/to/Controller.class]

query-plain.jar を見てみよう

$ docker run --rm -it container-name:latest sh

/ cp app.jar tmp/
/ cd tmp/
/ jar xvf app.jar
/ cd BOOT-INF/lib/
/ jar tf query-plain.jar
# domain(or infrastructure)しかない!

諦めてモジュール名を変えてもいいけど、絶対それっぽい設定値あるでしょ…とドキュメントやbuild.gradle.ktsをコネコネしてそれっぽい着地を見る

// modules/infrastructure/foo/query/build.gradle.kts

tasks.jar {
    enabled = true
    // 生成されるjar fileの名前が一意になるようにする
    archiveBaseName.set("infrastructure-foo-query")
}

※ 直接文字列で指定しなくてもなんらかいい方法ありそう
※ Groovy式だと直接代入しているが、Kotlinで記法が変わった様子

参考

Spring Boot 2 (Kotlin) + Gradle Kotlin DSL でマルチモジュールを実現してみた - Qiita ServerSide Kotlin Appをマルチモジュール化する - ロコガイド テックブログ