Skip to content

第 10 章 · 实战项目:从零搭一个企业级 Gradle 工程 🛠️

学习目标:把前 9 章的所有知识揉到一个真实项目里,亲手搭一个生产级 Java 多模块 Spring Boot 工程,含约定插件、Version Catalog、Build Cache、CI 集成 —— 从此告别"看懂别人的 build 脚本, 但自己写不出来"的尴尬期。


1. 项目背景:MiniShop 微店后端

我们要造一个简易电商后端"MiniShop",技术栈:

  • Spring Boot 3 + Java 17
  • 多模块(app / feature / lib)
  • Build Logic 集中(约定插件)
  • Version Catalog 统一版本
  • 完整 CI 配置(GitHub Actions / GitLab CI 都给)

目标

./gradlew build 一键全量构建 ✅ ./gradlew :app:bootRun 一键启动 ✅ 共享构建逻辑(每个模块只写自己的依赖,不重复 plugins) ✅ 完整 CI 流水线(编译 + 测试 + 缓存)


2. 总体架构

mini-shop/
├── settings.gradle.kts              # 拼装清单 + includeBuild("build-logic")
├── build.gradle.kts                  # 根项目(聚合 task)
├── gradle.properties                 # 性能开关全开
├── gradlew / gradlew.bat / gradle/   # Wrapper
├── gradle/libs.versions.toml         # ★ 统一版本管理

├── build-logic/                      # ★ 共享构建逻辑
│   ├── settings.gradle.kts
│   ├── build.gradle.kts              # kotlin-dsl
│   └── src/main/kotlin/
│       ├── shop.java-conventions.gradle.kts
│       ├── shop.spring-conventions.gradle.kts
│       └── shop.test-conventions.gradle.kts

├── app/                              # SpringBoot 启动
│   ├── build.gradle.kts
│   └── src/main/java/com/shop/MiniShopApp.java

├── feature-user/                     # 用户域
│   ├── build.gradle.kts
│   └── src/main/java/com/shop/user/UserController.java

├── feature-order/                    # 订单域
│   ├── build.gradle.kts
│   └── src/main/java/com/shop/order/OrderController.java

├── lib-domain/                       # 公共领域模型
│   ├── build.gradle.kts
│   └── src/main/java/com/shop/domain/User.java

└── .github/workflows/ci.yml          # GitHub Actions CI

3. 一步步从空目录开始

Step 1:创建 Wrapper

bash
mkdir mini-shop && cd mini-shop
gradle init --type basic --dsl kotlin --project-name mini-shop

