Gradle 拥有丰富的 API,提供多种创建构建逻辑的方法。相关的灵活性很容易导致不必要的复杂构建,而自定义代码通常直接添加到构建脚本中。在本章中,我们介绍了几种最佳实践,将帮助您开发易于使用的、富有表现力和可维护的构建。

避免在脚本中使用命令式逻辑

Gradle 运行时不强制构建逻辑的特定样式。正是出于这个原因,很容易最终得到一个将声明性 DSL 元素与命令式过程代码混合在一起的构建脚本。我们来谈谈一些具体的例子。

每个构建脚本的最终目标应该是仅包含声明性语言元素,这使得代码更易于理解和维护。命令式逻辑应该存在于二进制插件中,然后应用于构建脚本。作为副产品,如果您将工件发布到二进制存储库,您将自动使您的团队能够在其他项目中重用插件逻辑。

以下示例构建显示了直接在构建脚本中使用条件逻辑的反面示例。虽然此代码片段很小,但很容易想象一个使用大量过程语句的完整构建脚本及其对可读性和可维护性的影响。通过将代码移动到一个类中,也可以单独进行测试。

build.gradle.kts
if (project.findProperty("releaseEngineer") != null) {
    tasks.register("release") {
        doLast {
            logger.quiet("Releasing to production...")

            // release the artifact to production
        }
    }
}
build.gradle
if (project.findProperty('releaseEngineer') != null) {
    tasks.register('release') {
        doLast {
            logger.quiet 'Releasing to production...'

            // release the artifact to production
        }
    }
}

让我们将构建脚本与作为二进制插件实现的相同逻辑进行比较。该代码乍一看可能看起来更复杂,但显然看起来更像典型的应用程序代码。这个特定的插件类位于buildSrc目录中,这使得构建脚本可以自动使用它。

ReleasePlugin.java
package com.enterprise;

import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskProvider;

public class ReleasePlugin implements Plugin<Project> {
    private static final String RELEASE_ENG_ROLE_PROP = "releaseEngineer";
    private static final String RELEASE_TASK_NAME = "release";

    @Override
    public void apply(Project project) {
        if (project.findProperty(RELEASE_ENG_ROLE_PROP) != null) {
            Task task = project.getTasks().create(RELEASE_TASK_NAME);

            task.doLast(new Action<Task>() {
                @Override
                public void execute(Task task) {
                    task.getLogger().quiet("Releasing to production...");

                    // release the artifact to production
                }
            });
        }
    }
}

现在构建逻辑已转换为插件,您可以将其应用到构建脚本中。构建脚本已从 8 行代码缩减为一行。

build.gradle.kts
plugins {
    id("com.enterprise.release")
}
build.gradle
plugins {
    id 'com.enterprise.release'
}

避免使用内部 Gradle API

当 Gradle 或插件发生更改时,在插件和构建脚本中使用 Gradle 内部 API 可能会破坏构建。

Gradle 公共 API 定义Kotlin DSL API 定义中列出了以下包 ,internal名称中带有 的任何子包除外。

Gradle API 包
org.gradle
org.gradle.api.*
org.gradle.authentication.*
org.gradle.build.*
org.gradle.buildinit.*
org.gradle.caching.*
org.gradle.concurrent.*
org.gradle.deployment.*
org.gradle.external.javadoc.*
org.gradle.ide.*
org.gradle.ivy.*
org.gradle.jvm.*
org.gradle.language.*
org.gradle.maven.*
org.gradle.nativeplatform.*
org.gradle.normalization.*
org.gradle.platform.*
org.gradle.plugin.devel.*
org.gradle.plugin.use
org.gradle.plugin.management
org.gradle.plugins.*
org.gradle.process.*
org.gradle.testfixtures.*
org.gradle.testing.jacoco.*
org.gradle.tooling.*
org.gradle.swiftpm.*
org.gradle.model.*
org.gradle.testkit.*
org.gradle.testing.*
org.gradle.vcs.*
org.gradle.work.*
org.gradle.workers.*
org.gradle.util.*
Kotlin DSL API 包
org.gradle.kotlin.dsl
org.gradle.kotlin.dsl.precompile

常用内部 API 的替代方案

