合理组织 Gradle 项目结构以优化构建性能至关重要。多项目构建是 Gradle 中的标准做法。

项目概念

理解 Gradle 项目需要掌握四个关键概念

  1. 根项目:构建中的顶层项目,包含 settings.gradle(.kts) 文件,通常聚合所有子项目。

  2. 子项目:多项目构建的单个模块(组件),通过根项目中的 settings.gradle(.kts) 文件包含。

  3. 设置文件:一个 settings.gradle(.kts) 配置文件,用于定义多项目构建的结构,包括哪些子项目是其一部分以及它们是如何命名或定位的(可选)。

  4. 构建脚本build.gradle(.kts) 文件,定义项目如何构建(应用插件、声明依赖、配置任务等...),每个项目(子项目都可以有一个)都会执行。

让我们看一个例子

my-project/     (1)
├── settings.gradle.kts (2)
├── app/                    (3)
│   ├── build.gradle.kts        (4)
│   └── src/
├── core/                   (3)
│   ├── build.gradle.kts        (4)
│   └── src/
└── util/                   (3)
    ├── build.gradle.kts        (4)
    └── src/
my-project/     (1)
├── settings.gradle (2)
├── app/                (3)
│   ├── build.gradle        (4)
│   └── src/
├── core/               (3)
│   ├── build.gradle        (4)
│   └── src/
└── util/               (3)
    ├── build.gradle        (4)
    └── src/
1 根项目目录
2 设置文件
3 子项目
4 子项目构建文件

单项目构建

structuring builds 1

让我们看一个包含根项目和单个子项目的基本多项目构建示例。

根项目名为 my-project,位于您机器上的某个位置。从 Gradle 的角度来看,根是顶层目录 .

该项目包含一个名为 app 的子项目

.   (1)
├── settings.gradle (2)
└── app/                (3)
    ├── build.gradle       (4)
    └── src/            (5)
.   (1)
├── settings.gradle.kts (2)
└── app/                    (3)
    ├── build.gradle.kts        (4)
    └── src/                (5)
1 根项目
2 子项目
3 子项目构建文件
4 设置文件
5 源代码及更多

这是启动任何 Gradle 项目的推荐项目结构。Build Init 插件也会生成遵循此结构的骨架项目——一个根项目带一个子项目。

settings.gradle(.kts) 文件向 Gradle 描述项目结构

settings.gradle.kts
rootProject.name = "my-project"
include("app")
settings.gradle
rootProject.name = 'my-project'
include 'app'

在这种情况下,Gradle 将在 ./app 目录中查找 app 子项目的构建文件。

您可以通过运行 projects 命令来查看多项目构建的结构

$ ./gradlew -q projects

Projects:

------------------------------------------------------------
Root project 'my-project'
------------------------------------------------------------

Root project 'my-project'
\--- Project ':app'

To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :app:tasks

在此示例中,app 子项目是一个 Java 应用程序,它应用了Java 应用程序插件并配置了主类。

应用程序在控制台打印 Hello World

app/build.gradle.kts
plugins {
    id("application")
}

application {
    mainClass = "com.example.Hello"
}
app/build.gradle
plugins {
    id 'application'
}

application {
    mainClass = 'com.example.Hello'
}
app/src/main/java/com/example/Hello.java
package com.example;

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

您可以通过在项目根目录执行应用程序插件run 任务来运行应用程序

$ ./gradlew -q run
Hello, world!

多项目构建 (include())

structuring builds 2

在设置文件中,您可以使用 include 方法将另一个子项目添加到根项目

settings.gradle.kts
include("project1", "project2:child1", "project3:child1")
settings.gradle
include 'project1', 'project2:child1', 'project3:child1'

include 方法接受项目路径作为参数。项目路径假定等于相对物理文件系统路径。例如,路径 services:api 默认映射到文件夹 ./services/api(相对于项目根目录 .)。

有关如何使用项目路径的更多示例,请参阅 Settings.include(java.lang.String[]) 的 DSL 文档。

让我们向之前创建的项目添加另一个名为 lib 的子项目。

我们所需要做的就是在根设置文件中添加另一个 include 语句