会产出 gradlew / gradlew.bat / gradle/wrapper/*

Step 2:写 settings.gradle.kts

kotlin
pluginManagement {
    includeBuild("build-logic")
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
    }
}

rootProject.name = "mini-shop"

include(
    "app",
    "feature-user",
    "feature-order",
    "lib-domain"
)

Step 3:写 gradle/libs.versions.toml(Version Catalog)

toml
[versions]
spring-boot   = "3.2.0"
spring-deps   = "1.1.4"
junit         = "5.10.0"
slf4j         = "2.0.9"
mapstruct     = "1.5.5.Final"
lombok        = "1.18.30"

[libraries]
slf4j-api     = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
junit         = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
mapstruct     = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
lombok        = { module = "org.projectlombok:lombok", version.ref = "lombok" }

[bundles]
testing       = ["junit"]

[plugins]
spring-boot   = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-deps   = { id = "io.spring.dependency-management", version.ref = "spring-deps" }

Step 4:写 gradle.properties(性能全开)

properties
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
# 配置缓存先 warn 模式,跑稳后再改 fail
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

Step 5:写 build-logic 的约定插件

build-logic/settings.gradle.kts

kotlin
dependencyResolutionManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}
rootProject.name = "build-logic"

build-logic/build.gradle.kts

kotlin
plugins { `kotlin-dsl` }

repositories {
    gradlePluginPortal()
    mavenCentral()
}

dependencies {
    // 让 build-logic 里能引用 spring-boot 的 plugin
    implementation("org.springframework.boot:spring-boot-gradle-plugin:3.2.0")
    implementation("io.spring.gradle:dependency-management-plugin:1.1.4")
}

build-logic/src/main/kotlin/shop.java-conventions.gradle.kts

kotlin
plugins {
    java
}
java {
    toolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
}
tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation"))
}

build-logic/src/main/kotlin/shop.test-conventions.gradle.kts

kotlin
plugins {
    id("shop.java-conventions")
}
dependencies {
    "testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0")
    "testRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
tasks.named<Test>("test") {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}

build-logic/src/main/kotlin/shop.spring-conventions.gradle.kts

kotlin
plugins {
    id("shop.test-conventions")
    id("org.springframework.boot")
    id("io.spring.dependency-management")
}
dependencies {
    "implementation"("org.springframework.boot:spring-boot-starter")
    "testImplementation"("org.springframework.boot:spring-boot-starter-test")
}

Step 6:每个子模块只写自己关心的依赖

lib-domain/build.gradle.kts

kotlin
plugins { id("shop.test-conventions") }

看,就一行。所有 Java 编译选项、JUnit 测试都通过约定插件继承了。

feature-user/build.gradle.kts

kotlin
plugins { id("shop.spring-conventions") }
dependencies {
    implementation(project(":lib-domain"))
    implementation("org.springframework.boot:spring-boot-starter-web")
}

feature-order/build.gradle.kts

kotlin
plugins { id("shop.spring-conventions") }
dependencies {
    implementation(project(":lib-domain"))
    implementation(project(":feature-user"))
    implementation("org.springframework.boot:spring-boot-starter-web")
}

app/build.gradle.kts

kotlin
plugins { id("shop.spring-conventions") }
dependencies {
    implementation(project(":lib-domain"))
    implementation(project(":feature-user"))
    implementation(project(":feature-order"))
    implementation("org.springframework.boot:spring-boot-starter-web")
}

Step 7:写一个最简的 Spring Boot 启动类

app/src/main/java/com/shop/MiniShopApp.java

java
package com.shop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.shop")
public class MiniShopApp {
    public static void main(String[] args) {
        SpringApplication.run(MiniShopApp.class, args);
    }
}

Step 8:跑!

bash
./gradlew build
./gradlew :app:bootRun

打开 http://localhost:8080/users/1 应能看到结果。


4. CI/CD 集成

4.1 GitHub Actions

.github/workflows/ci.yml

yaml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      # ★ 启用 Gradle 缓存:极大加速 CI
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3
        with:
          cache-disabled: false
          gradle-home-cache-cleanup: true

      - name: Build
        run: ./gradlew build --scan --no-daemon

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: '**/build/reports/tests/**'

4.2 GitLab CI

.gitlab-ci.yml

yaml
image: eclipse-temurin:17-jdk

stages:
  - build
  - test

variables:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"
  GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"

cache:
  key: "${CI_JOB_NAME}"
  paths:
    - .gradle/caches/
    - .gradle/wrapper/

build:
  stage: build
  script:
    - ./gradlew --version
    - ./gradlew build --no-daemon

test:
  stage: test
  script:
    - ./gradlew test --no-daemon
  artifacts:
    when: always
    reports:
      junit: '**/build/test-results/test/**/TEST-*.xml'

5. 上手即用的 .gitignore

gitignore
# Gradle
.gradle/
build/

# IDE
.idea/
*.iml
out/
.vscode/

# OS
.DS_Store
Thumbs.db

# 日志
*.log

# ⚠️ 不要忽略 wrapper 文件
!gradle/wrapper/gradle-wrapper.jar

6. 检验是否成功的 4 个动作

验证步骤预期
./gradlew projects显示 4 个子模块
./gradlew build编译 + 测试全过,二次构建明显更快
./gradlew :app:bootRunSpring Boot 启动并监听 8080
./gradlew build --scan浏览器看到完整构建报告

7. 项目结构最佳实践 ✨

原则解释
app 模块只装配不写业务,只 import feature 模块和写启动类
feature 之间允许有方向依赖order 可以依赖 user,但 user 不能依赖 order
lib-domain 是叶子不依赖任何 feature,只放 POJO/接口
build-logic 内不放业务只放构建约定,写多了也算反模式
Version Catalog 集中管理业务模块永远只写 libs.xxx,从不写裸版本号
CI 与本地构建一致CI 上不要悄悄加额外参数,让两端行为一致

8. 常见踩坑

坑 1:把版本号写在 build-logic 而不是 Version Catalog

后果:未来升级要翻整个 build-logic 解法:哪怕约定插件需要 spring-boot-plugin,也通过插件管理 + libs注入

坑 2:根项目 build.gradle.kts 写了一堆 subprojects {}

