Gradle学习(十五)——增量构建

时间:2023-01-01 22:20:16

转载请注明出处:http://blog.csdn.net/lastsweetop/article/details/79025517

任何构建工具最重要的一个功能就是防止做重复工作。例如对于编译进程来说,如果已经执行了一次编译,那么就不需要再进行第二次,除非发生了一些会影响输出的操作,比如源代码改了或者输出被删掉了,编译会消耗很多时间,如果没必要去的情况下跳过这步就会节省很多时间。

Gradle是通过增量构建的特性来支持这个功能的,我们来详细了解一下

任务的输入输出

在通常情况下,任务接收一些输入然后产生一些输出。如果用编译的例子来讲,比如java的编译,它会接收一些源文件作为输入,然后产出class文件作为输出,还有一些输入,比如可以指定是否包含日志文件。
Gradle学习(十五)——增量构建
就像上图看到的一样,输入最重要的特征就是可以影响一个或者多个输出。依赖于源代码和源码所跑在的java运行时的版本都会影响字节码的生成,这些都算输入。但是比如memoryMaximumSize指定的编译时最大内存的大小是不会影响最终字节码生成的,如果按Gradle的术语,memoryMaximumSize应该叫做内部任务属性。

作为增量构建的一部分,Gradle会去检查输入和输出是否改变,如果没有改变它就认为任务是up-to-date,并且跳过任务的action。要注意的一点是,除非任务至少有一个输出,否则增量构建将不起作用。

这对构建者来说是非常容易的:你仅仅要做的就是告诉Gradle哪些是输入,哪些是输出。如果任务的一个属性可以影响输出,那么也需要将它设为输入。还要注意哪些不确定的任务,就是哪些相同输入都可能产生不同输出的任务,它们不应该被配置成增量构建。

以下是把任务属性作为输入的几种方法:

自定义任务类型

如果你写class实现了自定义任务,那么只需要两步就可以把它变成增量构建:

  1. 为你的每个输入输出通过getter方法创建类型属性
  2. 给这些属性加上适当的注解

Gradle主要支持三种输入输出

  • 简单类型
    比如字符串或者数字,大部分情况下,这些类型都要实现了Serializable接口
  • 文件系统类型
    包括标准的File类型,还有Gradle的FileCollection类型的派生类,还有那些可以作为参数传递给Project.file(java.lang.Object)Project.files(java.lang.Object[])这两个方法的任意类型
  • 内嵌类型
    内嵌类型不需要遵照其他两种类型,但是它有自己的输入输出属性,实际上任务的输入输出就内嵌在这些类型中。

想象下你有个任务需要处理各种各样的模板,比如FreeMarker, Velocity, Moustache等,他们接收模板源文件然后用一些模型数据组合起来形成模板文件的填充版本。

这个任务有三个输入一个输出:

  • 模板源文件
  • 数据模型
  • 模板引擎
  • 输出文件

官方的例子是用java实现的,代码量太冗余了,这里讲groovy实现的方式,前提是你已经有些groovy的基础,可以看得懂代码。
buildSrc/src/main/groovy/com/lastsweetop/tasks/ProcessTemplates.groovy文件

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
@Input
TemplateEngineType templateEngineType
@Nested
TemplateData templateData
@InputFiles
FileCollection sourceFiles
@OutputDirectory
File outputDir

@TaskAction
void processTemplates() {

}
}

buildSrc/src/main/groovy/com/lastsweetop/tasks/TemplateData.groovy文件

class TemplateData {
@Input
String name
@Input
Map<String, String> variables
}

buildSrc/src/main/groovy/com/lastsweetop/tasks/TemplateEngineType.groovy

enum TemplateEngineType {
FreeMarker,Velocity
}

build.gradle文件

task processTemplates(type: ProcessTemplates) {
templateEngineType TemplateEngineType.Velocity
templateData new TemplateData(name:'1',variables: [:])
sourceFiles files('src1')
outputDir file('dst')
}

