多项目构建中的子项目通常共享通用依赖项。

Gradle 允许您将共享构建逻辑集中到一个特殊目录中,而不是在多个构建脚本中重复相同的依赖项声明。这样,您可以在一个地方声明依赖项版本,并让它自动应用于所有子项目。

structuring builds 8

使用 buildSrc

buildSrc 是 Gradle 构建中的一个特殊目录,它允许您在构建中的所有项目之间组织和共享构建逻辑,例如自定义插件、任务、配置和实用函数。

structuring builds 9

让我们看一个具有以下结构的示例

典型的多项目构建具有以下布局

.
├── api
│   ├── src/
│   └── build.gradle.kts    (1)
├── services
│   ├── src/
│   └── build.gradle.kts    (1)
├── shared
│   ├── src/
│   └── build.gradle.kts    (1)
└── settings.gradle.kts
1 包含构建逻辑的构建脚本,包括与子项目共享的部分。
.
├── api
│   ├── src/
│   └── build.gradle    (1)
├── services
│   ├── src/
│   └── build.gradle    (1)
├── shared
│   ├── src/
│   └── build.gradle    (1)
└── settings.gradle
1 包含构建逻辑的构建脚本,包括与子项目共享的部分。

apiservicesshared 的构建文件有许多共同之处

api/build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9")
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}
services/build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9")
    implementation("com.google.guava:guava:32.1.2-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}
shared/build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9")
    implementation("com.google.guava:guava:32.1.2-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}
api/build.gradle
plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named('test', Test) {
    useJUnitPlatform()
}
services/build.gradle
plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.1.2-jre'
    implementation 'org.slf4j:slf4j-api:2.0.9'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named('test', Test) {
    useJUnitPlatform()
}
shared/build.gradle
plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.1.2-jre'
    implementation 'org.slf4j:slf4j-api:2.0.9'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named('test', Test) {
    useJUnitPlatform()
}

为了避免在 apiservicesshared 中重复构建逻辑,我们可以将共享部分移到 buildSrc 中。这允许我们定义依赖项和其他配置一次,并将其统一应用于所有子项目。

例如,如果您需要更新 org.slf4j:slf4j-api:1.7.32 的版本,您只需更改一次——在 buildSrc 中的构建逻辑中——而不是更新每个单独的构建脚本。

让我们扩展布局以包含 buildSrc 目录

.
├── buildSrc
│   ├── src
│   │   └──main
│   │      └──kotlin
│   │         └──java-common-conventions.gradle.kts  (1)
│   └── build.gradle.kts
├── api
│   ├── src/
│   └── build.gradle.kts            (2)
├── services
│   ├── src/
│   └── build.gradle.kts            (2)
├── shared
│   ├── src/
│   └── build.gradle.kts            (2)
└── settings.gradle.kts
1 共享构建脚本。
2 应用共享构建脚本。
.
├── buildSrc
│   ├── src
│   │   └──main
│   │      └──groovy
│   │         └──java-common-conventions.gradle  (1)
│   └── build.gradle
├── api
│   ├── src/
│   └── build.gradle            (2)
├── services
│   ├── src/
│   └── build.gradle            (2)
├── shared
│   ├── src/
│   └── build.gradle            (2)
└── settings.gradle
1 共享构建脚本。
2 应用共享构建脚本。

当 Gradle 构建的根目录中存在 buildSrc 目录时,Gradle 将其视为 复合构建。检测到 buildSrc 目录后,Gradle 会

  • buildSrc 视为一个独立的 Gradle 项目,拥有自己的 build.gradle(.kts) 文件和自己的 src/ 文件夹。

  • 在评估主构建中的任何其他构建脚本之前,编译 buildSrc 中的所有类和脚本(通常在 src/main/kotlinsrc/main/groovy 下)。

  • 使编译后的类和脚本在根项目和子项目中的所有其他项目构建脚本的类路径上可用。

因此,我们新的 buildSrc 具有以下构建文件

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}
buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}

buildSrc 中,构建脚本 java-common-conventions.gradle(.kts)src/main/kotlinsrc/main/groovy 中创建。它包含我们子项目共有的依赖项和其他构建信息

buildSrc/src/main/kotlin/java-common-conventions.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9")
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}
buildSrc/src/main/groovy/java-common-conventions.gradle
plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named('test', Test) {
    useJUnitPlatform()
}

