主题
第 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 CI3. 一步步从空目录开始
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=warnStep 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.jar6. 检验是否成功的 4 个动作
| 验证步骤 | 预期 |
|---|---|
./gradlew projects | 显示 4 个子模块 |
./gradlew build | 编译 + 测试全过,二次构建明显更快 |
./gradlew :app:bootRun | Spring 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. 进阶扩展(自由发挥)
如果觉得不够过瘾,可以继续:
- 加个 Code Quality 模块:集成 spotless/checkstyle/spotbugs,做
shop.quality-conventions - 加个发布模块:用
maven-publish把 lib-domain 发到 Maven 私服 - 加 Jib 插件:
./gradlew :app:jib一键打 Docker 镜像,无需 Dockerfile - 加 OpenAPI 生成:用
org.openapi.generator自动生成 client SDK - 多环境配置:通过 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)你会怎么操作?
答:
- 看 release note 是否有 breaking change
- 在 libs.versions.toml 里改一处版本
./gradlew build --refresh-dependencies强制刷新- 跑全套测试 + 启动一次
- 用 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 多模块工程?
答(推荐顺序):
- settings.gradle.kts 是"目录"
- 子模块的 build.gradle.kts 像"个人简历",只写跟自己有关的
- 所有团队约定都封装在 build-logic 里
- 版本号集中在 libs.versions.toml
- 性能 = 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=truetoml
# 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 ↗