执行任务:

± % gradle processTemplates
:processTemplates

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed

再次执行:

± % gradle processTemplates
:processTemplates UP-TO-DATE

BUILD SUCCESSFUL in 8s
1 actionable task: 1 up-to-date

我们来详细讲解下这个过程中的输入和输出:

  • templateEngineType
    代表填充模板的所使用的模板引擎的类型,比如FreeMarker,Velocity,你可以仅仅用一个字符串就实现,但我们这里为了提供更多的类型信息和安全性的考虑使用了枚举,因为枚举自动实现了Serializable接口,因此我们增加@Input注解可以把它作为简单类型使用,就像使用String一样
  • sourceFiles
    需要填充的模板源,可以是单个文件也可以是多个文件需要特殊的注解,在这里因为是输入,因此我们采用@InputFiles注解,我们稍后的列表中有更多面向文件的注解
  • templateData
    在这个例子中,我们使用了一个自定义的类来表示模型数据,但是它没有实现序列化接口,因此我们不能直接用@Input注解,但是没关系,templateData里面的两个属性一个字符串和一个map都实现了序列号接口,我们可以在这两个属性上增加@Input注解,然后在templateData上添加@Nested注解,告诉Gradle,这是一个内嵌输入类型,
  • outputDir
    表示输出文件的目录,和输入文件一样,也有各种各样的注解,这里是单独的输出目录,因此使用@OutputDirectory

这些注解的属性表示,如果模板引擎的类型,需要填充的模板源,模型数据和最后组合的模板和Gradle的上一次构建结果没有变化的话,本次构建就会跳过执行,这常常可以节省很多时间。

还有一点值得考虑,比如仅仅是一个源文件更改了,是否整个模板源都需要重新填充,答案是的,一个文件修改,全部都要重新构建,你可能觉得这个不合理啊,针对本篇所讲的例子确实如此,增量构建的范围就在此,如果想进一步那就是增量任务输入特性所做的事情了。

让我们来看看输入输出所用的所有注解和他们可以使用附加到的相应的属性

注解 预期属性类型 描述
@Input 任何序列化的类型 一个简单的输入值
@InputFile File * 一个单独的输入文件(非目录)
@InputDirectory File * 一个单独的输出文件(非文件)
@InputFiles Iterable<File>* 输入文件或者目录的迭代
@Classpath Iterable<File>* 表示java的classpath的输入文件或者目录的迭代。它允许任务忽略这个属性的不相干的改变,比如相同文件的不同名字,和属性的注解@PathSensitive(RELATIVE)类似,但是它会忽略附加给classpath的jar的名字,把顺序的改变视为classpath的改变,Gradle将检查jar文件的内容而忽视无关classpath的改变,比如文件的名字和日期。
@CompileClasspath Iterable<File>* 表示java的编译classpath的输入文件或者目录的迭代。它允许忽略不影响classpath中class的API的不相干的改变。可以忽略的改变种类如下:
更改jar或者*目录的路径
更改jar中实体的顺序和时间戳
改变resource和jar的manifests,包括增加或者移除
改变私有元素,比如私有属性,私有方法,私有内部类
修改代码,比如方法体,静态初始化或者字段初始化(不包括常量)
debug信息的改变,比如增加或者减少注释引起了debug信息的改变
目录更改,包括在jar内部的实体的目录
@OutputFile File * 一个单独的输出文件(非目录)
@OutputDirectory File * 一个单独的输出目录(非文件)
@OutputFiles Map<String, File>**
or Iterable<File>*
一个输出文件的迭代(非目录)只有Map时,这些食醋胡才能被缓存
@OutputDirectories Map<String, File>**
or Iterable<File>*
一个输出目录的迭代(非文件)只有是Map时,这些输出才能被缓存
@Destroys File or Iterable<File>* 指定一个或者多个文件需要移除,要注意的是任务只能在输入输出或者销毁两者选其一,但是不能两者都存在
@LocalState File or Iterable<File>* 指定一个或者多个文件来表示任务的本地状态,如果任务是从缓存中加载的,则这些文件将会被移除
@Nested 任意自定义类型 一个自定义类型,可以不实现序列号接口,但是必须有一个属性或者字段被添加了本表格中的任意一个注解,包括@Nested
@Console 任意类型 表示这个属性既不是输入也不是输出,仅仅影响控制台的输出信息,比如增加或减少任务的详细信息
@Internal 任意类型 表示这个属性内部使用,没有任何输入输出