后果:和约定插件功能重叠,难以维护 解法:所有共享逻辑都搬到 build-logic,根项目只做聚合

坑 3:CI 上没缓存 Gradle Home

后果:每次 CI 都要重新下载所有依赖(10 分钟变 30 分钟) 解法:用 gradle/actions/setup-gradle@v3 自动缓存

坑 4:./gradlew test 看不到日志

后果:测试通过没问题,失败时找不到原因 解法:在约定插件里设置 testLogging.events("passed", "skipped", "failed", "standardOut", "standardError")

坑 5:configuration-cache 报错就关掉

后果:白白错失 5x 提速 解法:先用 =warn 模式跑,看报告里哪些插件不兼容,逐个升级或替换


9. 进阶扩展(自由发挥)

如果觉得不够过瘾,可以继续:

  1. 加个 Code Quality 模块:集成 spotless/checkstyle/spotbugs,做 shop.quality-conventions
  2. 加个发布模块:用 maven-publish 把 lib-domain 发到 Maven 私服
  3. 加 Jib 插件./gradlew :app:jib 一键打 Docker 镜像,无需 Dockerfile
  4. 加 OpenAPI 生成:用 org.openapi.generator 自动生成 client SDK
  5. 多环境配置:通过 Gradle property 切换 dev / staging / prod 的 application.yml

10. 面试题与陷阱题 🎯

Q1:你为什么用 build-logic 而不是 buildSrc?

  • buildSrc 一改全工程要重新评估,CI 上反复失败
  • buildSrc 不能被自身或其他子项目 includeBuild
  • build-logic 是 Composite Build,更灵活,可以独立编译/测试
  • 大型项目上 build-logic 是 Gradle 官方推荐

Q2:约定插件 (Convention Plugin) 解决了什么?

:避免每个子模块都重复同一段 plugins/dependencies/tasks 配置。一处定义,处处复用,升级也只改一份。

Q3:Version Catalog 比传统的 ext { } 强在哪?

  • 类型安全:IDE 自动补全 + 编译时检查
  • 集中管理:单一源,多地引用
  • 支持 bundles:一组依赖一起引
  • 支持 plugins:插件版本也纳入管理

Q4:app 模块为什么没有业务代码?

:分层架构思想,app 只做"最终装配"。

  • 业务在 feature-* 里
  • 启动类在 app
  • 这样 feature-* 单独打包成 jar,可以被其他形态(H5 中台、SDK)复用

Q5:feature-order 依赖 feature-user 是好实践吗?

  • 单向依赖(user 不反过来依赖 order)→ ✅ 可接受
  • 但更好的做法是抽出 feature-user-api(接口)+ feature-user-impl(实现),order 只依赖 api,避免实现细节耦合

Q6:CI 上为什么用 --no-daemon

:CI 环境通常每个 job 是一次性容器,下次构建不会复用 daemon,反而占用内存导致 OOM。本地开发则相反,daemon 越用越快。

Q7:依赖大版本升级(比如 Spring Boot 3 → 3.2)你会怎么操作?

  1. 看 release note 是否有 breaking change
  2. 在 libs.versions.toml 里改一处版本
  3. ./gradlew build --refresh-dependencies 强制刷新
  4. 跑全套测试 + 启动一次
  5. 用 build scan 对比依赖树看变化

Q8:怎么排查"为什么这个 jar 包被引进来了"?

bash
./gradlew :app:dependencyInsight --dependency commons-collections \
  --configuration runtimeClasspath

一目了然显示是哪条依赖链拉进来的。

Q9:./gradlew build --scan 上传后,有哪些指标值得关注?

  • Total time vs Configuration time(看配置阶段是否过慢)
  • Cache hit rate(缓存命中率,理想 60%+)
  • Top tasks by duration(找瓶颈)
  • Avoidance saved(如果"省下"的时间还不够多说明缓存没用对)

Q10:你会怎么向新人解释整个 Gradle 多模块工程?

(推荐顺序):

  1. settings.gradle.kts 是"目录"
  2. 子模块的 build.gradle.kts 像"个人简历",只写跟自己有关的
  3. 所有团队约定都封装在 build-logic 里
  4. 版本号集中在 libs.versions.toml
  5. 性能 = daemon + parallel + cache(不是玄学)

📌 全书总结

恭喜你坚持读到这里!让我们回顾一下 14 天能力地图:

