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

第三方Gradle lint 插件有助于在构建脚本中强制使用所需的代码样式(如果你对此感兴趣的话)。

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

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 内部 API 可能会在 Gradle 或插件发生更改时破坏构建。

以下包列在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")

不要使用 org.gradle.util.CollectionUtilsorg.gradle.util.internal.GFileUtilsorg.gradle.util.* 中的其他类,而应使用其他集合或 I/O 框架。

声明任务时遵循惯例

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

  • 任务类型应是任务名称后括号中唯一的键值对。

  • 其他配置应在任务的配置块中完成。

  • 任务操作在声明任务时添加,应仅使用 Task.doFirst{}Task.doLast{} 方法声明。

  • 在声明临时任务(没有显式类型的任务)时,如果只声明一个操作,则应使用 Task.doLast{}

  • 任务应 定义组和说明

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 构建的任务。通常不鼓励使用此类型。在某些极端情况下,被调用的构建不会显示与命令行或通过 Tooling API 相同的运行时行为,从而导致意外结果。

通常,有更好的方法来对需求进行建模。适当的方法取决于手头的问题。以下是一些选项

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

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

避免项目间配置

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

应避免以下做法

将密码外化并加密

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

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

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

不要预先配置创建

Gradle 将使用“按需检查”策略创建某些配置,例如 defaultarchives。这意味着它只会创建这些配置,如果它们尚未存在。

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

此情况将通过以下弃用警告进行宣布

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 创建配置,然后对其进行调整。或者,如果可能,在看到此警告时通过重命名自定义配置来使用非冲突名称。