复合构建:复合构建概念、includeBuild、本地依赖替换

说到复合构建,我得先坦白一件事。我刚开始接触 Gradle 那会儿,一直觉得多项目构建就是全部了。直到有一次,我在一个大型微服务项目里,需要同时调试三个互相依赖的库……嗯,那场面简直是一场噩梦。每次改一个库的代码,都要先发布到本地 Maven 仓库,再让另一个项目引用。一来一回,半天就过去了。

后来我发现了复合构建,说白了就是「把多个独立的 Gradle 项目,临时组合到一起开发」。它不会改变你项目的发布方式,只是在本地开发时,让你能直接引用源码。这感觉就像……你本来要等快递,现在直接去仓库拿货了。

复合构建的核心概念

复合构建(Composite Build)允许你在一个构建中,包含另一个完整的 Gradle 构建。被包含的构建称为「参与构建」(Included Build)。

我个人习惯把复合构建理解为「开发时的临时合并」。它有几个关键特征:

  • 独立生命周期:每个参与构建都有自己的 settings.gradle.kts、build.gradle.kts,独立配置
  • 任务隔离:参与构建的任务不会自动暴露给主构建,除非你显式依赖
  • 依赖替换:这是最实用的功能——自动用源码替换外部依赖

核心思想:复合构建不是「合并项目」,而是「在开发时临时建立源码级依赖关系」。

includeBuild:如何引入外部构建

在 settings.gradle.kts 里,用 includeBuild 方法引入另一个 Gradle 项目。路径可以是相对路径或绝对路径。

// 主项目的 settings.gradle.kts
rootProject.name = "my-app"

// 引入本地的工具库构建
includeBuild("../my-tools")

// 也可以引入多个
includeBuild("../shared-lib")
includeBuild("../config-module")

这里要注意:被引入的目录必须是一个完整的 Gradle 项目,也就是要有 settings.gradle.kts 文件。我曾经犯过一个错,把一个只有 build.gradle.kts 的目录 include 进来,结果 Gradle 直接报错说找不到 settings 文件。

本地依赖替换:自动生效的魔法

复合构建最强大的地方在于依赖替换。假设你的主项目依赖了 com.example:my-tools:1.0.0,而你又通过 includeBuild("../my-tools") 引入了 my-tools 构建。

Gradle 会自动做一件事:它会检查 my-tools 构建输出的所有模块(通过其 settings.gradle.kts 定义的子项目),如果某个模块的 group:name 和主项目的依赖匹配,就自动用源码替换掉外部 jar。

举个例子:

// 主项目的 build.gradle.kts
dependencies {
    implementation("com.example:my-tools:1.0.0")
    implementation("com.example:shared-lib:2.3.0")
}

// my-tools 项目的 settings.gradle.kts
rootProject.name = "my-tools"
// 这个项目会输出 com.example:my-tools:1.0.0

// shared-lib 项目的 settings.gradle.kts
rootProject.name = "shared-lib"
// 这个项目会输出 com.example:shared-lib:2.3.0

当主项目通过 includeBuild("../my-tools")includeBuild("../shared-lib") 引入后,Gradle 会自动匹配 group:name,把这两个依赖替换成本地源码。你改完代码,直接运行主项目的任务,就能看到效果。

小技巧:依赖替换只匹配 group 和 name,不匹配 version。所以即使你依赖的是 1.0.0,而本地构建输出的是 2.0.0-SNAPSHOT,也能替换成功。这在实际开发中非常方便。

手动控制依赖替换

有时候自动替换不一定符合你的需求。比如你只想替换某个特定模块,或者想禁用自动替换。这时候可以用 dependencySubstitution 手动控制。

// 主项目的 settings.gradle.kts
includeBuild("../my-tools") {
    dependencySubstitution {
        // 只替换这个特定的依赖
        substitute(module("com.example:my-tools-core"))
            .using(project(":my-tools-core"))
        
        // 明确不替换某个依赖
        substitute(module("com.example:my-tools-legacy"))
            .using(module("com.example:my-tools-legacy:1.0.0"))
    }
}