如果您创建像 java-common-conventions.gradle(.kts) 这样的脚本文件,您可以将其视为插件并在子项目中应用。插件的 ID 是构建文件的名称,不带 gradle(.kts) 扩展名。这种插件称为约定插件

共享逻辑从 apiservicesshared 构建文件中移除。并且 shared 插件在文件中应用

api/build.gradle.kts
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
}
services/build.gradle.kts
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre")
}
shared/build.gradle.kts
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre")
}
api/build.gradle
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
}
services/build.gradle
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre")
}
shared/build.gradle
plugins {
    id("java-common-conventions")
}

dependencies {
    implementation("com.google.guava:guava:32.1.2-jre")
}

从现在开始,如果您想更改 slf4j-api 的版本之类的东西,您只需要在 buildSrc 中更新它,更改将自动应用于所有子项目。

关于 buildSrc 目录

buildSrc 是一个 Gradle 识别的特殊目录,它提供了一种方便的方式来组织和重用整个构建中的自定义构建逻辑。Gradle 自动将其视为一个包含构建,具有多个优点

  1. 可重用构建逻辑:您可以将通用构建逻辑、任务和插件集中在 buildSrc 中。这促进了子项目之间的一致性和可维护性,因为 buildSrc 中的更改会自动反映在其逻辑使用的地方。

  2. 自动编译和类路径包含:Gradle 自动编译 buildSrc 中的代码,并将其包含在所有构建脚本的类路径中。这允许直接使用 buildSrc 中定义的类、插件和实用程序,而无需额外设置。

  3. 更简洁的构建脚本:将逻辑移到 buildSrc 中可以使项目的主要构建脚本保持集中和整洁,从而提高可读性和可维护性。

  4. 易于测试:由于 buildSrc 被视为一个独立的构建,您可以像任何其他项目代码一样编写和运行自定义任务和插件的单元测试。

  5. 方便的插件开发:如果您正在编写用于内部使用的自定义 Gradle 插件,buildSrc 提供了一种定义和使用它们而无需发布到外部仓库的简便方法。

buildSrc 遵循与常规 Java、Groovy 或 Kotlin 项目相同的源布局约定,并可以直接访问 Gradle API。依赖项可以在其自己的 build.gradlebuild.gradle.kts 文件中声明。

在多项目构建中,只允许有一个 buildSrc 目录,并且它必须位于根项目目录中。

buildSrc 中代码的更改将使配置阶段无效,并需要重新执行所有任务,这可能会减慢构建速度。

使用名为 build-logic 的复合构建

除了 buildSrc,另一种在子项目之间共享构建逻辑的强大方法是使用专用的复合构建,通常命名为 build-logic

使用 build-logic 作为复合构建的典型多项目构建如下所示

.
├── build-logic/                   (1)
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src/main/kotlin
│       └── java-common-conventions.gradle.kts
├── api/
│   └── build.gradle.kts           (2)
├── services/
│   └── build.gradle.kts           (2)
├── shared/
│   └── build.gradle.kts           (2)
└── settings.gradle.kts            (3)
1 定义约定插件的独立构建逻辑项目。
2 应用复合构建中的共享插件。
3 将 build-logic 作为包含构建。
.
├── build-logic/                (1)
│   ├── build.gradle
│   ├── settings.gradle
│   └── src/main/groovy
│       └── java-common-conventions.gradle
├── api/
│   └── build.gradle            (2)
├── services/
│   └── build.gradle            (2)
├── shared/
│   └── build.gradle            (2)
└── settings.gradle             (3)
1 定义约定插件的独立构建逻辑项目。
2 应用复合构建中的共享插件。
3 build-logic 作为包含构建。

您可以在复合构建中了解更多信息。

避免使用 subprojectsallprojects 进行跨项目配置

在子项目之间共享构建逻辑的一种不当方式是通过 subprojects {}allprojects {} DSL 构造进行跨项目配置

通过跨项目配置,构建逻辑可以注入到子项目中,这在查看其构建脚本时并不明显。

从长远来看,跨项目配置通常会变得越来越复杂,并成为一种负担。跨项目配置还可能在项目之间引入配置时耦合,这可能会阻止按需配置等优化正常工作。

约定插件与跨项目配置

跨项目配置的两种最常见用途可以使用约定插件更好地建模

  1. 将插件或其他配置应用于特定类型的子项目。
    通常,跨项目配置逻辑是 如果子项目是 X 类型,则配置 Y。这等同于将 X-conventions 插件直接应用于子项目。

  2. 从特定类型的子项目中提取信息。
    此用例可以使用变体感知配置建模。