任何构建工具的重要组成部分是能够避免重复已经完成的工作。考虑编译过程。一旦你的源文件被编译,就不需要重新编译它们,除非某些影响输出的内容发生了变化,例如源文件的修改或输出文件的删除。而编译可能需要相当长的时间,因此在不需要的时候跳过这一步可以节省大量时间。

Gradle 通过名为 **增量构建** 的功能开箱即用地支持这种行为。你几乎肯定已经看到它在行动。当你运行一个任务并且该任务在控制台输出中标记为 UP-TO-DATE 时,这意味着增量构建正在起作用。

增量构建是如何工作的?如何确保你的任务支持增量运行?让我们来看看。

任务输入和输出

在最常见的情况下,一个任务会接收一些输入并生成一些输出。我们可以将 Java 编译过程作为任务的一个例子。Java 源文件充当任务的输入,而生成的类文件,即编译结果,是任务的输出。

taskInputsOutputs
图 1. 任务输入和输出示例

输入的一个重要特征是它会影响一个或多个输出,正如您从上图中看到的。根据源文件的内容和您希望运行代码的 Java 运行时的最低版本,会生成不同的字节码。这使得它们成为任务输入。但是,编译是否具有 500MB 或 600MB 的最大可用内存(由 memoryMaximumSize 属性决定)不会影响生成的字节码。在 Gradle 术语中,memoryMaximumSize 只是一个内部任务属性。

作为增量构建的一部分,Gradle 会测试自上次构建以来是否有任何任务输入或输出发生更改。如果它们没有改变,Gradle 可以认为任务是最新的,因此可以跳过执行其操作。还要注意,除非任务至少有一个任务输出,否则增量构建将无法正常工作,尽管任务通常也至少有一个输入。

这对构建作者来说意味着:您需要告诉 Gradle 哪些任务属性是输入,哪些是输出。如果任务属性影响输出,请确保将其注册为输入,否则当任务不是最新的时,它将被视为最新的。相反,如果属性不影响输出,请不要将其注册为输入,否则任务可能会在不需要执行时执行。还要注意可能对完全相同的输入生成不同输出的非确定性任务:这些任务不应配置为增量构建,因为最新检查将无法正常工作。

现在让我们看看如何将任务属性注册为输入和输出。

通过注解声明输入和输出

如果您正在将自定义任务实现为一个类,那么只需两个步骤即可使其与增量构建一起使用

  1. 为每个任务输入和输出创建类型化属性(通过 getter 方法)

  2. 为每个属性添加相应的注解

注解必须放在 getter 上或 Groovy 属性上。放在 setter 上或在没有相应注解 getter 的 Java 字段上的注解将被忽略。

Gradle 支持四种主要的输入和输出类别

  • 简单值

    例如字符串和数字。更一般地说,简单值可以具有实现 Serializable 的任何类型。

  • 文件系统类型

    这些包括 RegularFileDirectory 和标准的 File 类,但也包括 Gradle 的 FileCollection 类型的派生类,以及任何可以传递给 Project.file(java.lang.Object) 方法(对于单个文件/目录属性)或 Project.files(java.lang.Object...) 方法的任何其他内容。

  • 依赖项解析结果

    这包括用于工件元数据的 ResolvedArtifactResult 类型和用于依赖项图的 ResolvedComponentResult 类型。请注意,它们仅在包装在 Provider 中时才受支持。

  • 嵌套值

    不符合其他两类但具有自己的输入或输出属性的自定义类型。实际上,任务输入或输出嵌套在这些自定义类型中。

例如,假设您有一个处理各种类型模板的任务,例如 FreeMarker、Velocity、Moustache 等。它接受模板源文件并将它们与一些模型数据结合起来,以生成模板文件的填充版本。

此任务将有三个输入和一个输出

  • 模板源文件

  • 模型数据

  • 模板引擎

  • 输出文件写入的位置

在编写自定义任务类时,可以通过注解轻松地将属性注册为输入或输出。为了演示,这里是一个带有某些合适输入和输出的骨架任务实现,以及它们的注解

buildSrc/src/main/java/org/example/ProcessTemplates.java
package org.example;

import java.util.HashMap;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileSystemOperations;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.*;

import javax.inject.Inject;

public abstract class ProcessTemplates extends DefaultTask {

    @Input
    public abstract Property<TemplateEngineType> getTemplateEngine();

    @InputFiles
    public abstract ConfigurableFileCollection getSourceFiles();

    @Nested
    public abstract TemplateData getTemplateData();

    @OutputDirectory
    public abstract DirectoryProperty getOutputDir();

    @Inject
    public abstract FileSystemOperations getFs();

    @TaskAction
    public void processTemplates() {
        // ...
    }
}
buildSrc/src/main/java/org/example/TemplateData.java
package org.example;

import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;

public abstract class TemplateData {

    @Input
    public abstract Property<String> getName();

    @Input
    public abstract MapProperty<String, String> getVariables();
}
gradle processTemplates 的输出
> gradle processTemplates
> Task :processTemplates

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date
gradle processTemplates 的输出(再次运行)
> gradle processTemplates
> Task :processTemplates UP-TO-DATE

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 up-to-date

这个例子有很多值得讨论的地方,所以让我们依次讨论每个输入和输出属性

  • templateEngine

    表示在处理源模板时要使用哪个引擎,例如 FreeMarker、Velocity 等。您可以将其实现为字符串,但在这种情况下,我们选择了自定义枚举,因为它提供了更多类型信息和安全性。由于枚举自动实现 Serializable,我们可以将其视为简单值并使用 @Input 注解,就像使用 String 属性一样。

  • sourceFiles

    任务将处理的源模板。单个文件和文件集合需要各自的特殊注释。在本例中,我们处理的是输入文件的集合,因此我们使用@InputFiles注释。您将在后面的表格中看到更多与文件相关的注释。

  • templateData

    对于此示例,我们使用自定义类来表示模型数据。但是,它没有实现Serializable,因此我们无法使用@Input注释。这不是问题,因为TemplateData中的属性——一个字符串和一个具有可序列化类型参数的哈希映射——是可序列化的,并且可以使用@Input进行注释。我们在templateData上使用@Nested来让 Gradle 知道这是一个具有嵌套输入属性的值。

  • outputDir

    生成的 文件存放的目录。与输入文件一样,输出文件和目录也有几种注释。表示单个目录的属性需要@OutputDirectory。您很快就会了解其他注释。