Day 1-2   ✅ 跑通 ./gradlew build      → 入门
Day 3-5   ✅ 看懂任意 build.gradle.kts → 基础
Day 6-8   ✅ 写自定义 Task / 调依赖     → 进阶
Day 9-11  ✅ 多模块 + 约定插件         → 工程化
Day 12-14 ✅ 性能优化 + 实战项目       → 高级

接下来你可以:

  • 把任何一个项目重写成本章演示的形态
  • 在公司项目里推动 Convention Plugin / Version Catalog 落地
  • 阅读 Gradle 官方文档 中你感兴趣的高级章节
  • Develocity 在 Spring/Android 等大项目里的玩法

构建工具是工程师的"基础设施"。把基础设施做扎实,团队才能跑得又快又稳。 祝你在 Gradle 之路上越走越顺!🎉

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

txt
/*
 * build-logic/build.gradle.kts
 * ------------------------------------------------------------
 * kotlin-dsl 让 src/main/kotlin/*.gradle.kts 自动编译为可被
 * 子模块通过 plugins { id("xxx") } 应用的 Convention Plugin。
 *
 * 真实生活类比:build-logic = 公司"标准 SOP 手册",
 * 写一份,所有部门都按这个流程走。
 */
plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
    mavenCentral()
}

dependencies {
    // 让我们的 spring-conventions 能 plugins { id("org.springframework.boot") }
    implementation("org.springframework.boot:spring-boot-gradle-plugin:3.2.0")
    implementation("io.spring.gradle:dependency-management-plugin:1.1.4")
}
txt
/*
 * build-logic 是独立的 Composite Build。
 * 它在主项目 settings.gradle.kts 里被 includeBuild 引入。
 */

dependencyResolutionManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

rootProject.name = "build-logic"
txt
/*
 * shop.java-conventions
 * ------------------------------------------------------------
 * 所有 Java 模块的"标配电脑":JDK 17、UTF-8 编码、严格的编译警告。
 */
plugins {
    java
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
    withSourcesJar()
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs.addAll(
        listOf(
            "-Xlint:unchecked",
            "-Xlint:deprecation",
            "-parameters"
        )
    )
}

tasks.withType<Jar>().configureEach {
    manifest {
        attributes(
            "Implementation-Title"   to project.name,
            "Implementation-Version" to (project.version.toString().ifBlank { "0.0.0-SNAPSHOT" }),
            "Built-By"               to System.getProperty("user.name"),
            "Built-Jdk"              to System.getProperty("java.version")
        )
    }
}
txt
/*
 * shop.spring-conventions
 * ------------------------------------------------------------
 * 给所有 Spring Boot 模块的标配:
 *   - test-conventions 已自动包含 java-conventions
 *   - Spring Boot Plugin + dependency-management
 *   - 默认引入 starter + starter-test
 */

plugins {
    id("shop.test-conventions")
    id("org.springframework.boot")
    id("io.spring.dependency-management")
}