*
事实上File可以是能被Project.file(java.lang.Object)方法接受的任意类型,Iterable<File>可以是能被Project.files(java.lang.Object[])方法接受的任意类型。它包括Callable的实例,比如closure,支持属性的惰性计算。要注意的是FileCollectionFileTreeIterable<File>
**
和上面类似,可以被Project.file(java.lang.Object)接受的任意类型,Map可以是Callable实例,比如是closure。

可以附加在之上注解之上的注解

注解 描述
@SkipWhenEmpty 如果@InputFiles@InputDirectory注解的文件列表或者目录是空的那么将跳过任务。所有带该注解的输入文件列表或者目录为空时才会跳过,会产生一个no source的任务结果
@Optional 可用于可选API文档中列出的任何属性类型注解。该注解禁止对相应属性进行验证检查。
@PathSensitive 可用于输入文件的任意属性,告诉Gradle只考虑文件的某些部分,比如注解为PathSensitivity.NAME_ONLY,如果只改变文件的路径,不改变文件内容将不会引起out-of-date

属性的注解是可以继承的,包括实现的接口的方式。子类可以重写父类的属性的注解,比如父类是@InputFile属性,那么子类可以更改为@InputDirectory属性。子类型在属性上的注解会覆盖父类的注解和实现接口的注解,父类的注解又优先于实现接口的注解。

ConsoleInternal是两个特别的注解,他们没有任何输入输出。主要用在使用Java Gradle Plugin Development plugin进行插件开发时来帮助你检查你的自定义任务是否添加必要的增量构建注解,防止你忘记。

使用classpath注解

@InputFiles外,与JVM相同的任务Gradle还理解classpath作为输入的概念,Gradle会分运行时和编译时两部分去检查更改。

@InputFiles不同,对于classpath属性来说,文件集合中顺序相当重要,但是classpath中jar的文件名和路径却可以忽略掉,包括jar内部实体的class和resource的时间戳和顺序也可以忽略掉。比如重新创建不同时间戳的jar并不会引起up-to-date

运行时的classpath可以标记为@Classpath,他们可以通过classpath的标准化进行进一步定制,我们后面会讲。

带有@CompileClasspath注解的输入属性将会被当做java的编译时classpath,除了前面的提到的那些,编译classpath还好忽略除了class文件外的所有更改,有些情况下class都改变了也不会影响任务的up-to-date,意味着只是更改的具体的实现,但是不影响编译。

运行时API

使用为自定义任务添加注解的方式转换成增量构建任务是很方便,但是有时候你没条件这样做,比如你访问不到自定义任务的源码,Gradle还提供了额外的api可以让任何任务都变成增量构建任务。

ad-hoc任务

运行时API通过一组适当的属性来提供,可以为每个task所用。

  • Task.getInputs() 类型是 TaskInputs
  • Task.getOutputs() 类型是 TaskOutputs
  • Task.getDestroyables() 类型是 TaskDestroyables

这些属性拥有一些方法允许你设置文件,目录或值来组成任务的输入输出,运行时API有时候和注解有很多相同的特性,但是不足是,不会去验证定义的目录是不是真的目录,定义的文件是不是真的文件。

之前的模板的例子,我们来看下如何改成使用运行时api的ad-hoc任务