这些带注释的属性意味着如果自 Gradle 上次执行任务以来,源文件、模板引擎、模型数据或生成的文件都没有发生变化,Gradle 将跳过该任务。这通常可以节省大量时间。您可以了解 Gradle 如何检测更改

此示例特别有趣,因为它使用源文件的集合。如果只有一个源文件发生变化会怎样?任务会再次处理所有源文件,还是只处理修改后的文件?这取决于任务的实现。如果是后者,那么任务本身是增量的,但这与我们正在讨论的功能不同。Gradle 通过其增量任务输入功能帮助任务实现者实现这一点。

现在您已经看到了实践中的一些输入和输出注释,让我们看一下您可以使用的所有注释以及何时应该使用它们。下表列出了可用的注释以及您可以与每个注释一起使用的相应属性类型。

表 1. 增量构建属性类型注解
注解 预期属性类型 描述

任何 Serializable 类型或依赖项解析结果类型

一个简单的输入值或依赖项解析结果

文件*

单个输入文件(不是目录)

文件*

单个输入目录(不是文件)

Iterable<File>*

输入文件和目录的可迭代集合

Iterable<File>*

表示 Java 类路径的输入文件和目录的可迭代集合。这允许任务忽略对属性的无关更改,例如相同文件的不同名称。它类似于用 @PathSensitive(RELATIVE) 注解属性,但它会忽略直接添加到类路径的 JAR 文件的名称,并且会将文件顺序的更改视为类路径的更改。Gradle 将检查类路径上 jar 文件的内容,并忽略不影响类路径语义的更改(例如文件日期和条目顺序)。另请参阅 使用类路径注解

注意:@Classpath 注解是在 Gradle 3.2 中引入的。为了与早期 Gradle 版本保持兼容,类路径属性也应该用 @InputFiles 注解。

Iterable<File>*

表示 Java 编译类路径的输入文件和目录的可迭代集合。这允许任务忽略不影响类路径中类 API 的无关更改。另请参阅 使用类路径注解

以下类型的类路径更改将被忽略

  • jar 或顶级目录路径的更改。

  • jar 中条目的时间戳和顺序的更改。

  • 资源和 jar 清单的更改,包括添加或删除资源。

  • 对私有类元素的更改,例如私有字段、方法和内部类。

  • 对代码的更改,例如方法体、静态初始化器和字段初始化器(常量除外)。

  • 对调试信息的更改,例如当对注释的更改影响类调试信息中的行号时。

  • 目录更改,包括 Jar 中的目录条目。

注意 - @CompileClasspath 注解是在 Gradle 3.4 中引入的。为了与 Gradle 3.3 和 3.2 保持兼容,编译类路径属性也应该使用 @Classpath 注解。为了与 Gradle 3.2 之前的版本兼容,该属性也应该使用 @InputFiles 注解。

文件*

单个输出文件(不是目录)

文件*

单个输出目录(不是文件)

Map<String, File>** 或 Iterable<File>*

输出文件的可迭代对象或映射。使用文件树会关闭任务的 缓存

Map<String, File>** 或 Iterable<File>*

输出目录的可迭代对象。使用文件树会关闭任务的 缓存

FileIterable<File>*

指定一个或多个由该任务删除的文件。请注意,任务可以定义输入/输出或可销毁项,但不能同时定义两者。

FileIterable<File>*

指定一个或多个文件,这些文件代表 任务的本地状态。当任务从缓存中加载时,这些文件将被删除。

任何自定义类型

一个自定义类型,它可能没有实现 Serializable,但至少有一个字段或属性被标记为本表中的一个注解。它甚至可以是另一个 @Nested

任何类型

表示该属性既不是输入也不是输出。它只是以某种方式影响任务的控制台输出,例如增加或减少任务的详细程度。

任何类型

表示该属性在内部使用,但既不是输入也不是输出。

任何类型

表示该属性已被另一个属性替换,应将其忽略为输入或输出。

FileIterable<File>*

@InputFiles@InputDirectory 一起使用,告诉 Gradle 如果相应的文件或目录为空,以及所有其他使用此注解声明的输入文件,则跳过该任务。由于所有使用此注解声明的输入文件为空而被跳过的任务将导致不同的“无源”结果。例如,NO-SOURCE 将在控制台输出中发出。

暗示 @Incremental

Provider<FileSystemLocation>FileCollection

@InputFiles@InputDirectory 一起使用,指示 Gradle 跟踪注释文件属性的更改,以便可以通过 @InputChanges.getFileChanges() 查询这些更改。对于 增量任务 来说是必需的。

任何类型

Optional API 文档中列出的任何属性类型注释一起使用。此注释禁用对相应属性的验证检查。有关更多详细信息,请参阅 验证部分

FileIterable<File>*

与任何输入文件属性一起使用,告诉 Gradle 只将文件路径的给定部分视为重要。例如,如果一个属性用 @PathSensitive(PathSensitivity.NAME_ONLY) 注释,那么在不更改文件内容的情况下移动文件将不会使任务失效。

FileIterable<File>*

@InputFiles@InputDirectory 一起使用,指示 Gradle 只跟踪目录内容的更改,而不是跟踪目录本身的差异。例如,删除、重命名或在目录结构中的某个位置添加一个空目录将不会使任务失效。

FileIterable<File>*

@InputFiles@InputDirectory@Classpath 一起使用,指示 Gradle 在计算最新检查或构建缓存键时规范化行尾。例如,将文件在 Unix 行尾和 Windows 行尾之间切换(反之亦然)将不会使任务失效。

File 可以是 Project.file(java.lang.Object) 接受的任何类型,而 Iterable<File> 可以是 Project.files(java.lang.Object…​) 接受的任何类型。这包括 Callable 的实例,例如闭包,允许对属性值进行延迟评估。请注意,类型 FileCollectionFileTreeIterable<File>

