多模块项目结构搭建
4.1 原理:多模块本质与模块发现机制
Gradle 的多模块(Multi-Module)结构允许将一个大项目拆分为多个独立的子项目(Subproject),每个子项目拥有独立的构建逻辑、依赖和输出。所有子项目通过根项目的 settings.gradle 文件进行注册,Gradle 在初始化阶段读取该文件,构建出项目树(Project Tree),然后按依赖顺序依次执行各模块的任务。
build.gradle(或 build.gradle.kts)中的 subprojects { } 或 allprojects { } 块实现。
4.2 目录组织规范
推荐的多模块项目目录结构如下(以 Java / Kotlin 为例):
my-project/
├── settings.gradle.kts # 注册所有子模块
├── build.gradle.kts # 根模块配置(可空,用于共享配置)
├── common/ # 公共库子模块
│ ├── build.gradle.kts
│ └── src/
├── app/ # 应用主模块
│ ├── build.gradle.kts
│ └── src/
├── api/ # API 接口模块
│ ├── build.gradle.kts
│ └── src/
└── test-utils/ # 测试工具模块
├── build.gradle.kts
└── src/
每个子模块必须包含自己的 build.gradle.kts(或 .gradle),且模块目录名称通常与模块标识符一致。模块标识符在 settings.gradle.kts 中通过 include 指定。
4.3 settings.gradle 配置详解
settings.gradle.kts 是 Gradle 构建的入口文件,必须位于根项目目录下。其核心声明为 include,用于将子目录注册为 Gradle 子项目。
4.3.1 基本 include 语法
rootProject.name = "my-project"
include("common")
include("app")
include("api")
include("test-utils")
| 声明 | 含义 |
|---|---|
rootProject.name | 设置根项目名称,影响生成的任务名称和构建缓存路径 |
include(":module-a") | 注册子模块,使用冒号作为路径分隔符 |
include(":libs:utils") | 注册嵌套子模块(目录深度 > 1) |
4.3.2 嵌套子模块与多级目录
当子模块位于子目录下时,需使用冒号分隔路径层级:
include(":libs:utils")
include(":libs:data")
include(":services:auth")
对应的目录结构:
my-project/
├── libs/
│ ├── utils/ # 对应 :libs:utils
│ └── data/ # 对应 :libs:data
├── services/
│ └── auth/ # 对应 :services:auth
└── settings.gradle.kts
include 只是注册路径,Gradle 不会自动创建目录。必须手动创建对应的目录和 build.gradle.kts 文件,否则初始化会报错:Project with path ':libs:utils' could not be found。
4.4 子项目共享配置(subprojects 与 allprojects)
在根项目的 build.gradle.kts 中,可以使用 subprojects { } 和 allprojects { } 块为所有子项目或所有项目(包括根项目)添加通用配置,避免在每个子项目中重复书写相同的依赖、插件或仓库。
| 作用域 | 包含的模块 | 使用场景 |
|---|---|---|
allprojects | 根项目 + 所有子项目 | 全局仓库、全局插件、全局任务 |
subprojects | 仅子项目(不包括根项目) | 子项目通用依赖、通用编译配置、通用测试配置 |
4.4.1 配置示例
// 根项目 build.gradle.kts
// 全局仓库配置(所有项目均使用)
allprojects {
repositories {
mavenCentral()
google()
}
}
// 子项目通用配置
subprojects {
apply(plugin = "java")
apply(plugin = "kotlin")
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
subprojects { } 中共享,而仓库(如 mavenCentral)放在 allprojects { } 中,因为根项目也可能需要解析依赖(如 buildSrc)。
4.5 模块依赖声明
子项目之间可以通过 implementation(project(":模块路径")) 声明依赖。例如 app 模块依赖 common 和 api:
// app/build.gradle.kts
dependencies {
implementation(project(":common"))
implementation(project(":api"))
implementation("com.google.guava:guava:32.0.1-android")
}
当使用 project() 指定模块路径时,路径必须与 settings.gradle.kts 中 include 声明的路径一致。
4.6 多模块项目结构内部流程图
下图展示了从 settings.gradle.kts 到项目树构建,再到依赖解析的完整流程。SVG 采用高对比配色,节点清晰标注注册、初始化、依赖等关键步骤。
4.7 调试与常见错误
4.7.1 常见错误列表
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
Project with path ':xxx' could not be found in root project 'yyy'. | include 中声明的子项目路径不存在或拼写错误 | 检查 settings.gradle.kts 中的路径字符串与实际目录是否一致,确认子目录存在并有 build.gradle.kts |
Cannot change dependencies of configuration after it has been resolved. | 在配置阶段之后动态添加 project() 依赖 | 确保依赖声明只在 dependencies { } 块内,不要在 doFirst 或 afterEvaluate 中修改 |
Circular dependency between projects. | 两个子项目相互依赖(A→B 且 B→A) | 检查依赖图,提取公共接口到独立模块,或使用 api 代替 implementation 导致传递依赖循环 |
Could not determine the dependencies of task ':app:compileJava'. | 依赖的模块不存在或未注册 | 检查 include 是否包含了所有被依赖的模块 |
4.7.2 调试命令
使用 gradle projects 可以查看当前项目树的完整层级:
./gradlew projects
输出示例:
------------------------------------------------------------
Root project 'my-project'
------------------------------------------------------------
Root project 'my-project'
+--- Project ':app'
+--- Project ':common'
+--- Project ':api'
\--- Project ':test-utils'
使用 gradle dependencies 可以查看特定模块的依赖树:
./gradlew :app:dependencies
输出示例(部分):
------------------------------------------------------------
Project :app
------------------------------------------------------------
implementation
+--- project :common
| +--- com.google.guava:guava:32.0.1-android
| \--- org.slf4j:slf4j-api:2.0.7
\--- project :api
\--- com.fasterxml.jackson.core:jackson-databind:2.15.2
4.8 优化与性能建议
- 合理划分模块粒度:避免创建过多细粒度模块(如每个工具类一个模块),会增加配置解析和任务调度的开销。推荐按业务领域或层(common、api、service、app)划分。
- 使用
configuration cache:Gradle 7+ 支持配置缓存,在多模块项目中能大幅减少初始化时间。启用方式:gradle.properties中添加org.gradle.configuration-cache=true。 - 避免共享可变配置:在
subprojects {}中修改version或group时,应确保所有子项目使用统一的版本控制,否则可能出现依赖版本冲突。 - 使用
buildSrc管理版本目录:对于大型多模块项目,将依赖版本定义在buildSrc/src/main/kotlin/Dependencies.kt或使用libs.version.toml版本目录,避免硬编码。
4.9 部署与持续集成适配
在多模块项目中,CI 脚本通常需要按模块并行构建或只构建变动的模块。Gradle 的 --continue 参数允许某个模块编译失败后继续执行其他模块,结合 parallel 加速:
./gradlew build --parallel --continue
若只想构建特定模块及其依赖,可使用路径限定:
./gradlew :app:build :common:build
对于 Jenkins/GitLab CI,建议将各模块的测试结果分开收集:
./gradlew :app:test :common:test --no-daemon --xml-report-path=build/reports/xml