task processTemplatesAdHoc {
inputs.property('engine',TemplateEngineType.FreeMarker)
inputs.files(fileTree('src1'))
inputs.property('templateData.name','1')
inputs.property('templateData.variables',[year:2017])
outputs.dir('dst1')
doLast {

}
}

然后执行任务:

± % gradle processTemplatesAdHoc
:processTemplatesAdHoc

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

首先,你应该编写自定义的任务还要添加各种属性,而在这个例子中没有任何需要存储源文件目录,输出目录和其他一些设置的地方,这是为了突出Ad-hoc任务并不需要任务附带任何状态,就可以实现和自定义任务在增量构建方面一样的效果。

所以的输入输出都是通过inputsoutputs上的方法进行定义,比如property(),files(),和dir()方法,Gradle会对这些参数进行up-to-date检查,来确定任务是否需要执行。每个方法都对应一个增量构建的注解,比如inputs.property()对应@Input,而 outputs.dir()对应@OutputDirectory.有一点不同的是file(),files(),dir()dirs()不会校验给定的路径是文件还是目录。

将会被移除掉文件列表就可以由destroyables.register()指定。

task removeTempDir {
destroyables.register("$projectDir/tempDir")
doLast {
delete("$projectDir/tempDir")
}
}

运行时API和注解最大的不同就是没有@Nested,这就是为什么需要两个property(),每个代表一个模板数据的属性,当然,还可以使用inputs.properties([name:'1',variables: [year: 2018]])这种方式来定义。和注解一样,只能在销毁和输入输出中二选一,不能一个任务同时设置两个。

为自定义任务添加运行时api

还有一种情况,就是为那些缺少相应注解的自定义任务添加输入输出的定义。比如,假设ProcessTemplatesNoAnnotations是第三方插件的任务,但是它不支持增量构建,为了给它添加增量构建,你就需要使用运行时API了
ProcessTemplatesNoAnnotations是去掉注解的ProcessTemplates

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplatesNoAnnotations extends DefaultTask {
TemplateEngineType templateEngineType
TemplateData templateData
FileCollection sourceFiles
File outputDir

@TaskAction
void processTemplates() {

}
}

对应的任务:

task processTemplatesRuntime(type: ProcessTemplatesNoAnnotations) {
inputs.property('engine', TemplateEngineType.FreeMarker)
inputs.files(fileTree('src1'))
inputs.properties([name: '2', variables: [year: 2018]])
outputs.dir('dst1')
}

执行任务:

± % gradle processTemplatesRuntime
:processTemplatesRuntime

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed

再次执行:

± % gradle processTemplatesRuntime
:processTemplatesRuntime UP-TO-DATE

BUILD SUCCESSFUL in 8s
1 actionable task: 1 up-to-date

使用运行时api有点像使用doLast和doFirst,都是为任务附加些什么,运行时API附加的是用于增量构建的输入输出信息。要注意的是如果自定义任务已经有增量构建的注解,那么运行时API所附加的输入输出是添加而不是替代。

进一步配置

运行时API的方法仅仅允许你添加输入输出,但是面向文件的输入输出将会返回一个TaskInputFilePropertyBuilder实例,基于这个实例你可以附加更多配置信息。

task processTemplatesRuntimeConf(type: ProcessTemplatesNoAnnotations) {
//...
inputs.files(fileTree('src1') {
include '**/*.fm'
}).skipWhenEmpty()
//...
}

执行任务

± % gradle processTemplatesRuntimeConf
:processTemplatesRuntimeConf NO-SOURCE

BUILD SUCCESSFUL in 0s

TaskInputs.files()返回的builder有个skipWhenEmpty(),调用她和给属性增加注解@SkipWhenEmpty一样的。

现在你已经掌握了注解和运行时API两种方法,你可以考虑自己喜欢使用哪种了。不过我推荐尽量用注解,实在无法使用注解的情况下再考虑使用运行时API。