与上面类似,File 可以是 Project.file(java.lang.Object) 接受的任何类型。Map 本身可以包装在 Callable 中,例如闭包。

注释从所有父类型继承,包括实现的接口。属性类型注释会覆盖在父类型中声明的任何其他属性类型注释。这样,@InputFile 属性可以在子任务类型中变为 @InputDirectory 属性。

在类型中声明的属性上的注释会覆盖超类和任何实现的接口中声明的类似注释。超类注释优先于在实现的接口中声明的注释。

表中的 ConsoleInternal 注解是特殊情况,因为它们不声明任务输入或任务输出。那么为什么要使用它们呢?这样你就可以利用 Java Gradle Plugin Development 插件 来帮助你开发和发布自己的插件。此插件会检查你的自定义任务类中是否有任何属性缺少增量构建注解。这可以防止你在开发过程中忘记添加适当的注解。

使用依赖解析结果

依赖解析结果可以通过两种方式用作任务输入。首先,通过使用 ResolvedComponentResult 使用解析的元数据图。其次,通过使用 ResolvedArtifactResult 使用解析的工件的扁平集合。

可以从 Configuration 的传入解析结果中延迟获取解析的图,并将其连接到 @Input 属性

任务声明
@Input
public abstract Property<ResolvedComponentResult> getRootComponent();
任务配置
Configuration runtimeClasspath = configurations.getByName("runtimeClasspath");

task.getRootComponent().set(
    runtimeClasspath.getIncoming().getResolutionResult().getRootComponent()
);

可以从 Configuration 的传入工件中延迟获取解析的工件集。鉴于 ResolvedArtifactResult 类型包含元数据和文件信息,实例需要在连接到 @Input 属性之前转换为仅元数据。

任务声明
@Input
public abstract ListProperty<ComponentArtifactIdentifier> getArtifactIds();
任务配置
Configuration runtimeClasspath = configurations.getByName("runtimeClasspath");
Provider<Set<ResolvedArtifactResult>> artifacts = runtimeClasspath.getIncoming().getArtifacts().getResolvedArtifacts();

task.getArtifactIds().set(artifacts.map(new IdExtractor()));

static class IdExtractor
    implements Transformer<List<ComponentArtifactIdentifier>, Collection<ResolvedArtifactResult>> {
    @Override
    public List<ComponentArtifactIdentifier> transform(Collection<ResolvedArtifactResult> artifacts) {
        return artifacts.stream().map(ResolvedArtifactResult::getId).collect(Collectors.toList());
    }
}

图和扁平结果都可以组合在一起,并使用解析的文件信息进行增强。所有这些都在 使用依赖解析结果输入的任务 示例中进行了演示。

使用类路径注解

除了 @InputFiles 之外,对于与 JVM 相关的任务,Gradle 理解类路径输入的概念。当 Gradle 查找更改时,运行时类路径和编译时类路径的处理方式不同。

与使用@InputFiles注解的输入属性不同,对于类路径属性,文件集合中条目的顺序很重要。另一方面,类路径本身中目录和 jar 文件的名称和路径会被忽略。类路径上 jar 文件中类文件和资源的时间戳和顺序也会被忽略,因此使用不同的文件日期重新创建 jar 文件不会使任务失效。

运行时类路径使用@Classpath标记,并通过类路径规范化提供进一步的自定义。

使用@CompileClasspath注解的输入属性被视为 Java 编译类路径。除了上述一般类路径规则之外,编译类路径还会忽略除类文件以外的所有更改。Gradle 使用Java 编译避免中描述的相同类分析来进一步过滤不影响类 ABI 的更改。这意味着仅触及类实现的更改不会使任务失效。

嵌套输入

在分析@Nested任务属性以查找声明的输入和输出子属性时,Gradle 使用实际值的类型。因此,它可以发现由运行时子类型声明的所有子属性。

当将@Nested添加到Provider时,Provider的值将被视为嵌套输入。

当将@Nested添加到可迭代对象时,每个元素都被视为一个单独的嵌套输入。可迭代对象中的每个嵌套输入都分配一个名称,默认情况下是美元符号后跟可迭代对象中的索引,例如$2。如果可迭代对象的元素实现了Named,则使用该名称作为属性名称。如果并非所有元素都实现了Named,则可迭代对象中元素的顺序对于可靠的最新检查和缓存至关重要。不允许多个具有相同名称的元素。

当在映射中添加@Nested时,对于每个值,都会使用键作为名称添加一个嵌套输入。

嵌套输入的类型和类路径也会被跟踪。这确保了对嵌套输入实现的更改会导致构建过时。通过这种方式,也可以将用户提供的代码作为输入添加,例如,通过使用@Nested注释@Action属性。请注意,对这些操作的任何输入都应被跟踪,无论是通过操作上的注释属性,还是通过手动将它们注册到任务中。

使用嵌套输入允许对任务进行更丰富的建模和扩展,例如,如Test.getJvmArgumentProviders()所示。

这使我们能够对 JaCoCo Java 代理进行建模,从而声明必要的 JVM 参数并向 Gradle 提供输入和输出。

JacocoAgent.java
class JacocoAgent implements CommandLineArgumentProvider {
    private final JacocoTaskExtension jacoco;

    public JacocoAgent(JacocoTaskExtension jacoco) {
        this.jacoco = jacoco;
    }

    @Nested
    @Optional
    public JacocoTaskExtension getJacoco() {
        return jacoco.isEnabled() ? jacoco : null;
    }

    @Override
    public Iterable<String> asArguments() {
        return jacoco.isEnabled() ? ImmutableList.of(jacoco.getAsJvmArg()) : Collections.<String>emptyList();
    }
}

test.getJvmArgumentProviders().add(new JacocoAgent(extension));

为了使这能够正常工作,JacocoTaskExtension需要具有正确的输入和输出注释。

这种方法适用于 Test JVM 参数,因为Test.getJvmArgumentProviders()是一个用@Nested注释的Iterable