settings.gradle.kts
rootProject.name = "my-project"
include("app")
include("lib")
settings.gradle
rootProject.name = 'my-project'
include 'app'
include 'lib'

Gradle 将在 ./lib/ 目录中查找新的 lib 子项目的构建文件

.       (1)
├── settings.gradle.kts (2)
├── app/                    (3)
│   ├── build.gradle.kts        (4)
│   └── src/
└── lib/                    (3)
    ├── build.gradle.kts        (4)
    └── src/
.       (1)
├── settings.gradle (2)
├── app/                (3)
│   ├── build.gradle        (4)
│   └── src/
└── lib/                (3)
    ├── build.gradle        (4)
    └── src/
1 根项目
2 设置文件
3 子项目
4 子项目构建文件

您可以在多项目构建中了解更多关于多项目构建的信息。

共享构建逻辑 (buildSrc)

structuring builds 3

随着项目规模和复杂性的增长,同一逻辑(如应用相同的插件、配置相同的任务或声明相同的依赖)在多个子项目中重复出现的情况很常见。

重复的构建逻辑难以维护且容易出错。Gradle 提供了一种内置方法来集中和重用此逻辑:一个名为 buildSrc 的特殊目录。

buildSrc 是位于 Gradle 项目根目录下的一个独立构建。您在此目录中放置的任何代码都会自动编译并添加到主构建的类路径中。

让我们看看我们的多项目构建

.
├── settings.gradle.kts
├── app/
│   ├── build.gradle.kts    (1)
│   └── src/
└── lib/
    ├── build.gradle.kts    (1)
    └── src/
.
├── settings.gradle
├── app/
│   ├── build.gradle    (1)
│   └── src/
└── lib/
    ├── build.gradle    (1)
    └── src/
1 子项目构建脚本,应用 java-library 和测试逻辑

我们现在将可重用的配置逻辑封装在 java-library-convention.gradle.kts 文件中。

因为文件名为 java-library-convention.gradle.kts,Gradle 自动将其注册为 ID 为 java-library-convention 的插件。它编译此插件并使其可用于项目中的所有其他构建脚本。

.
├── settings.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotin/
│       └── java-library-convention.gradle.kts  (1)
├── app/
│   ├── build.gradle.kts    (2)
│   └── src/
└── lib/
    ├── build.gradle.kts    (2)
    └── src/
.
├── settings.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/
│       └── java-library-convention.gradle  (1)
├── app/
│   ├── build.gradle    (2)
│   └── src/
└── lib/
    ├── build.gradle    (2)
    └── src/
1 应用 java-library 和测试逻辑
2 子项目构建脚本,应用 java-library 和测试逻辑

您可以在使用 BuildSrc 在子项目之间共享构建逻辑中了解更多关于多项目构建的信息。

复合构建 (includeBuild())

structuring builds 4

在 Gradle 中,复合构建(或包含构建)是将多个构建组合在一起的方式。

它们允许您像处理单个构建的一部分一样处理多个 Gradle 项目(构建),而无需将工件发布到仓库。

想象一下我们想将我们的多项目构建拆分为两个独立的构建

  • libs 中的共享库

  • 一个使用它的应用程序(我们之前的多项目构建)

我们想要

  • 将它们保留在独立的构建中(它们可能在独立的仓库中)

  • 但一起开发它们,而无需发布 lib

.
├── settings.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotin/
│       └── java-library-convention.gradle.kts
├── app/
│   ├── build.gradle.kts
│   └── src/
├── core/
│   ├── build.gradle.kts
│   └── src/
├── util/
│   ├── build.gradle.kts
│   └── src/
└── libs/           (1)
    └── lib/
        ├── settings.gradle.kts
        ├── build.gradle.kts
        └── src/
.
├── settings.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/
│       └── java-library-convention.gradle
├── app/
│   ├── build.gradle
│   └── src/
├── core/
│   ├── build.gradle
│   └── src/
├── util/
│   ├── build.gradle
│   └── src/
└── libs/           (1)
    └── lib/
        ├── settings.gradle
        ├── build.gradle
        └── src/
1 独立的、可重用库(包含构建)

这里,lib 是一个独立的 Gradle 构建,位于 libs/lib 中。它不是正常 include(…​) 多项目结构的一部分。