定义任务输入输出的额外福利

一旦你定义了任务的输入输出,Gradle就会去推断这些属性。比如,一个任务的输出正好是一个任务的输入,这样是不是会产生依赖关系,不用担心,Gradle帮你搞定。
我们来看下其他的一些好玩的特性。

推断任务依赖

想下一个归档任务可以打包processTemplates任务的输出,构建者可能会想这两者有依赖关系,于是显式的声明了依赖,其实大可不必,你可以使用下面的方法:

task packageFiles(type: Zip) {
from processTemplates.outputs
}

执行任务:

± % gradle packageFiles
:processTemplates
:packageFiles

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Gradle将自己可以推断出packageFiles任务依赖于processTemplates任务。因为packageFiles任务需要processTemplates任务的输出作为输入,我们把这个叫推断任务依赖。

输入输出校验

增量构建的注解为Gradle提供了足够的信息,一遍Gradle可以为这些被注解的属性提供一些基础的校验,这些校验是在任务执行之前进行的,详细如下所示:

  • @InputFile 校验属性值是否是个正确的文件路径(而不是目录)并且存在
  • @InputDirectory 校验属性值是否是个正确的目录路径(而不是文件)并且存在
  • @OutputDirectory 校验属性值是否不是一个文件,并且如果不存在就创建

这些校验提高了构建的健壮性,可以帮你快速找到输入输出的错误信息。

有时候你想禁用掉这些校验,那么你就可以使用@Optional注解,它会告诉Gradle这个属性是可选的,不需要进行校验。

持续构建

定义任务输入输出的另一个福利就是持续构建,它可以知道任务依赖于哪些文件,一旦这些文件发生变化,那么任务就会自动执行。运行时加上--continuous或者-t选型,Gradle就会不断的检查是否更新,如果更新则执行任务。

任务并行化

定义任务的输入输出的最后一个福利就是,当你给任务增加--parallel选项是,任务利用这些信息可以知道应该这么执行。比如,Gradle在执行下一个任务时,会检查所有任务的输出防止并行的任务写入相同的文件目录。还有,Gradle可以知道哪些任务在销毁文件,这样可以避免其他并行任务使用这些文件,或者正在写入这些文件。如果一个任务创建了一些文件,而另一个任务正在基于这些文件运行,它也可以防止其他任务来销毁这些文件。通过定义任务的输入和输出提供的信息,Gradle创建/消费/销毁之间的关系,防止任务并行时违反这种关系规则。

工作原理

在任务执行第一次之前,Gradle会获取任务输入的快照,这个快照包括每个输入文件的路径和内容构成的hash,然后在任务执行之后,Gradle获取输出的快照,这些快照包括每个输出文件的路径和内容构成的hash。Gradle会保存这个两个快照在下一次任务执行时使用。

在此之后,任务的每一次执行,Gradle都会获取输入输出新的快照。如果新的快照和上一次的相同,Gradle就假定这些任务是up-to-date并且跳过任务,如果不同,就会执行任务,Gradle会为下一次保留快照。

Gradle还好将任务的代码作为任务输入的一部分,当任务的action或者依赖发生并会,Gradle也会任务这个任务过期了,需要再次执行。

Gradle还会考虑文件属性是否对顺序是敏感的,比如java的classpath。如果这类属性的快照发生改变,比如更改了文件的顺序,那么也会认为是过期的,任务需要再次执行。

如果一个任务指定了输出目录,那么往这个目录中增加了一个文件,也不会被认为是过期的,因为这对任务的输出来说是不相干,任务的输出目录是可以共享的,如果你不想这样的话,你可以考虑使用TaskOutputs.upToDateWhen(groovy.lang.Closure)

当构建缓存激活时,任务的输出还可以被计算为构建缓存的key,用于从缓存中获取构建的输出

进阶技术