还有其他任务类型可以使用这种嵌套输入。

同样,这种建模方式也适用于自定义任务。

运行时验证

执行构建时,Gradle 会检查任务类型是否使用正确的注释声明。它会尝试识别问题,例如,注释是否用于不兼容的类型或 setter 等。任何未用输入/输出注释注释的 getter 也会被标记。这些问题会导致构建失败,或者在执行任务时变为弃用警告。

具有验证警告的任务将不会进行任何优化执行。具体来说,它们永远不会

  • 最新更新的,

  • 构建缓存 中加载或存储的,

  • 即使启用了 并行执行,也与其他任务并行执行的,

  • 增量执行的。

在执行无效任务之前,文件系统状态的内存表示(虚拟文件系统) 也会失效。

通过运行时 API 声明输入和输出

自定义任务类是将您自己的构建逻辑引入增量构建领域的一种简单方法,但您并不总是拥有这种选择。这就是 Gradle 还提供了一个可用于任何任务的替代 API 的原因,我们将在下面介绍。

当您无法访问自定义任务类的源代码时,就无法添加我们在上一节中介绍的任何注释。幸运的是,Gradle 提供了一个运行时 API,用于处理此类场景。它也可以用于临时任务,正如您将在下面看到的那样。

声明临时任务的输入和输出

此运行时 API 是通过几个名称恰当的属性提供的,这些属性在每个 Gradle 任务中都可用

这些对象具有允许您指定构成任务输入和输出的文件、目录和值的方法。实际上,运行时 API 几乎与注释具有相同的特性。

它缺少以下内容的等效项

让我们以之前的模板处理示例为例,看看它如何作为使用运行时 API 的临时任务。

示例 4. 临时任务
build.gradle.kts
tasks.register("processTemplatesAdHoc") {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", mapOf("year" to "2013"))
    outputs.dir(layout.buildDirectory.dir("genOutput2"))
        .withPropertyName("outputDir")

    doLast {
        // Process the templates here
    }
}
build.gradle
tasks.register('processTemplatesAdHoc') {
    inputs.property('engine', TemplateEngineType.FREEMARKER)
    inputs.files(fileTree('src/templates'))
        .withPropertyName('sourceFiles')
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property('templateData.name', 'docs')
    inputs.property('templateData.variables', [year: '2013'])
    outputs.dir(layout.buildDirectory.dir('genOutput2'))
        .withPropertyName('outputDir')

    doLast {
        // Process the templates here
    }
}
gradle processTemplatesAdHoc 的输出
> gradle processTemplatesAdHoc
> Task :processTemplatesAdHoc

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

和以前一样,这里有很多内容需要讨论。首先,您应该真正为此编写一个自定义任务类,因为它是一个非平凡的实现,具有多个配置选项。在这种情况下,没有任务属性来存储根源文件夹、输出目录的位置或任何其他设置。这是为了突出运行时 API 不需要任务有任何状态这一事实。在增量构建方面,上面的临时任务将与自定义任务类表现相同。

所有输入和输出定义都是通过 inputsoutputs 上的方法完成的,例如 property()files()dir()。Gradle 对参数值执行最新检查,以确定任务是否需要再次运行。每个方法对应于一个增量构建注释,例如 inputs.property() 映射到 @Inputoutputs.dir() 映射到 @OutputDirectory

任务删除的文件可以通过 `destroyables.register()` 指定。

build.gradle.kts
tasks.register("removeTempDir") {
    val tmpDir = layout.projectDirectory.dir("tmpDir")
    destroyables.register(tmpDir)
    doLast {
        tmpDir.asFile.deleteRecursively()
    }
}
build.gradle
tasks.register('removeTempDir') {
    def tempDir = layout.projectDirectory.dir('tmpDir')
    destroyables.register(tempDir)
    doLast {
        tempDir.asFile.deleteDir()
    }
}

运行时 API 和注解之间的一个显著区别是缺少直接对应于 `@Nested` 的方法。这就是为什么示例对模板数据使用两个 `property()` 声明,每个 `TemplateData` 属性一个。在使用运行时 API 处理嵌套值时,您应该使用相同的技术。任何给定任务都可以声明可销毁项或输入/输出,但不能同时声明两者。

细粒度配置

运行时 API 方法只允许您在自身中声明输入和输出。但是,面向文件的方法会返回一个构建器 - 类型为 TaskInputFilePropertyBuilder - 允许您提供有关这些输入和输出的附加信息。

您可以在其 API 文档中了解构建器提供的所有选项,但我们将在此处向您展示一个简单的示例,让您了解可以做什么。

假设我们不想在没有源文件的情况下运行 `processTemplates` 任务,无论是否是干净构建。毕竟,如果没有源文件,任务就无事可做。构建器允许我们像这样配置它

build.gradle.kts
tasks.register("processTemplatesAdHocSkipWhenEmpty") {
    // ...

    inputs.files(fileTree("src/templates") {
            include("**/*.fm")
        })
        .skipWhenEmpty()
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
        .ignoreEmptyDirectories()

    // ...
}
build.gradle
tasks.register('processTemplatesAdHocSkipWhenEmpty') {
    // ...

    inputs.files(fileTree('src/templates') {
            include '**/*.fm'
        })
        .skipWhenEmpty()
        .withPropertyName('sourceFiles')
        .withPathSensitivity(PathSensitivity.RELATIVE)
        .ignoreEmptyDirectories()

    // ...
}
`gradle clean processTemplatesAdHocSkipWhenEmpty` 的输出
> gradle clean processTemplatesAdHocSkipWhenEmpty
> Task :processTemplatesAdHocSkipWhenEmpty NO-SOURCE

BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

`TaskInputs.files()` 方法返回一个具有 `skipWhenEmpty()` 方法的构建器。调用此方法等同于使用 `@SkipWhenEmpty` 对属性进行注释。

现在您已经看到了注解和运行时 API,您可能想知道应该使用哪个 API。我们的建议是在可能的情况下使用注解,有时创建自定义任务类也是值得的,这样您就可以使用它们。运行时 API 更适合您无法使用注解的情况。