要为您的自定义任务提供嵌套 DSL,请勿使用org.gradle.internal.reflect.Instantiator;请改用ObjectFactory。阅读有关延迟配置的章节也可能会有所帮助。

不要使用org.gradle.api.internal.ConventionMapping.使用Provider和/或Property。您可以在实现插件部分找到捕获用户输入以配置运行时行为的示例。

代替org.gradle.internal.os.OperatingSystem,使用另一种方法来检测操作系统,例如Apache commons-lang SystemUtilsSystem.getProperty("os.name").

使用其他集合或 I/O 框架代替org.gradle.util.CollectionUtilsorg.gradle.util.internal.GFileUtils和 下的其他类org.gradle.util.*

声明任务时遵循约定

任务 API 为构建作者在构建脚本中声明任务提供了很大的灵活性。为了获得最佳的可读性和可维护性,请遵循以下规则:

build.gradle.kts
import com.enterprise.DocsGenerate

tasks.register<DocsGenerate>("generateHtmlDocs") {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = "Generates the HTML documentation for this project."
    title = "Project docs"
    outputDir = layout.buildDirectory.dir("docs")
}

tasks.register("allDocs") {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = "Generates all documentation for this project."
    dependsOn("generateHtmlDocs")

    doLast {
        logger.quiet("Generating all documentation...")
    }
}
build.gradle
import com.enterprise.DocsGenerate

def generateHtmlDocs = tasks.register('generateHtmlDocs', DocsGenerate) {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = 'Generates the HTML documentation for this project.'
    title = 'Project docs'
    outputDir = layout.buildDirectory.dir('docs')
}

tasks.register('allDocs') {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = 'Generates all documentation for this project.'
    dependsOn generateHtmlDocs

    doLast {
        logger.quiet('Generating all documentation...')
    }
}

提高任务的可发现性

即使是构建的新用户也应该能够快速、轻松地找到关键信息。在 Gradle 中,您可以为构建的任何任务声明一个描述。任务报告使用分配的值来组织和呈现任务,以便于发现。分配组和描述对于您希望构建用户调用的任何任务最有帮助。

该示例任务generateDocs以 HTML 页面的形式生成项目文档。任务应该组织在桶下面Documentation。描述应表达其意图。

build.gradle.kts
tasks.register("generateDocs") {
    group = "Documentation"
    description = "Generates the HTML documentation for this project."

    doLast {
        // action implementation
    }
}
build.gradle
tasks.register('generateDocs') {
    group = 'Documentation'
    description = 'Generates the HTML documentation for this project.'

    doLast {
        // action implementation
    }
}

任务报告的输出反映了分配的值。

> gradle tasks

> Task :tasks

Documentation tasks
-------------------
generateDocs - Generates the HTML documentation for this project.

最小化配置阶段执行的逻辑

对于每个构建脚本开发人员来说,了解构建生命周期的不同阶段及其对构建逻辑的性能和评估顺序的影响非常重要。在配置阶段,应该配置项目及其域对象,而执行阶段仅执行命令行上请求的任务的操作及其依赖项。请注意,任何不属于任务操作的代码都将在每次构建运行时执行。构建扫描可以帮助您确定每个生命周期阶段所花费的时间。它是诊断常见性能问题的宝贵工具。

让我们考虑一下上述反模式的以下咒语。在构建脚本中,您可以看到分配给配置的依赖项printArtifactNames是在任务操作之外解析的。

build.gradle.kts
dependencies {
    implementation("log4j:log4j:1.2.17")
}

tasks.register("printArtifactNames") {
    // always executed
    val libraryNames = configurations.compileClasspath.get().map { it.name }

    doLast {
        logger.quiet(libraryNames.joinToString())
    }
}
build.gradle
dependencies {
    implementation 'log4j:log4j:1.2.17'
}

tasks.register('printArtifactNames') {
    // always executed
    def libraryNames = configurations.compileClasspath.collect { it.name }

    doLast {
        logger.quiet libraryNames
    }
}

用于解决依赖关系的代码应移至任务操作中,以避免在实际需要依赖关系之前解决依赖关系对性能造成影响。

build.gradle.kts
dependencies {
    implementation("log4j:log4j:1.2.17")
}