之前所讲的已经涵盖了增量构建的大部分内容,但有时候你需要一些特殊的处理,我们下面就来讲这些进阶的技术

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

你可能已经想知道Copy任务的from()方法是如何工作的,它并没有@InputFiles注解,但是传给他的文件都被当做了输入,而且还支持增量构建。让我们来解密一下:

实现这个也相当的简单,其实还是老办法,只是增加了api而已。自己写个方法然后往已经添加注解的属性值里添加输入即可。我们可以之前的例子添加sources()方法:

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
@Input
TemplateEngineType templateEngineType
@Nested
TemplateData templateData
@InputFiles
FileCollection sourceFiles = getProject().files()
@OutputDirectory
File outputDir

void sources(FileCollection fileCollection) {
sourceFiles += fileCollection
}

@TaskAction
void processTemplates() {

}
}

添加任务:

task processTemplatesOwn(type: ProcessTemplates) {
templateEngineType TemplateEngineType.Velocity
templateData new TemplateData(name: '1', variables: [:])
sources files('src1')
outputDir file('dst')
}

执行任务:

± % gradle processTemplatesOwn
:processTemplatesOwn

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

也就是说,只要在配置阶段你可以任意添加值或者文件到输入输出之中,无论你在哪添加都会被当做输入输出。
如果你想将一个任务的输出添加到进来也是可以的,你需要使用project.files()方法

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class ProcessTemplates extends DefaultTask {
@Input
TemplateEngineType templateEngineType
@Nested
TemplateData templateData
@InputFiles
FileCollection sourceFiles = getProject().files()
@OutputDirectory
File outputDir

void sources(FileCollection fileCollection) {
sourceFiles += fileCollection
}

void sources(Task inputTask) {
sourceFiles += getProject().files(inputTask)
}


@TaskAction
void processTemplates() {

}
}

添加任务:

task processTemplatesFromTask(type: ProcessTemplates) {
templateEngineType TemplateEngineType.Velocity
templateData new TemplateData(name: '1', variables: [:])
sources copyTemplate
outputDir file('dst')
}

执行任务:

± % gradle processTemplatesFromTask
:processTemplatesFromTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

这种技术可以让你的自定义任务更容易使用,构建文件也可以很简洁。使用getProject().files()可以自定义任务的内部依赖关系。

最后要提醒的是:如果你需要创建一个获取源文件作为输入的任务,你可以考虑试试内置的SourceTask任务,

链接@OutputDirectory@InputFiles

当你想把一个任务的输出链接到另一个任务的输入时,类型往往是匹配的,非常容易建立这种链接。比如File类型的输出和File类型的输入。

但是如果你想要@OutputDirectory注解的属性(File类型)的输出,链接成另一个任务@InputFiles注解的属性(FileCollection类型)作为输入时,这种链接就不起作用了。

我们来看一个例子,利用java编译任务的输出,通过destinationDir属性作为另一个任务Instrument的输入时.Instrument任务是基于java字节码文件的工具,任务有个输入属性classFiles,注解为@InputFiles。示例如下:

@Builder(builderStrategy = SimpleStrategy, prefix = '')
class Instrument extends DefaultTask {
@InputFiles
FileCollection classFiles
@OutputDirectory
File destinationDir

@TaskAction
void action(){

}
}

定义任务:

task badInstrumentClasses(type: Instrument) {
classFiles fileTree(compileJava.destinationDir)
destinationDir file("$buildDir/instrument")
}

执行任务:

± % gradle badInstrumentClasses
:badInstrumentClasses

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

在代码层面没有什么明显的问题,但是任务执行的时候你发现java编译任务没有执行,在这种情况下你需要显示得通过dependsOnbadInstrumentClasses任务添加compileJava任务的依赖,使用fileTree()意味着Gradle无法推断他们之间的依赖关系

有种解决方案就是使用TaskOutputs.files属性