相反,我们将其视为一个包含构建,一个独立的构建,我们无需发布即可使用它。

在根 settings.gradle(.kts) 中,我们告诉 Gradle 使用 includeBuild()lib 作为复合构建的一部分包含进来

settings.gradle.kts
includeBuild("lib")
settings.gradle
includeBuild 'lib'

您可以在复合构建中了解更多关于多项目构建的信息。

结构建议

源代码和构建逻辑应该以清晰、一致且有意义的方式组织。本节概述了可使 Gradle 项目更具可读性和可维护性的建议。它还强调了常见陷阱以及如何避免它们,以确保您的构建保持健壮和可扩展。

使用单独的特定语言源文件

Gradle 的语言插件定义了发现和编译源代码的约定。例如,当应用Java 插件时,Gradle 会自动编译 src/main/java 中的源文件。

其他语言插件遵循类似的约定:源目录的最后一部分(例如,javagroovykotlin)指示其包含的源文件的语言。

一些编译器支持从同一目录交叉编译多种语言。例如,Groovy 编译器可以从 src/main/groovy 编译 Java 和 Groovy 源文件。

然而,Gradle 建议按语言将源文件分离到不同的目录中(例如,src/main/javasrc/main/kotlin)。这可以提高构建性能,并使构建更具可预测性——对于 Gradle 和阅读项目布局的人来说都是如此。

这是一个同时使用 Java 和 Kotlin 的项目的示例源布局

.
├── build.gradle.kts
└── src
    └── main
        ├── java
        │   └── HelloWorld.java
        └── kotlin
            └── Utils.kt
.
├── build.gradle
└── src
    └── main
        ├── java
        │   └── HelloWorld.java
        └── kotlin
            └── Utils.kt

每种测试类型使用单独的源文件

项目通常会定义并运行多种类型的测试——例如单元测试、集成测试、功能测试或冒烟测试。为了保持可维护性和组织性,Gradle 建议将每种测试类型的源代码存储在其专用的源目录中。

例如,您可以不将所有测试放在 src/test/java 下,而是使用

src/
├── test/                      // Unit tests
│   └── java/
├── integrationTest/           // Integration tests
│   └── java/
└── functionalTest/            // Functional tests
    └── java/

要查看这在实践中如何运作,请查看示例项目,该项目演示了如何在基于 Java 的项目中定义自定义的 integrationTest 源集和任务。

Gradle 允许您定义多个源集和测试任务,因此您可以在构建中完全隔离和控制每种类型的测试。

使用标准约定

所有 Gradle 核心插件都遵循约定优于配置的原则,这是一种著名的软件工程范式,它偏爱合理的默认值而非手动设置。您可以在此处阅读更多相关信息:约定优于配置

Gradle 插件提供了预定义的行为和目录结构,在大多数情况下“开箱即用”。以Java 插件为例

  • 默认源目录是 src/main/java

  • 编译后的类和打包工件(如 JAR)的默认输出位置是 build/

虽然 Gradle 允许您覆盖大多数默认值,但这样做可能会使您的构建更难理解和维护——特别是对于团队或新手来说。

除非您有充分的理由偏离(例如,适应遗留布局),否则请坚持标准约定。请参阅每个插件的参考文档,了解其默认约定和行为。

使用 Settings 文件

每次运行 Gradle 构建时,Gradle 都会尝试定位 settings.gradle (Groovy DSL) 或 settings.gradle.kts (Kotlin DSL) 文件。为此,它会从当前工作目录向上遍历目录层次结构到文件系统根目录。一旦找到设置文件,它就会停止搜索并将其用作构建的入口点。

多项目构建中,设置文件是必需的。它定义了哪些项目是构建的一部分,并使 Gradle 能够正确配置和评估整个项目层次结构。

您可能还需要一个设置文件来使用 pluginManagementdependencyResolutionManagement共享库或插件添加到构建类路径中。

以下示例显示了标准的 Gradle 项目布局

.
├── settings.gradle.kts
├── subproject-one
│   └── build.gradle.kts
└── subproject-two
    └── build.gradle.kts
.
├── settings.gradle
├── subproject-one
│   └── build.gradle
└── subproject-two
    └── build.gradle