我记得有一次,一个同事的本地构建里有个模块叫 :legacy,和线上发布的 jar 版本不兼容。自动替换导致主项目编译失败。我们用 dependencySubstitution 手动排除了那个模块,问题就解决了。

复合构建的任务执行

复合构建中,你可以从主构建直接执行参与构建的任务。比如:

// 执行 my-tools 构建中的 :clean 任务
./gradlew :my-tools:clean

// 执行 shared-lib 构建中的 :utils:test 任务
./gradlew :shared-lib:utils:test

任务路径的格式是 :<构建名称>:<任务路径>。构建名称默认就是被引入项目的根项目名称。如果你觉得名字太长,可以在 includeBuild 时指定别名:

includeBuild("../my-tools") {
    name = "tools"
}
// 现在执行任务用 :tools:clean 即可

注意:复合构建的任务执行有性能开销。Gradle 需要先配置所有参与构建,再执行任务。如果参与构建很多,建议用 --parallel 并行执行。

复合构建的典型场景

我总结了几种最适合用复合构建的场景:

  • 同时开发多个库:比如你维护一个工具库和一个业务库,业务库依赖工具库。用复合构建可以一边改工具库,一边在业务库中测试
  • 调试第三方依赖:把第三方库的源码 clone 下来,用 includeBuild 引入,直接调试它的行为
  • 跨团队协作:不同团队维护不同模块,用复合构建做集成测试,不用等发布
  • 迁移改造:把旧项目拆分成多个独立构建,用复合构建逐步过渡

避坑指南

我曾经踩过几个坑,分享给你:

  • 循环引用:两个构建互相 includeBuild,Gradle 会直接报错。复合构建不支持循环
  • 版本冲突:参与构建和主构建用了不同版本的同一个依赖,可能导致运行时异常。建议统一版本管理
  • 构建缓存失效:复合构建中,参与构建的缓存可能不会自动失效。如果改了代码但没生效,试试 --no-build-cache
  • IDE 支持:IntelliJ IDEA 对复合构建的支持还不错,但有时候需要手动刷新 Gradle 项目。如果遇到代码跳转不对,试试重新导入

知识体系图

复合构建知识体系 复合构建 核心概念 独立生命周期 任务隔离 依赖替换 includeBuild 引入外部构建 自动依赖替换 手动 substitution 典型场景 多库并行开发 调试第三方依赖 跨团队集成测试 ⚠️ 注意事项 避免循环引用 | 统一版本管理 | 注意构建缓存失效 | IDE 可能需要手动刷新 settings.gradle.kts → includeBuild → 自动依赖替换 → 本地开发调试

实际项目中的使用建议

如果你刚开始用复合构建,我建议从一个小场景入手。比如你有一个工具库和一个应用项目,先在应用项目的 settings.gradle.kts 里 includeBuild 工具库,看看依赖替换是否生效。

我个人习惯在项目根目录放一个 composite 文件夹,里面放所有参与构建的项目的 git submodule 或 clone。这样路径管理起来比较清晰。

my-project/
├── settings.gradle.kts      # 主构建配置
├── build.gradle.kts
├── composite/               # 复合构建目录
│   ├── my-tools/            # 工具库(git submodule)
│   ├── shared-lib/          # 共享库(git submodule)
│   └── config-module/       # 配置模块(git submodule)
└── app/
    └── build.gradle.kts

对应的 settings.gradle.kts:

rootProject.name = "my-project"

includeBuild("composite/my-tools")
includeBuild("composite/shared-lib")
includeBuild("composite/config-module")

这样做的好处是,你可以用 git 管理所有参与构建的版本,团队成员 clone 后执行 git submodule update --init 就能拉取所有依赖的源码。

总结一下:复合构建是 Gradle 提供的一个非常实用的本地开发工具。它不会改变你的发布流程,只是在开发时让你能直接引用源码。核心就是 includeBuild 加上自动的依赖替换。用好它,能大幅提升多项目并行开发的效率。

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