随着项目的发展,通常会将其拆分为更小、更集中的模块,这些模块一起构建、测试和发布。Gradle 通过多项目构建支持这一点,允许您在单个构建下组织相关的代码库,同时保持每个模块的逻辑隔离。

多项目布局

一个多项目构建由一个根项目和一个或多个子项目组成,所有这些都在单个 settings.gradle(.kts) 文件中定义。这种结构支持模块化、并行执行和代码重用。

structuring builds 6

典型的多项目结构如下所示

my-project/
├── settings.gradle.kts     (1)
├── build.gradle.kts            (2)
├── app/                    (3)
│   └── build.gradle.kts        (4)
├── core/                   (3)
│   └── build.gradle.kts        (5)
└── util/                   (3)
    └── build.gradle.kts        (6)
my-project/
├── settings.gradle         (1)
├── build.gradle        (2)
├── app/                    (3)
│   └── build.gradle    (4)
├── core/                   (3)
│   └── build.gradle    (5)
└── util/                   (3)
    └── build.gradle    (6)
1 声明子项目
2 根项目构建逻辑(可选)
3 子项目
4 应用模块
5 共享核心逻辑
6 工具代码

每个子项目都可以定义自己的构建逻辑、依赖和插件。

settings.gradle(.kts) 中,您可以使用 include() 按名称包含子项目。include() 方法接受 项目路径 作为参数

rootProject.name = "my-project"
include("app", "core", "util")
rootProject.name = 'my-project'
include('app', 'core', 'util')

默认情况下,项目路径对应于项目目录的相对物理位置。例如,路径 services:api 映射到相对于根项目的目录 ./services/api

您可以在 Settings.include(String…​) 的 DSL 参考中找到更多示例和详细用法。

项目描述符

为了进一步向 Gradle 描述项目架构,设置文件提供了项目描述符

您可以随时在设置文件中修改这些描述符。

要访问描述符,您可以

settings.gradle.kts
include("project-a")
println(rootProject.name)
println(project(":project-a").name)
settings.gradle
include('project-a')
println rootProject.name
println project(':project-a').name

使用此描述符,您可以更改项目的名称、项目目录和构建文件

settings.gradle.kts
rootProject.name = "main"
include("project-a")
project(":project-a").projectDir = file("custom/my-project-a")
project(":project-a").buildFileName = "project-a.gradle.kts"
settings.gradle
rootProject.name = 'main'
include('project-a')
project(':project-a').projectDir = file('custom/my-project-a')
project(':project-a').buildFileName = 'project-a.gradle'

有关更多信息,请查阅 API 文档中的 ProjectDescriptor 类。

仅包含现有项目

当您在 settings.gradle(.kts) 文件中包含一个子项目时,Gradle 会期望该项目关联的目录存在且可写。

Gradle 9.0 开始,这将严格执行:如果项目目录缺失或只读,构建将失败。 这取代了 Gradle 以前静默允许缺失项目目录的行为。

以下是您如何在配置时创建缺失目录的方法

settings.gradle.kts
include("project-without-directory")
project(":project-without-directory").projectDir.mkdirs()
settings.gradle
include 'project-without-directory'
project(":project-without-directory").projectDir.mkdirs()

修改子项目路径

让我们来看一个具有以下结构的假设项目

.
├── settings.gradle.kts
├── app/
│   ├── build.gradle.kts
│   └── src/
└── subs/               (1)
    ├── build.gradle.kts
    └── web             (1)
        └── my-web-module   (2)
.
├── settings.gradle
├── app/
│   ├── build.gradle
│   └── src/
└── subs/               (1)
    ├── build.gradle
    └── web             (1)
        └── my-web-module   (2)
1 Gradle 可能会将其视为一个子项目
2 实际预期的子项目

如果您的 settings.gradle(.kts) 看起来像这样

include(':subs:web:my-web-module')

Gradle 会看到一个逻辑项目名为 :subs:web:my-web-module 的子项目,以及另外两个可能无意的逻辑项目名称为

  1. :subs

  2. :subs:web

这可能导致虚拟构建目录,尤其是在使用 allprojects{}subproject{} 时。

为避免这种情况,您可以使用

include(':my-web-module')
project(':my-web-module').projectDir = "subs/web/my-web-module"

这样您最终只会得到一个名为 :my-web-module 的子项目。

因此,尽管物理项目布局相同,但逻辑结果却不同。

命名建议

随着项目的发展,命名和一致性变得越来越重要。

为了保持构建的可维护性,我们建议以下几点

  1. 保留子项目的默认项目名称:可以在设置文件中配置自定义项目名称。但是,这会给开发人员带来不必要的额外工作,以跟踪哪些项目属于哪些文件夹。

  2. 所有项目名称都使用小写连字符:所有字母都小写,单词之间用破折号 (-) 分隔。

  3. 在设置文件中定义根项目名称rootProject.name 有效地为构建分配了一个名称,用于构建扫描等报告中。如果未设置根项目名称,则名称将是容器目录名称,这可能不稳定(即,您可以在任何目录中检出项目)。如果未设置根项目名称并将其检出到文件系统的根目录(例如 /C:\),则名称将随机生成。

声明子项目之间的依赖

如果一个子项目依赖于另一个子项目怎么办?如果一个子项目依赖于另一个子项目产生的工件怎么办?

structuring builds 7

这是多项目构建中的常见用例。Gradle 通过 项目依赖 支持此场景。

依赖于另一个项目

考虑一个具有以下布局的多项目构建

.
├── api
│   ├── src
│   │   └──...
│   └── build.gradle.kts
├── services
│   └── person-service
│       ├── src
│       │   └──...
│       └── build.gradle.kts
├── shared
│   ├── src
│   │   └──...
│   └── build.gradle.kts
└── settings.gradle.kts
.
├── api
│   ├── src
│   │   └──...
│   └── build.gradle
├── services
│   └── person-service
│       ├── src
│       │   └──...
│       └── build.gradle
├── shared
│   ├── src
│   │   └──...
│   └── build.gradle
└── settings.gradle

在此示例中

  1. person-service 依赖于 apishared

  2. api 依赖于 shared

您使用项目路径声明这些关系,项目路径使用冒号 (:) 表示嵌套。例如

  • :shared 指的是 shared 子项目

  • services:person-service 指的是一个嵌套子项目

settings.gradle.kts
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
shared/build.gradle.kts
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
}
api/build.gradle.kts
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
    implementation(project(":shared"))
}
services/person-service/build.gradle.kts
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("junit:junit:4.13")
    implementation(project(":shared"))
    implementation(project(":api"))
}
settings.gradle
rootProject.name = 'basic-dependencies'
include 'api', 'shared', 'services:person-service'
shared/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
}
api/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
    implementation project(':shared')
}
services/person-service/build.gradle
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
    implementation project(':shared')
    implementation project(':api')
}

有关项目路径的更多详细信息,请查阅 Settings.include(String…​) 的 DSL 文档。

项目依赖会影响构建顺序和类路径

  • 所需的项目将首先构建。

  • 其编译后的类和传递依赖将添加到消费项目的类路径中。

例如,运行 ./gradlew :api:compileJava 将首先构建 shared,然后构建 api

依赖于另一个项目产生的工件

有时,您只需要另一个项目中特定任务的输出——而不是整个项目本身。

虽然您可以在项目之间创建任务到任务的依赖,但 Gradle 不鼓励这样做,因为它会在任务之间创建紧密耦合。

相反,使用输出工件来暴露任务的输出并将其建模为依赖。Gradle 的变体感知依赖管理允许一个项目以结构化、按需的方式消费另一个项目的工件。