dependencies {
    "implementation"("org.springframework.boot:spring-boot-starter")
    "testImplementation"("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

// 让所有 spring-boot 模块默认 disable bootJar 任务,
// 只有真正应该被打成可执行 jar 的 app 模块手动开启。
// 这样 feature/lib 模块只产出普通 jar 就行。
tasks.named("bootJar") {
    enabled = false
}
tasks.named<Jar>("jar") {
    enabled = true
    archiveClassifier.set("")  // 普通 jar 不带 classifier
}
txt
/*
 * shop.test-conventions
 * ------------------------------------------------------------
 * 在 java-conventions 之上,再统一 JUnit 5 + 详细测试日志。
 */
import org.gradle.api.tasks.testing.logging.TestExceptionFormat

plugins {
    id("shop.java-conventions")
}

dependencies {
    "testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0")
    "testRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}

tasks.named<Test>("test") {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
        exceptionFormat = TestExceptionFormat.FULL
        showStandardStreams = true
        showCauses = true
        showStackTraces = true
    }
    // 失败时不立即终止,跑完所有测试再汇总
    ignoreFailures = false
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
txt
/*
 * MiniShop · 根项目 build.gradle.kts
 * ------------------------------------------------------------
 * 根项目原则:不写业务,只放聚合 task。
 */

tasks.register("listAll") {
    group = "help"
    description = "列出所有子模块"
    doLast {
        println("\n📦 MiniShop 包含 ${subprojects.size} 个子模块:")
        subprojects.forEach { sp ->
            println("  • ${sp.path}")
        }
    }
}

tasks.register("info") {
    group = "help"
    description = "项目元信息"
    doLast {
        println("""
            
        ╔══════════════════════════════════════════╗
        ║          MiniShop 微店后端 v1.0.0         ║
        ╠══════════════════════════════════════════╣
        ║ Gradle:  ${gradle.gradleVersion}                            ║
        ║ JVM:     ${System.getProperty("java.version")}                              ║
        ║ Modules: ${subprojects.size}                                ║
        ╚══════════════════════════════════════════╝
        
        ✦ 常用命令:
            ./gradlew :app:bootRun
            ./gradlew build --scan
            ./gradlew :feature-user:test
        
        """.trimIndent())
    }
}
txt
# MiniShop · 性能优化全开
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

# JVM 参数
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8

# Daemon 30 分钟空闲回收
org.gradle.daemon.idletimeout=1800000

# Kotlin 增量
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true

# 文件监听
org.gradle.vfs.watch=true
toml
# MiniShop · 统一版本管理
# ============================================================
# 改版本号只动这一个文件,全工程同步更新。
# 业务模块永远只写 libs.xxx,从不写裸版本。
# ============================================================

[versions]
spring-boot   = "3.2.0"
spring-deps   = "1.1.4"
junit         = "5.10.0"
slf4j         = "2.0.9"
mapstruct     = "1.5.5.Final"
lombok        = "1.18.30"

[libraries]
slf4j-api                = { module = "org.slf4j:slf4j-api",                              version.ref = "slf4j" }
junit-jupiter            = { module = "org.junit.jupiter:junit-jupiter",                  version.ref = "junit" }
junit-platform-launcher  = { module = "org.junit.platform:junit-platform-launcher" }
mapstruct                = { module = "org.mapstruct:mapstruct",                          version.ref = "mapstruct" }
lombok                   = { module = "org.projectlombok:lombok",                         version.ref = "lombok" }

# Spring Boot starters(版本由 spring-boot-dependencies 管理)
spring-boot-starter      = { module = "org.springframework.boot:spring-boot-starter" }
spring-boot-starter-web  = { module = "org.springframework.boot:spring-boot-starter-web" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" }

[bundles]
testing = ["junit-jupiter"]

[plugins]
spring-boot = { id = "org.springframework.boot",        version.ref = "spring-boot" }
spring-deps = { id = "io.spring.dependency-management", version.ref = "spring-deps" }
txt
/*
 * lib-domain · 公共领域模型
 * ------------------------------------------------------------
 * 只有"实体类 / 接口 / DTO",不依赖任何 feature。
 * 不需要 Spring,所以只 apply test-conventions(已包含 java-conventions)。
 */
plugins {
    id("shop.test-conventions")
}

dependencies {
    // 这里也可以使用 libs.xxx(来自 settings 加载的 Version Catalog)
    // implementation(libs.slf4j.api)
    implementation("org.slf4j:slf4j-api:2.0.9")
}
java
package com.shop.domain;

/**
 * 用户领域模型。被 feature-user / feature-order 共用。
 */
public class User {
    private final long id;
    private final String name;
    private final String email;

    public User(long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
    }
}
txt
/*
 * MiniShop · settings.gradle.kts
 * ------------------------------------------------------------
 * 1) pluginManagement.includeBuild("build-logic") 把约定插件挂上
 * 2) dependencyResolutionManagement 强制集中管理仓库
 * 3) include 列出所有子模块
 */

pluginManagement {
    includeBuild("build-logic")
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        // 国内镜像示例(按需取消注释)
        // maven("https://maven.aliyun.com/repository/public")
    }
}

rootProject.name = "mini-shop"

include(
    "app",
    "feature-user",
    "feature-order",
    "lib-domain"
)

build-logic/build.gradle.kts ↗ · build-logic/settings.gradle.kts ↗ · build-logic/src/main/kotlin/shop.java-conventions.gradle.kts ↗ · build-logic/src/main/kotlin/shop.spring-conventions.gradle.kts ↗ · build-logic/src/main/kotlin/shop.test-conventions.gradle.kts ↗ · build.gradle.kts ↗ · gradle.properties ↗ · gradle/libs.versions.toml ↗ · lib-domain/build.gradle.kts ↗ · lib-domain/src/main/java/com/shop/domain/User.java ↗ · settings.gradle.kts ↗