21、Gradle Composite Build:复合构建

说实话,我第一次接触复合构建这个概念时,脑子里冒出的第一个念头是:「这不就是多项目构建吗?」

后来踩了坑才明白——两者完全是两码事。今天我们就来好好掰扯掰扯。

什么是复合构建?

复合构建(Composite Build)是 Gradle 提供的一种构建组织方式。它允许你把多个独立的 Gradle 项目组合在一起,让它们像一个整体一样协同工作。

我个人的理解是:复合构建 = 多个独立项目的「联邦」。每个项目都有自己的 build.gradle,自己管自己的依赖,但合在一起时又能互相引用。

核心特点:

  • 每个子项目都是独立的 Gradle 构建
  • 子项目之间通过 includeBuild 引入
  • 支持跨项目任务依赖
  • 子项目可以有自己的版本管理

includeBuild 的使用

用法其实很简单。你在根项目的 settings.gradle.kts 里写:

// settings.gradle.kts
rootProject.name = "my-app"

includeBuild("../my-library") {
    name = "my-library"
    dependencySubstitution {
        substitute(module("com.example:my-library"))
            .using(project(":"))
    }
}

includeBuild("../shared-utils") {
    name = "shared-utils"
}

嗯,这里要注意:dependencySubstitution 这块容易搞混。它的作用是告诉 Gradle——「当有人依赖 com.example:my-library 这个外部坐标时,直接用本地项目替换掉」。

我在项目中遇到过一个问题:团队里有人改了库的代码,但忘了发布新版本,结果主项目一直用的旧版。用复合构建后,本地改完立刻生效,再也不用等发布流程了。

复合构建 vs 多项目构建

很多人分不清这两者。我画了张图帮你理解:

多项目构建 根项目(settings.gradle 中 include) :app :library build.gradle 共享 build.gradle 共享 同一个 settings.gradle 管理 共享根项目的构建逻辑 子模块不能独立发布 复合构建 根项目(settings.gradle 中 includeBuild) :app(独立构建) 有自己的 settings.gradle :library(独立构建) 有自己的 settings.gradle 独立版本管理 独立版本管理 每个项目独立 settings.gradle 通过 includeBuild 组合 子项目可以独立发布

说白了,多项目构建是「一家人住在一个大房子里」,复合构建是「几个独立的房子组成一个小区」。前者共享水电煤气(构建逻辑),后者各管各的但互相串门。

对比维度 多项目构建 复合构建
settings.gradle 只有一个根 settings.gradle 每个项目都有自己的 settings.gradle
构建逻辑 共享根项目的 build.gradle 各自独立,互不干扰
版本管理 统一版本号 每个项目独立版本号
发布能力 子模块不能独立发布 每个项目可以独立发布
适用场景 单体应用内部模块拆分 跨项目、跨团队协作

避坑指南:我曾经在一个大项目里把复合构建和多项目构建混着用,结果 Gradle 的依赖解析直接炸了。记住一条原则:复合构建里的子项目不要再用 include 引入其他模块,否则会出现循环依赖。

模块化开发的最佳实践

复合构建在模块化开发中特别有用。我总结了几条经验:

1. 按团队边界拆分

每个团队维护一个独立的 Gradle 项目。比如:

  • user-module/ — 用户团队维护
  • payment-module/ — 支付团队维护
  • core-module/ — 基础架构团队维护

每个团队可以自由选择自己的构建策略、版本号、甚至 Gradle 版本。你想想看,这比把所有代码塞到一个仓库里灵活多了。

2. 用版本目录统一外部依赖

虽然每个项目独立,但外部依赖(比如 Retrofit、OkHttp)最好统一版本。我习惯在每个项目里都放一个 libs.versions.toml

// my-library/gradle/libs.versions.toml
[versions]
retrofit = "2.9.0"
okhttp = "4.11.0"

[libraries]
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
okhttp-core = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }

这样每个项目引用同一份版本号,不会出现「你的 Retrofit 是 2.9.0,我的是 2.8.0」这种尴尬情况。

3. 利用依赖替换进行本地调试

这是复合构建最爽的地方。假设你在开发 payment-module,同时也在改 core-module 的代码。你只需要在 payment-module 的 settings.gradle 里加上:

// payment-module/settings.gradle.kts
includeBuild("../core-module") {
    dependencySubstitution {
        substitute(module("com.company:core"))
            .using(project(":"))
    }
}

改完 core-module 的代码,直接运行 payment-module 的测试,改动立即可见。不用发布、不用等 CI、不用改版本号。

小技巧:我一般会在根项目里建一个 composite/ 目录,里面放一个总的 settings.gradle,把所有子项目用 includeBuild 引进来。这样开发时用这个总的 settings,发布时每个项目用自己的 settings。两不耽误。

4. 注意构建缓存

复合构建的构建缓存默认是隔离的。每个子项目有自己的缓存目录。如果你想让它们共享缓存,可以在 gradle.properties 里配置:

# gradle.properties
org.gradle.caching=true
org.gradle.caching.local.enabled=true

不过要注意——共享缓存可能导致缓存冲突。我建议在 CI 上开启,本地开发时保持默认。

5. 渐进式迁移策略

如果你想把一个多项目构建迁移到复合构建,别一次性全改。我的做法是:

  1. 先把最独立的模块抽出来,比如 networkdatabase 这种底层模块
  2. 用 includeBuild 引入,验证构建是否正常
  3. 逐步把业务模块也抽出来
  4. 最后把根项目变成一个「空壳」,只负责组合

这样做的好处是——万一出问题,回滚范围很小。我曾经一次性把 20 多个模块全抽了,结果 Gradle 配置调了整整两天……

总结

复合构建不是银弹,但它解决了一个很实际的问题:如何让多个独立项目协同工作,同时保持各自的独立性

如果你正在做一个大型项目,或者需要跨团队协作,我建议你试试复合构建。刚开始可能会觉得配置有点繁琐,但用顺手了就会发现——嗯,真香。


公众号:蓝海资料掘金营,微信deep3321