tasks.register("printArtifactNames") {
    val compileClasspath: FileCollection = configurations.compileClasspath.get()
    doLast {
        val libraryNames = compileClasspath.map { it.name }
        logger.quiet(libraryNames.joinToString())
    }
}
build.gradle
dependencies {
    implementation 'log4j:log4j:1.2.17'
}

tasks.register('printArtifactNames') {
    FileCollection compileClasspath = configurations.compileClasspath
    doLast {
        def libraryNames = compileClasspath.collect { it.name }
        logger.quiet libraryNames
    }
}

避免使用GradleBuild任务类型

GradleBuild任务类型允许构建脚本定义调用另一个 Gradle 构建的任务。通常不鼓励使用这种类型。在某些特殊情况下,调用的构建不会公开与命令行或工具 API 相同的运行时行为,从而导致意外结果。

通常,有更好的方法来对需求进行建模。适当的方法取决于当前的问题。这里有一些选项:

  • 如果目的是将不同模块中的任务作为统一构建来执行,则将构建建模为多项目构建。

  • 对于物理上分离但偶尔应构建为单个单元的项目使用复合构建。

避免项目间配置

Gradle 不限制构建脚本作者在多项目构建中从一个项目进入另一个项目的域模型。强耦合的项目会损害构建执行性能以及代码的可读性和可维护性。

应避免以下做法:

外部化并加密您的密码

大多数构建需要使用一个或多个密码。这种需要的原因可能有所不同。某些构建需要密码才能将工件发布到安全的二进制存储库,其他构建需要密码才能下载二进制文件。密码应始终安全,以防止欺诈。在任何情况下都不应将密码以纯文本形式添加到构建脚本中或在gradle.properties项目目录的文件中声明它。这些文件通常位于版本控制存储库中,任何有权访问它的人都可以查看。

密码和任何其他敏感数据应保存在版本控制项目文件的外部。 Gradle 公开了一个 API,用于在ProviderFactoryArtifact Repositories中提供凭证 ,允许 在构建需要时使用Gradle 属性提供凭证值。这样,凭据可以存储在gradle.properties驻留在用户主目录中的文件中,或者使用命令行参数或环境变量注入到构建中。

如果您将敏感凭据存储在用户 home 中gradle.properties,请考虑对其进行加密。目前 Gradle 不提供用于加密、存储和访问密码的内置机制。解决这个问题的一个很好的解决方案是Gradle Credentials 插件

不要预期配置创建

Gradle 将使用“如果需要则检查”策略创建某些配置,例如default或。archives这意味着它只会创建这些配置(如果它们尚不存在)。

应该自己创建这些配置。诸如此类的名称以及与源集关联的配置的名称应被视为隐式“保留”。保留名称的确切列表取决于应用的插件以及构建的配置方式。

这种情况将通过以下弃用警告来宣布:

Configuration customCompileClasspath already exists with permitted usage(s):
	Consumable - this configuration can be selected by another project as a dependency
	Resolvable - this configuration can be resolved by this project to a set of files
	Declarable - this configuration can have dependencies added to it
Yet Gradle expected to create it with the usage(s):
	Resolvable - this configuration can be resolved by this project to a set of files

然后,Gradle 将尝试改变允许的用法以匹配预期的用法,并发出第二个警告:

Gradle will mutate the usage of this configuration to match the expected usage. This may cause unexpected behavior. Creating configurations with reserved names has been deprecated. This is scheduled to be removed in Gradle 9.0. Create source sets prior to creating or accessing the configurations associated with them.

某些配置可能会锁定其用途以防止突变。在这种情况下,您的构建将失败,并且此警告之后将立即出现带有以下消息的异常:

Gradle cannot mutate the usage of configuration 'customCompileClasspath' because it is locked.

如果您遇到此错误,您必须:

  1. 更改配置的名称以避免冲突。

  2. 如果无法更改名称,请确保配置允许的用途(可消耗的、可解析的、可声明的)与 Gradle 的期望一致。

作为最佳实践,您不应该“预期”配置创建 - 让 Gradle 首先创建配置,然后调整它。或者,如果可能,请在看到此警告时重命名自定义配置,使用不冲突的名称。