为自定义任务类型声明输入和输出

另一种类型的示例涉及为自定义任务类的实例注册额外的输入和输出。例如,假设 `ProcessTemplates` 任务还需要读取 `src/headers/headers.txt`(例如,因为它从其中一个源文件中包含)。您希望 Gradle 了解此输入文件,以便它可以在此文件内容更改时重新执行任务。使用运行时 API,您可以做到这一点

build.gradle.kts
tasks.register<ProcessTemplates>("processTemplatesWithExtraInputs") {
    // ...

    inputs.file("src/headers/headers.txt")
        .withPropertyName("headers")
        .withPathSensitivity(PathSensitivity.NONE)
}
build.gradle
tasks.register('processTemplatesWithExtraInputs', ProcessTemplates) {
    // ...

    inputs.file('src/headers/headers.txt')
        .withPropertyName('headers')
        .withPathSensitivity(PathSensitivity.NONE)
}

像这样使用运行时 API 有点类似于使用 doLast()doFirst() 将额外的操作附加到任务,只是在这种情况下,我们附加的是有关输入和输出的信息。

如果任务类型已经使用增量构建注释,则使用相同属性名称注册输入或输出会导致错误。

声明任务输入和输出的好处

一旦你声明了任务的正式输入和输出,Gradle 就可以推断出有关这些属性的信息。例如,如果一个任务的输入设置为另一个任务的输出,这意味着第一个任务依赖于第二个任务,对吧?Gradle 知道这一点,并且可以根据它采取行动。

我们将在下一节中介绍此功能,以及 Gradle 了解输入和输出后带来的其他一些功能。

推断的任务依赖关系

考虑一个打包 processTemplates 任务输出的存档任务。构建作者会看到存档任务显然需要 processTemplates 首先运行,因此可能会添加一个显式的 dependsOn。但是,如果你像这样定义存档任务

build.gradle.kts
tasks.register<Zip>("packageFiles") {
    from(processTemplates.map { it.outputDir })
}
build.gradle
tasks.register('packageFiles', Zip) {
    from processTemplates.map { it.outputDir }
}
gradle clean packageFiles 的输出
> gradle clean packageFiles
> Task :processTemplates
> Task :packageFiles

BUILD SUCCESSFUL in 0s
5 actionable tasks: 4 executed, 1 up-to-date

Gradle 会自动使 packageFiles 依赖于 processTemplates。它可以做到这一点,因为它知道 packageFiles 的一个输入需要 processTemplates 任务的输出。我们称之为推断的任务依赖关系。

上面的示例也可以写成

build.gradle.kts
tasks.register<Zip>("packageFiles2") {
    from(processTemplates)
}
build.gradle
tasks.register('packageFiles2', Zip) {
    from processTemplates
}
gradle clean packageFiles2 的输出
> gradle clean packageFiles2
> Task :processTemplates
> Task :packageFiles2

BUILD SUCCESSFUL in 0s
5 actionable tasks: 4 executed, 1 up-to-date

这是因为 from() 方法可以接受任务对象作为参数。在幕后,from() 使用 project.files() 方法来包装参数,这反过来又将任务的正式输出公开为文件集合。换句话说,这是一个特例!

输入和输出验证

增量构建注释提供了足够的信息,使 Gradle 能够对带注释的属性执行一些基本验证。特别是,它在任务执行之前对每个属性执行以下操作

  • @InputFile - 验证属性是否有值,以及路径是否对应于存在的文件(而不是目录)。

  • @InputDirectory - 与 @InputFile 相同,但路径必须对应于目录。

  • @OutputDirectory - 验证路径不匹配文件,如果目录不存在,则创建目录。

如果一个任务在某个位置生成输出,而另一个任务通过将该位置作为输入来使用该位置,那么 Gradle 会检查消费者任务是否依赖于生产者任务。当生产者和消费者任务同时执行时,构建会失败以避免捕获不正确的状态。

这种验证提高了构建的健壮性,使您能够快速识别与输入和输出相关的问题。

您偶尔会想要禁用一些验证,特别是在输入文件可能有效地不存在的情况下。这就是 Gradle 提供 @Optional 注释的原因:您可以使用它告诉 Gradle 特定的输入是可选的,因此如果相应的文件或目录不存在,构建不应该失败。

持续构建

定义任务输入和输出的另一个好处是持续构建。由于 Gradle 知道任务依赖于哪些文件,因此如果任何输入发生更改,它可以自动再次运行任务。通过在运行 Gradle 时激活持续构建(通过 --continuous-t 选项),您将使 Gradle 进入一种状态,在这种状态下,它会不断检查更改,并在遇到此类更改时执行请求的任务。

您可以在 持续构建 中了解更多有关此功能的信息。

任务并行

定义任务输入和输出的最后一个好处是,Gradle 可以使用此信息来决定在使用 "--parallel" 选项时如何运行任务。例如,Gradle 会在选择要运行的下一个任务时检查任务的输出,并避免同时执行写入同一输出目录的任务。类似地,Gradle 会使用有关任务销毁哪些文件的信息(例如,由 Destroys 注释指定),并避免运行删除一组文件的同时,另一个任务正在运行该文件,该任务使用或创建这些相同的文件(反之亦然)。它还可以确定创建一组文件的任务已经运行,并且使用这些文件的任务尚未运行,并将避免运行在两者之间删除这些文件的任务。通过以这种方式提供任务输入和输出信息,Gradle 可以推断任务之间的创建/使用/销毁关系,并确保任务执行不会违反这些关系。

它是如何工作的?

在任务第一次执行之前,Gradle 会对输入进行指纹识别。这个指纹包含输入文件的路径和每个文件内容的哈希值。然后 Gradle 执行任务。如果任务成功完成,Gradle 会对输出进行指纹识别。这个指纹包含输出文件的集合和每个文件内容的哈希值。Gradle 会将这两个指纹保存起来,以便下次执行任务时使用。