task instrumentClasses(type: Instrument) {
classFiles compileJava.outputs.files
destinationDir file("$buildDir/instrument")
}

执行任务:

± % gradle instrumentClasses
:compileJava
:instrumentClasses

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

还有一种方法就是使用project.files()代替project.fileTree(),示例如下:

task instrumentClasses2(type: Instrument) {
classFiles files(compileJava)
destinationDir file("$buildDir/instrument")
}

执行任务:

± % gradle instrumentClasses2
:compileJava
:instrumentClasses2

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

files()可以接收Task作为参数,而fileTree()不可以

这两种方法的缺点就是把任务输出里的所有文件当做另一个任务的输入了,如果仅仅是一种输出文件那是没问题的,比如JavaCompile任务,但是有时候你只想将多个输出中的一个连接到其他任务作为输入,那可以使用buildBy来指定

task instrumentClassesBuiltBy(type: Instrument) {
classFiles fileTree(compileJava.destinationDir) {
builtBy compileJava
}

destinationDir file("$buildDir/instrument")
}

自定义up-to-date逻辑

Gradle会自动检查任务输出的文件和目录,但是如果任务输出是其他的一些东西怎么办,比如也许是对webservice的更新,或者对数据库表的更新,Gradle是无法检测到任务是否up-to-date的。

还好有个TaskOutputsupToDateWhen()方法,这个方法可以接收predicate函数,用于检测任务是否是up-to-date的,示例如下:

task alwaysInstrumentClasses(type: Instrument) {
classFiles fileTree(compileJava.destinationDir) {
builtBy compileJava
}
destinationDir file("$buildDir/instrument")
outputs.upToDateWhen {
false
}
}

执行任务:

± % gradle alwaysInstrumentClasses
:compileJava
:alwaysInstrumentClasses

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

然后再次执行

± % gradle alwaysInstrumentClasses
:compileJava UP-TO-DATE
:alwaysInstrumentClasses

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

闭包{ false }总是认为alwaysInstrumentClasses任务要重新执行,不管输入输出是否发生了变化。

当然你可以把更复杂的逻辑写在这个闭包中,比如判断数据库的某条记录是否存在,或者是否更改过。注意up-to-date检查的目的是为了节省时间,如果检查本身就非常耗费时间,那就没必要添加这些检查了。

常见的错误就是使用upToDateWhen()代替onlyIf(),记住,在于输入输出无关的情况下使用onlyIf(),与输入输出有关的使用upToDateWhen()

输入的标准化配置

对于up-to-date检查和构建缓存,Gradle都需要来判断两次任务的输入是否一致,为了达到这个目的,Gradle首先要规范化输入然后再比较两者。对于编译时classpath来说,Gradle会从classpath的class文件中提取ABI签名然后在比较两次任务的签名。

为实现运行时classpath的规范化,可以定制Gradle的内置策略。所有带@Classpath注解的输入都可以被当做运行时classpath。

如果你想在你所有的jar中添加一个build-info.properties文件,这个文件包含了一些构建的信息,比如构建开始的时间戳,用于发布工件的持续集成任务的ID,这个文件仅仅是为了审查,对运行测试没有任何影响。尽管如此,如果有了这个文件,test任务将永远不会过期,也不会从构建缓存中拉去结果,为了还能使用增量构建和构建缓存,你可以使用Project.normalization(org.gradle.api.Action)方法来告诉Gradle在运行时classath中忽略这个文件。

normalization {
runtimeClasspath {
ignore 'build-info.properties'
}

}

这样配置的效果就是在进行up-to-date检查和构建缓存计算key时,build-info.properties文件的更改将会被忽略掉,而且不会改变test任务运行时的操作,test任务仍然会加载build-info.properties文件,运行时的classpath和以前还是一样

过期的任务输出

当Gradle的版本发生变化时,基于原来版本的任务输出都会被移除掉,以便所有的任务可以基于当前版本的,使得所有任务的环境一致。