12、多项目构建:多项目结构、跨项目配置、子项目共享逻辑、allprojects与subprojects
说实话,单项目构建就像一个人单打独斗。一旦项目规模上来,比如一个电商系统拆成用户、订单、支付、库存几个模块,你就得用多项目构建了。Gradle 的多项目支持,是我认为它比 Maven 更灵活的地方之一。
我个人习惯把多项目构建比作「一个家族企业」—— 根项目是董事长,子项目是各个子公司。董事长定大方向(全局配置),子公司各自搞业务(独立构建),但有些资源(比如工具库、版本号)是共享的。
12.1 多项目结构:从物理布局到逻辑组织
Gradle 支持两种多项目布局:平面布局和分层布局。平面布局就是所有子项目都在根目录下平铺,适合模块数量少、耦合度低的情况。分层布局则是按功能或层级嵌套,适合大型项目。
先看一个典型的平面布局例子:
my-project/
├── settings.gradle.kts
├── build.gradle.kts
├── app/
│ └── build.gradle.kts
├── domain/
│ └── build.gradle.kts
└── infrastructure/
└── build.gradle.kts
关键在 settings.gradle.kts 里声明子项目:
rootProject.name = "my-project"
include("app", "domain", "infrastructure")
嗯,这里要注意:include 的参数是项目路径,不是目录名。默认情况下,Gradle 会认为 app 对应 ./app 目录。如果你想改目录名,可以用 projectDir 指定。
我在项目中遇到过一个问题:团队有人把子项目目录移到了别的位置,但忘了改 settings.gradle.kts,结果构建直接报错。所以我的建议是:子项目目录结构一旦确定,尽量别动,除非你同步更新 settings 文件。
12.2 跨项目配置:依赖与任务间的桥梁
多项目构建的核心价值之一,就是子项目之间可以互相依赖。比如 app 模块依赖 domain 模块:
// app/build.gradle.kts
dependencies {
implementation(project(":domain"))
}
这个 project(":domain") 的写法,冒号代表根项目,后面跟子项目路径。如果是嵌套子项目,比如 app:web,就写成 project(":app:web")。
除了依赖,你还可以跨项目执行任务。比如在根项目执行 gradle :app:build,只会构建 app 模块及其依赖。但如果你执行 gradle build,Gradle 会构建所有子项目。
gradle projects 命令可以查看当前项目的多项目结构树。我每次接手新项目,第一件事就是跑这个命令,快速摸清模块关系。
12.3 子项目共享逻辑:别重复造轮子
如果每个子项目的 build.gradle.kts 里都写一遍 Java 插件、仓库配置、依赖版本,那你就回到了 Maven 的「复制粘贴」时代。Gradle 提供了几种共享逻辑的方式:
- 根项目的 build.gradle.kts:对所有子项目生效,但只做通用配置。
- allprojects 和 subprojects 块:这是最常用的方式。
- 自定义插件:适合复杂共享逻辑,后面章节会讲。
- 约定插件(convention plugins):Gradle 官方推荐的方式,本质是预编译脚本插件。
我个人最常用的是「根项目配置 + subprojects 块」的组合。举个例子:
// 根项目的 build.gradle.kts
subprojects {
apply(plugin = "java")
apply(plugin = "jacoco")
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
"testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
这样每个子项目自动就有了 Java 插件、JUnit 依赖和测试配置。子项目只需要写自己特有的依赖和任务即可。
allprojects 里配置依赖。因为根项目通常不需要业务依赖,allprojects 会把配置也应用到根项目上,容易造成混乱。我一般只用 subprojects。
12.4 allprojects 与 subprojects:一字之差,天壤之别
这两个块的区别,说白了就是作用范围不同:
| 块 | 作用范围 | 典型用途 |
|---|---|---|
allprojects |
根项目 + 所有子项目 | 全局插件、仓库、通用配置 |
subprojects |
仅子项目 | 业务相关配置、依赖、任务 |
我曾经在一个项目里看到有人用 allprojects 配置了 application 插件,结果根项目也变成了一个可运行的应用,导致构建时多出很多不必要的任务。嗯,这就是典型的「好心办坏事」。
我的建议是:
- 仓库、公共插件版本 → 放
allprojects - Java 版本、测试框架、代码检查 → 放
subprojects - 业务依赖、特有任务 → 放子项目自己的
build.gradle.kts
12.5 实战:一个三模块项目的完整配置
来看一个真实案例。假设我们有三个模块:common(公共工具)、service(业务逻辑)、web(API 层)。
// settings.gradle.kts
rootProject.name = "demo-app"
include("common", "service", "web")
// 根项目 build.gradle.kts
allprojects {
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
group = "com.demo"
version = "0.1.0"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
dependencies {
"testImplementation"("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
// service/build.gradle.kts
dependencies {
implementation(project(":common"))
}
// web/build.gradle.kts
dependencies {
implementation(project(":service"))
implementation("org.springframework.boot:spring-boot-starter-web:3.1.0")
}
这个配置的好处是:common 模块只依赖 JDK,service 依赖 common,web 依赖 service 并引入 Spring Boot。每个模块职责清晰,构建时 Gradle 会自动处理依赖顺序。
12.6 知识体系图:多项目构建全景
这张图把多项目构建的四个核心维度串起来了。你从左上角开始看:先确定项目结构,再配置跨项目依赖,然后通过共享逻辑减少重复,最后搞清楚 allprojects 和 subprojects 的边界。嗯,基本上就掌握了多项目构建的精髓。