之后每次执行任务之前,Gradle 都会对输入和输出进行新的指纹识别。如果新的指纹与之前的指纹相同,Gradle 会认为输出是最新的,并跳过任务。如果它们不同,Gradle 会执行任务。Gradle 会将这两个指纹保存起来,以便下次执行任务时使用。

如果文件的统计信息(例如 lastModifiedsize)没有改变,Gradle 会重用上一次运行的指纹。这意味着如果文件的统计信息没有改变,Gradle 不会检测到更改。

Gradle 还将任务的代码视为任务输入的一部分。当任务、其操作或其依赖项在执行之间发生变化时,Gradle 会认为任务已过时。

Gradle 了解文件属性(例如包含 Java 类路径的属性)是否对顺序敏感。在比较此类属性的指纹时,即使文件顺序发生变化,也会导致任务过时。

请注意,如果任务指定了输出目录,则自上次执行以来添加到该目录的任何文件都会被忽略,并且不会导致任务过时。这是为了让不相关的任务可以共享输出目录,而不会相互干扰。如果出于某种原因,您不希望出现这种行为,请考虑使用 TaskOutputs.upToDateWhen(groovy.lang.Closure)

还要注意,更改不可用文件的可用性(例如,将损坏的符号链接的目标修改为有效文件,反之亦然),将被检测到并由最新检查处理。

任务的输入还用于计算 构建缓存 键,该键用于在启用时加载任务输出。有关更多详细信息,请参阅 任务输出缓存

为了跟踪任务、任务操作和嵌套输入的实现,Gradle 使用类名和包含实现的类路径的标识符。在某些情况下,Gradle 无法精确跟踪实现

未知类加载器

当加载实现的类加载器不是由 Gradle 创建时,无法确定类路径。

Java lambda

Java lambda 类是在运行时创建的,具有非确定性的类名。因此,类名无法识别 lambda 的实现,并且在不同的 Gradle 运行之间会发生变化。

当无法精确跟踪任务、任务操作或嵌套输入的实现时,Gradle 会禁用该任务的任何缓存。这意味着该任务永远不会是最新的,也不会从 构建缓存 中加载。

高级技术

在本节中,您所看到的所有内容都将涵盖您会遇到的大多数用例,但有一些场景需要特殊处理。接下来,我们将介绍其中的一些场景以及相应的解决方案。

添加您自己的缓存输入/输出方法

您是否曾经想过 Copy 任务的 from() 方法是如何工作的?它没有用 @InputFiles 进行注释,但传递给它的任何文件都被视为任务的正式输入。这是怎么回事?

实现非常简单,您可以对自己的任务使用相同的技术来改进它们的 API。编写您的方法,以便它们将文件直接添加到相应的带注释的属性中。例如,以下是如何将 sources() 方法添加到我们之前介绍的自定义 ProcessTemplates 类中

build.gradle.kts
tasks.register<ProcessTemplates>("processTemplates") {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData.name = "test"
    templateData.variables = mapOf("year" to "2012")
    outputDir = layout.buildDirectory.dir("genOutput")

    sources(fileTree("src/templates"))
}
build.gradle
tasks.register('processTemplates', ProcessTemplates) {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData.name = 'test'
    templateData.variables = [year: '2012']
    outputDir = file(layout.buildDirectory.dir('genOutput'))

    sources fileTree('src/templates')
}
ProcessTemplates.java
public abstract class ProcessTemplates extends DefaultTask {
    // ...
    @SkipWhenEmpty
    @InputFiles
    @PathSensitive(PathSensitivity.NONE)
    public abstract ConfigurableFileCollection getSourceFiles();

    public void sources(FileCollection sourceFiles) {
        getSourceFiles().from(sourceFiles);
    }

    // ...
}
gradle processTemplates 的输出
> gradle processTemplates
> Task :processTemplates

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

换句话说,只要您在配置阶段将值和文件添加到正式的任务输入和输出中,无论您从构建中的哪个位置添加它们,它们都将被视为正式的任务输入和输出。

如果我们希望支持任务作为参数,并将它们的输出视为输入,我们可以直接使用 TaskProvider,如下所示

build.gradle.kts
val copyTemplates by tasks.registering(Copy::class) {
    into(file(layout.buildDirectory.dir("tmp")))
    from("src/templates")
}

tasks.register<ProcessTemplates>("processTemplates2") {
    // ...
    sources(copyTemplates)
}
build.gradle
def copyTemplates = tasks.register('copyTemplates', Copy) {
    into file(layout.buildDirectory.dir('tmp'))
    from 'src/templates'
}

tasks.register('processTemplates2', ProcessTemplates) {
    // ...
    sources copyTemplates
}
ProcessTemplates.java
    // ...
    public void sources(TaskProvider<?> inputTask) {
        getSourceFiles().from(inputTask);
    }
    // ...
gradle processTemplates2 的输出
> gradle processTemplates2
> Task :copyTemplates
> Task :processTemplates2

BUILD SUCCESSFUL in 0s
4 actionable tasks: 4 executed

这种技术可以使您的自定义任务更易于使用,并导致更简洁的构建文件。作为一项额外的好处,我们对 TaskProvider 的使用意味着我们的自定义方法可以设置一个推断的任务依赖关系。

最后一点需要注意的是:如果您正在开发一个将源文件集合作为输入的任务,例如本示例,请考虑使用内置的 SourceTask。它将为您节省实现我们放入 ProcessTemplates 中的一些管道工作。

当您想要将一个任务的输出链接到另一个任务的输入时,类型通常匹配,简单的属性赋值将提供该链接。例如,一个 File 输出属性可以赋值给一个 File 输入。

不幸的是,当您想要将任务的 @OutputDirectory(类型为 File)中的文件作为另一个任务的 @InputFiles 属性(类型为 FileCollection)的源时,这种方法就会失效。由于两者类型不同,属性赋值将无法工作。

例如,假设您想使用 Java 编译任务的输出(通过 destinationDir 属性)作为自定义任务的输入,该自定义任务会对包含 Java 字节码的一组文件进行检测。这个自定义任务,我们称之为 Instrument,有一个用 @InputFiles 注释的 classFiles 属性。您可能最初会尝试像这样配置任务

