多模块项目结构搭建

4.1 原理:多模块本质与模块发现机制

Gradle 的多模块(Multi-Module)结构允许将一个大项目拆分为多个独立的子项目(Subproject),每个子项目拥有独立的构建逻辑、依赖和输出。所有子项目通过根项目的 settings.gradle 文件进行注册,Gradle 在初始化阶段读取该文件,构建出项目树(Project Tree),然后按依赖顺序依次执行各模块的任务。

核心概念:模块注册告诉 Gradle“哪些目录是子项目”,而子项目的共享配置通过根项目的 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()
    }
}
最佳实践:将插件、Java 版本、通用测试框架放在 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 采用高对比配色,节点清晰标注注册、初始化、依赖等关键步骤。

多模块项目构建流程 settings.gradle.kts include(":common", ":app", ":api", ":libs:utils") Gradle 初始化阶段构建项目树 :root → :common :app :api :libs:utils 子项目共享配置 (subprojects { }) 插件、仓库、Java版本、公共依赖、测试框架 按依赖顺序执行各模块任务 模块间依赖会导致 构建顺序自动排序 无需手动处理

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