10、多模块项目:多项目结构、settings.gradle配置、项目间依赖、统一配置与allprojects/subprojects

说实话,我刚开始用 Gradle 那会儿,所有代码都塞在一个模块里。项目小的时候还行,一旦业务复杂起来,那个 build.gradle 文件能写到上千行。每次构建都慢得像蜗牛,改一行代码要等半分钟才能看到效果。后来我接手了一个真正的大型项目,才意识到——不分模块,你根本没法好好干活

多模块项目,说白了就是把一个大项目拆成多个小项目。每个小项目有自己的职责,可以独立构建、独立测试。你想想看,这样是不是清爽多了?

10.1 多项目结构长什么样?

一个典型的多模块项目,目录结构大概是这样的:

my-project/
├── settings.gradle
├── build.gradle          # 根项目的构建文件
├── app/
│   ├── build.gradle
│   └── src/
├── common/
│   ├── build.gradle
│   └── src/
├── api/
│   ├── build.gradle
│   └── src/
└── utils/
    ├── build.gradle
    └── src/

根目录下有一个 settings.gradle,它负责告诉 Gradle:这个项目有哪些子模块。每个子模块都有自己的 build.gradle,可以独立配置依赖、插件、任务。

我个人习惯把子模块按功能分层:

  • common:公共工具类、常量、异常定义
  • api:对外暴露的接口定义
  • utils:一些辅助功能,比如文件处理、加密
  • app:主应用模块,依赖上面所有模块

这样分层的好处是,依赖关系清晰,不会出现循环依赖这种让人头疼的问题。

10.2 settings.gradle 配置

这个文件是 Gradle 多模块项目的入口。没有它,Gradle 根本不知道有哪些子模块。

最简单的写法:

rootProject.name = 'my-project'

include 'app'
include 'common'
include 'api'
include 'utils'

嗯,这里要注意:include 后面的字符串是子模块的路径,相对于根目录。如果子模块在子目录里,比如 libs/security,那就写成:

include 'libs:security'

我曾经踩过一个坑:子模块名称和目录名称不一致。Gradle 默认会用目录名作为模块名,但如果你在 settings.gradle 里显式指定了名称,那就以你指定的为准。比如:

include ':my-app'
project(':my-app').projectDir = file('app')

这样,模块名叫 my-app,但实际代码在 app/ 目录下。说实话,这种写法容易让人迷惑,我建议尽量保持名称一致。

小技巧:如果你有几十个子模块,可以用 file('modules').eachDir 的方式批量 include,省得一个个写。

10.3 项目间依赖

子模块之间怎么互相依赖?很简单,在 build.gradle 里用 project() 语法:

// app/build.gradle
dependencies {
    implementation project(':common')
    implementation project(':api')
    implementation project(':utils')
}

这样,app 模块就能使用 commonapiutils 里的代码了。

为什么用 implementation 而不是 api

  • implementation:依赖只在当前模块内部可见,外部模块看不到。编译更快,推荐优先使用。
  • api:依赖会透传给外部模块。比如 common 依赖了某个库,如果 app 也需要这个库,那 common 就得用 api 声明。

我在项目中遇到过一个问题:A 模块依赖 B 模块,B 模块依赖 C 模块。如果 B 用 implementation 依赖 C,那 A 是看不到 C 的。这其实是好事,减少了不必要的耦合。但如果你不小心,可能会在 A 里直接引用 C 的类,然后编译报错——嗯,这时候你就知道该调整依赖关系了。

避坑指南:我曾经因为循环依赖,折腾了一整个下午。A 依赖 B,B 依赖 C,C 又依赖 A。Gradle 会直接报错,告诉你循环依赖了。解决办法是:重新设计模块结构,把公共部分抽出来放到一个独立的模块里。

10.4 统一配置与 allprojects/subprojects

多模块项目里,每个子模块可能都需要配置一些公共的东西,比如:

  • Java 版本
  • 仓库地址
  • 插件版本
  • 编码格式

如果每个子模块都写一遍,那跟单模块项目有什么区别?Gradle 提供了两个好东西:allprojectssubprojects

allprojects

作用于所有项目,包括根项目本身。通常用来配置仓库和公共插件:

// 根项目的 build.gradle
allprojects {
    repositories {
        mavenCentral()
        google()
    }
}

这样,所有子模块都不用再写 repositories 了。

subprojects

只作用于子模块,不包括根项目。适合配置 Java 版本、编码等:

subprojects {
    apply plugin: 'java'

    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17

    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
    }
}

你看,这样所有子模块都统一用了 Java 17 和 UTF-8 编码。如果某个子模块需要特殊配置,可以在它自己的 build.gradle 里覆盖。

核心区别
  • allprojects:根项目 + 所有子模块
  • subprojects:仅子模块
我个人习惯把仓库配置放 allprojects,把插件和编译配置放 subprojects

10.5 知识体系总览

下面这张图,帮你理清多模块项目的核心逻辑:

根项目 (Root) settings.gradle app 主应用模块 common 公共工具模块 api 接口定义模块 utils 辅助功能模块 统一配置 allprojects → 仓库、插件版本 subprojects → Java版本、编码、公共依赖

10.6 实战建议

最后,分享几个我在实际项目中的经验:

  1. 不要过度拆分。模块太多,管理成本会上升。我一般建议 3-6 个模块比较合适。
  2. 公共依赖统一管理。可以用 ext 块定义版本号,或者用 buildSrc 统一管理依赖版本。
  3. 注意构建顺序。Gradle 会自动根据依赖关系决定构建顺序,但如果你用了 compileOnlytestOnly,要小心循环依赖。
  4. 善用 subprojects。把公共配置抽出来,每个子模块只写自己特有的内容,代码量能减少 60% 以上。

多模块项目,说白了就是分而治之。每个模块做好自己的事,通过清晰的依赖关系协作。这样项目大了也不怕,改一个模块不会影响其他模块,构建速度也快得多。

避坑指南:我曾经在一个项目里,把所有子模块的依赖都写在了根项目的 build.gradle 里,结果子模块之间互相看不到依赖,编译报错一堆。后来才明白,每个子模块的依赖应该写在它自己的 build.gradle,根项目只负责统一配置。

嗯,多模块项目的内容就这些。记住核心三件事:settings.gradle 声明模块、project() 声明依赖、allprojects/subprojects 统一配置。掌握了这些,你就能轻松驾驭大型 Gradle 项目了。


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