build.gradle.kts
plugins {
    id("java-library")
}

tasks.register<Instrument>("badInstrumentClasses") {
    classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }))
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
plugins {
    id 'java-library'
}

tasks.register('badInstrumentClasses', Instrument) {
    classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {}
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean badInstrumentClasses 的输出
> gradle clean badInstrumentClasses
> Task :clean UP-TO-DATE
> Task :badInstrumentClasses NO-SOURCE

BUILD SUCCESSFUL in 0s
3 actionable tasks: 2 executed, 1 up-to-date

这段代码没有明显错误,但您可以从控制台输出中看到编译任务缺失。在这种情况下,您需要通过 dependsOninstrumentClassescompileJava 之间添加显式任务依赖关系。使用 fileTree() 意味着 Gradle 无法自行推断任务依赖关系。

一种解决方案是使用 TaskOutputs.files 属性,如下面的示例所示

build.gradle.kts
tasks.register<Instrument>("instrumentClasses") {
    classFiles.from(tasks.compileJava.map { it.outputs.files })
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClasses', Instrument) {
    classFiles.from tasks.named('compileJava').map { it.outputs.files }
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses 的输出
> gradle clean instrumentClasses
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses

BUILD SUCCESSFUL in 0s
5 actionable tasks: 4 executed, 1 up-to-date

或者,您可以使用 project.files()project.layout.files()project.objects.fileCollection() 代替 project.fileTree(),让 Gradle 自己访问相应的属性。

build.gradle.kts
tasks.register<Instrument>("instrumentClasses2") {
    classFiles.from(layout.files(tasks.compileJava))
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClasses2', Instrument) {
    classFiles.from layout.files(tasks.named('compileJava'))
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses2 的输出
> gradle clean instrumentClasses2
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClasses2

BUILD SUCCESSFUL in 0s
5 actionable tasks: 4 executed, 1 up-to-date

请记住,files()layout.files()objects.fileCollection() 可以接受任务作为参数,而 fileTree() 则不能。

这种方法的缺点是,源任务的所有文件输出都将成为目标任务的输入文件(在本例中为 instrumentClasses)。只要源任务只有一个基于文件的输出,比如 JavaCompile 任务,这就可以了。但是,如果您必须在多个输出属性中链接一个输出属性,那么您需要使用 builtBy 方法明确告诉 Gradle 哪个任务生成输入文件。

build.gradle.kts
tasks.register<Instrument>("instrumentClassesBuiltBy") {
    classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }) {
        builtBy(tasks.compileJava)
    })
    destinationDir = layout.buildDirectory.dir("instrumented")
}
build.gradle
tasks.register('instrumentClassesBuiltBy', Instrument) {
    classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {
        builtBy tasks.named('compileJava')
    }
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClassesBuiltBy 的输出
> gradle clean instrumentClassesBuiltBy
> Task :clean UP-TO-DATE
> Task :compileJava
> Task :instrumentClassesBuiltBy

BUILD SUCCESSFUL in 0s
5 actionable tasks: 4 executed, 1 up-to-date

当然,您可以通过dependsOn显式添加任务依赖关系,但上述方法提供了更多语义含义,解释了为什么compileJava必须先运行。

禁用最新检查

Gradle 自动处理输出文件和目录的最新检查,但如果任务输出是完全不同的东西呢?也许是对 Web 服务或数据库表的更新。或者有时您有一个应该始终运行的任务。

这就是Task上的doNotTrackState()方法的作用。您可以使用它完全禁用任务的最新检查,如下所示

build.gradle.kts
tasks.register<Instrument>("alwaysInstrumentClasses") {
    classFiles.from(layout.files(tasks.compileJava))
    destinationDir = layout.buildDirectory.dir("instrumented")
    doNotTrackState("Instrumentation needs to re-run every time")
}
build.gradle
tasks.register('alwaysInstrumentClasses', Instrument) {
    classFiles.from layout.files(tasks.named('compileJava'))
    destinationDir = file(layout.buildDirectory.dir('instrumented'))
    doNotTrackState("Instrumentation needs to re-run every time")
}
gradle clean alwaysInstrumentClasses的输出
> gradle clean alwaysInstrumentClasses
> Task :compileJava
> Task :alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
4 actionable tasks: 1 executed, 3 up-to-date
gradle alwaysInstrumentClasses的输出
> gradle alwaysInstrumentClasses
> Task :compileJava UP-TO-DATE
> Task :alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
4 actionable tasks: 1 executed, 3 up-to-date

如果您正在编写始终应该运行的自定义任务,那么您也可以在任务类上使用@UntrackedTask注释,而不是调用Task.doNotTrackState()

集成执行自身最新检查的外部工具

有时您想集成像 Git 或 Npm 这样的外部工具,它们都执行自己的最新检查。在这种情况下,Gradle 也进行最新检查没有多大意义。您可以使用@UntrackedTask注释在包装工具的任务上禁用 Gradle 的最新检查。或者,您可以使用运行时 API 方法Task.doNotTrackState()

例如,假设您想实现一个克隆 Git 存储库的任务。

buildSrc/src/main/java/org/example/GitClone.java
@UntrackedTask(because = "Git tracks the state") (1)
public abstract class GitClone extends DefaultTask {

    @Input
    public abstract Property<String> getRemoteUri();

    @Input
    public abstract Property<String> getCommitId();

    @OutputDirectory
    public abstract DirectoryProperty getDestinationDir();

    @TaskAction
    public void gitClone() throws IOException {
        File destinationDir = getDestinationDir().get().getAsFile().getAbsoluteFile(); (2)
        String remoteUri = getRemoteUri().get();
        // Fetch origin or clone and checkout
        // ...
    }

}
build.gradle.kts
tasks.register<GitClone>("cloneGradleProfiler") {
    destinationDir = layout.buildDirectory.dir("gradle-profiler") // <3
    remoteUri = "https://github.com/gradle/gradle-profiler.git"
    commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
build.gradle
tasks.register("cloneGradleProfiler", GitClone) {
    destinationDir = layout.buildDirectory.dir("gradle-profiler") (3)
    remoteUri = "https://github.com/gradle/gradle-profiler.git"
    commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
1 将任务声明为未跟踪。
2 使用输出目录运行外部工具。
3 在您的构建中添加任务并配置输出目录。

配置输入规范化

为了进行最新检查和使用 构建缓存,Gradle 需要确定两个任务输入属性是否具有相同的值。为此,Gradle 首先会对两个输入进行规范化,然后比较结果。例如,对于编译类路径,Gradle 会从类路径上的类中提取 ABI 签名,然后将上次 Gradle 运行和当前 Gradle 运行的签名进行比较,如 Java 编译避免 中所述。

规范化适用于类路径上的所有 zip 文件(例如 jar、war、aar、apk 等)。这使得 Gradle 能够识别出两个 zip 文件在功能上是否相同,即使 zip 文件本身可能由于元数据(例如时间戳或文件顺序)而略有不同。规范化不仅适用于类路径上的 zip 文件本身,还适用于类路径上目录或其他 zip 文件中嵌套的 zip 文件。

可以自定义 Gradle 用于运行时类路径规范化的内置策略。所有用 @Classpath 注解的输入都被视为运行时类路径。

假设您希望将一个名为 build-info.properties 的文件添加到所有生成的 jar 文件中,该文件包含有关构建的信息,例如构建开始的时间戳或用于标识发布工件的 CI 作业的 ID。此文件仅用于审计目的,不会影响运行测试的结果。但是,此文件是 test 任务的运行时类路径的一部分,并且在每次构建调用时都会发生变化。因此,test 将永远不会是最新的,也不会从构建缓存中提取。为了再次从增量构建中获益,您可以使用 Project.normalization(org.gradle.api.Action)(在使用项目中)告诉 Gradle 在运行时类路径上忽略此文件。

build.gradle.kts
normalization {
    runtimeClasspath {
        ignore("build-info.properties")
    }
}
build.gradle
normalization {
    runtimeClasspath {
        ignore 'build-info.properties'
    }
}

如果您在构建中的所有项目中都将此类文件添加到 jar 文件中,并且希望为所有使用者过滤此文件,则应考虑在 约定插件 中配置此类规范化,以便在子项目之间共享。

此配置的效果是,对 build-info.properties 的更改将被忽略,不会进行最新检查和 构建缓存 密钥计算。请注意,这不会改变 test 任务的运行时行为 - 即任何测试仍然能够加载 build-info.properties,并且运行时类路径仍然与以前相同。

属性文件规范化

默认情况下,属性文件(即以.properties扩展名结尾的文件)将被规范化,以忽略注释、空白和属性顺序的差异。Gradle 通过加载属性文件并在最新检查或构建缓存键计算期间仅考虑单个属性来实现这一点。

但是,有时某些属性会对运行时产生影响,而另一些属性则不会。如果更改的属性对运行时类路径没有影响,则可能希望将其从最新检查和构建缓存键计算中排除。但是,排除整个文件也会排除对运行时有影响的属性。在这种情况下,可以从运行时类路径上的任何或所有属性文件中选择性地排除属性。

可以使用RuntimeClasspathNormalization中描述的模式,将忽略属性的规则应用于特定的一组文件。如果文件匹配规则,但无法加载为属性文件(例如,由于格式不正确或使用非标准编码),它将作为普通文件合并到最新或构建缓存键计算中。换句话说,如果文件无法加载为属性文件,则对空白、属性顺序或注释的任何更改都可能导致任务过时或导致缓存未命中。

build.gradle.kts
normalization {
    runtimeClasspath {
        properties("**/build-info.properties") {
            ignoreProperty("timestamp")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        properties('**/build-info.properties') {
            ignoreProperty 'timestamp'
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        properties {
            ignoreProperty("timestamp")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        properties {
            ignoreProperty 'timestamp'
        }
    }
}

Java META-INF 规范化

对于 jar 存档中 META-INF 目录中的文件,由于其运行时影响,并非总是可以完全忽略文件。

META-INF 中的清单文件被规范化以忽略注释、空白和顺序差异。清单属性名称的比较不区分大小写和顺序。清单属性文件根据属性文件规范化进行规范化。

build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreAttribute("Implementation-Version")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreAttribute("Implementation-Version")
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreProperty("app.version")
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreProperty("app.version")
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreManifest()
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreManifest()
        }
    }
}
build.gradle.kts
normalization {
    runtimeClasspath {
        metaInf {
            ignoreCompletely()
        }
    }
}
build.gradle
normalization {
    runtimeClasspath {
        metaInf {
            ignoreCompletely()
        }
    }
}

提供自定义的最新逻辑

Gradle 自动处理输出文件和目录的最新检查,但如果任务输出是完全不同的东西怎么办?也许是对 Web 服务或数据库表的更新。在这种情况下,Gradle 无法知道如何检查任务是否是最新的。

这就是 TaskOutputs 上的 upToDateWhen() 方法的用武之地。它接受一个谓词函数,用于确定任务是否是最新的。例如,您可以从数据库中读取数据库模式的版本号。或者,您可以检查数据库表中的特定记录是否存在或是否已更改。

请注意,最新检查应该为您节省时间。不要添加与任务标准执行成本相同或更高的检查。事实上,如果任务最终经常运行,因为它很少是最新的,那么可能根本不值得进行任何最新检查,如 禁用最新检查 中所述。请记住,如果任务在执行任务图中,您的检查将始终运行。

一个常见的错误是使用 upToDateWhen() 而不是 Task.onlyIf()。如果您想根据与任务输入和输出无关的某些条件跳过任务,那么您应该使用 onlyIf()。例如,在您希望在设置或未设置特定属性时跳过任务的情况下。

过时的任务输出

当 Gradle 版本更改时,Gradle 会检测到使用旧版本 Gradle 运行的任务的输出需要删除,以确保最新版本的任务从已知的干净状态开始。

仅针对源集(Java/Groovy/Scala 编译)的输出实现了过时输出目录的自动清理。