Gradle 1.12用户指南翻译——第五十七章. 编写自定义任务类

其他章节的翻译请参见:
http://blog.csdn.net/column/details/gradle-translation.html
翻译项目请关注Github上的地址:
https://github.com/msdx/gradledoc
本文翻译所在分支:
https://github.com/msdx/gradledoc/tree/1.12。
直接浏览双语版的文档请访问:
http://gradledoc.qiniudn.com/1.12/userguide/userguide.html。
另外,Android 手机用户可通过我写的一个程序浏览文档,带缓存功能的,目前0.6开发中版本兼容 Android 2.3以上系统,项目地址如下:
https://github.com/msdx/gradle-doc-apk
翻译不易,转载请注明本文在CSDN博客上的出处:
http://blog.csdn.net/maosidiaoxian/article/details/70341369

关于我对Gradle的翻译,以Github上的项目及http://gradledoc.qiniudn.com 上的文档为准。如发现翻译有误的地方,将首先在以上两个地方更新。因时间精力问题,博客中发表的译文基本不会同步修改。

另外,目前Gradle1.12版本的文档已经翻译完并进入校稿阶段,校稿的方式为到该项目https://github.com/msdx/gradledoc 提交issue或是pull request。校稿的结果不只是在此版本更新,也会用于改善Gradle下一版本(2.0)文档的翻译。


第五十七章. 编写自定义任务类

Gradle 支持两种类型的任务。一种是简单的任务,你可以使用一个action闭包来定义它。这样的任务我们在第六章, 构建脚本基础已经看到过。对于这种类型的任务,action闭包决定了这个任务的行为。这个类型的任务适合于实现在构建脚本中只执行一次的任务。

另一种类型的任务是增强型的任务,它的任务内置在任务中,并且这个任务提供了一些属性能够让你用于配置其行为。我们在 第十五章 有关任务的详细信息中看到过它们。大部分的Gradle插件都使用增强型的任务。使用这些增强型的任务,你不需要像使用简单任务一样去实现这个任务的行为。你只需要简单地声明这个任务,并用它的属性来配置它。通过这种方式,增强型的任务能够让你在许多不同的地方,甚至是跨越不同的构建,利用同样的行为。

一个增强型的任务的行为和属性是通过这个任务的类定义的。当你定义一个增强型任务时,你要定义这个任务的类型或者是类。

在Gradle中实现你的自定义任务类并不难。你可以使用你喜欢的几乎任何一种语言来实现一个自定义任务类,只要它最终能够提供编译的字节码。在我们的例子中,我们将使用Groovy来作为实现的语言,但你也可以使用Java或者是Scala。一般情况下,使用Groovy是最简单的选择,因为Gradle API就是被设计为能使用Groovy良好地执行。

57.1. 封装一个任务类

有几个地方可以让你放任务类的源码。

构建脚本

你可以在构建脚本中直接包含这个任务类。这样做的好处是,你不需要再做什么,这个任务类就能够被自动地编译并且包含到这个构建脚本的类路径当中。然而,在这个构建脚本脚本之外,这个任务类是不可见的,因此你不能够在你定义这个任务类的脚本之外的地方来复用它。

buildSrc 项目

你可以把任务类的源码放在 rootProjectDir/buildSrc/src/main/groovy 目录中。Gradle将会编译和测试这个任务类,并且使它在构建脚本的类路径中可用。这个任务类在该构建所使用的每一个构建脚本当中都是可见的。然而,它在这个构建之外并不可见,因为你不能在定义它的这个构建之外的其他地方来重用这个任务类。使用 buildSrc 项目的方法能够保持任务的声明——即它应该做什么,与这个任务的实现——即它是怎么做的,相互独立。

有关buildSrc 项目的更详细信息,请参阅 第五十九章, 组织构建逻辑 。

独立项目

你可以为你的任务类创建一个独立的项目。这个项目会输出和发布一个JAR文件,然后你可以在多个构建中使用,并且分享出去。一般来说,这个JAR可能包含一些自定义的插件,或者是捆绑几个相关的任务类到一个单独的库当中。或者是上面两者都有。

在我们的例子中,为了简单,我们将从在构建脚本中定义任务类开始。然后我们会看看创建一个单独的项目的方式。

57.2. 编写一个简单的任务类

要实现一个自定义任务类,你需要继承DefaultTask

示例 57.1. 定义一个自定义任务

build.gradle

class GreetingTask extends DefaultTask {
}

这个任务没有进行任何有用的操作,所以让我们来添加一些行为。要添加一些行为,我们需要添加一个方法到这个任务中,并且使用 TaskAction 注解来标记它。当任务执行的时候,Gradle 就会调用这个方法。你不需要使用一个方法来定义这个任务的行为。你能够,例如,在构建方法中调用doFirst() 或者 doLast() 并传入一个闭包来添加行为。

示例 57.2. 一个hello world 任务

build.gradle

task hello(type: GreetingTask)

class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}

gradle -q hello的输出结果

> gradle -q hello
hello from GreetingTask

让我们添加一个属性到这个任务中,这样我们就可以自定义它。任务是简单的POGOs,当你声明一个任务时,你可以在这个任务对象上设置属性或者是调用方法。这里我们添加了一个 greeting 属性,并且在我们定义 greeting 任务的时候设置它的值。

示例 57.3. 一个自定义的hello world任务

build.gradle

// Use the default greeting
task hello(type: GreetingTask)

// Customize the greeting
task greeting(type: GreetingTask) {
    greeting = 'greetings from GreetingTask'
}

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

gradle -q hello greeting的输出结果

> gradle -q hello greeting
hello from GreetingTask
greetings from GreetingTask

57.3. 一个独立项目

现在我们将移动我们的任务到一个单独的项目中,这样我们就可以发布它,并与他人分享。这个项目只是一个简单的Groovy项目,它将产生一个包含任务类的JAR包。下面是该项目的一个简单的构建脚本。它配置使用Groovy插件,并且添加Gradle API 作为编译时依赖。

示例 57.4. 一个自定义任务的构建

build.gradle

apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

注意: 此例子的代码可以在Gradle的二进制文件或源码中的 samples/customPlugin/plugin 里看到。

我们只是按照约定将任务类的源码放在对应的位置。.

示例 57.5. 一个自定义任务

src/main/groovy/org/gradle/GreetingTask.groovy

package org.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}

57.3.1. 在另一个项目中使用你的任务类

想在一个构建脚本中使用一个任务类,你需要把这个类添加到构建脚本的类路径中。要做到这一点,你要使用在第 59.5 节,“构建脚本的外部依赖”中描述的 buildscript { } 块。下面的示例展示了当包含任务类的JAR文件已经被发布到一个本地仓库时,你可以怎么做。

示例 57.6. 在另一个项目中使用一个自定义任务

build.gradle

buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
    }
    dependencies {
        classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT'
    }
}

task greeting(type: org.gradle.GreetingTask) {
    greeting = 'howdy!'
}

57.3.2. 编写你的任务类的测试

当你要测试你的任务类时,你可以使用ProjectBuilder 类来创建 Project 实例去使用你的任务类。

示例 57.7. 测试一个自定义任务

src/test/groovy/org/gradle/GreetingTaskTest.groovy

class GreetingTaskTest {
    @Test
    public void canAddTaskToProject() {
        Project project = ProjectBuilder.builder().build()
        def task = project.task('greeting', type: GreetingTask)
        assertTrue(task instanceof GreetingTask)
    }
}

57.4. 增量任务

增量任务还是一个孵化中 的功能。

从上面所述的实现的引入(早在Gradle 1.6 发布周期) 以来,Gradle社区的讨论中就产生了一些优秀的想法,有关暴露更改的信息到下面所描述的内容的任务实现者。因此,这个功能的API几乎可以肯定将在即将推出的版本中。然而,请使用当前的实现做实验,并且与Gradle 社区分享你的经验。

这个功能孵化过程,是Gradle 功能生命周期的一部分(参见 附录 C, 功能的生命周期),它存在的目的是为了通过早期用户反馈内容的合并来保证高质量的最终实现。

通过Gradle,实现一个当输入和输出都是up to date时自动跳过的任务是非常轻松的(参见第 15.9节,“跳过 up-to-date 的任务”)。然而,有时候从上次执行以来,只有少量输入文件被修改了,而你想要避免重新处理所有未修改的输入。对于将输入文件按1:1的基础转换为输出文件的转换任务,这将会特别有用。

如果你想优化你的构建,以便只有out-of-date的输入被处理,你可以使用一个增量任务来做。

57.4.1. 实现增量任务

对于一个要增量处理输入的任务,它必须包含一个 增量任务操作。这是一个任务操作方法,它包含了一个简单的 IncrementalTaskInputs 参数,该参数提示了Gradle 这个操作将只处理那些更改的输入。

这个增量任务操作可能提供了一个IncrementalTaskInputs.outOfDate() 操作,用于处理所有out-of-date的输入文件,以及一个IncrementalTaskInputs.removed()操作,对任何从前一次执行开始已经被删除的文件执行。

示例 57.8. 定义增量任务操作

build.gradle

class IncrementalReverseTask extends DefaultTask {
    @InputDirectory
    def File inputDir

    @OutputDirectory
    def File outputDir

    @Input
    def inputProperty

    @TaskAction
    void execute(IncrementalTaskInputs inputs) {
        println inputs.incremental ?"CHANGED inputs considered out of date" : "ALL inputs considered out of date"
        inputs.outOfDate { change ->
            println "out of date: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.text = change.file.text.reverse()
        }

        inputs.removed { change ->
            println "removed: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.delete()
        }
    }
}

注︰ 此示例的代码可以在Gradle 的二进制及源码分发包的samples/userguide/tasks/incrementalTask中找到。

对于像这样的简单转换任务,任务操作只需要对任何out-of-date的输入生成输出的文件,并对任何已移除的输入删除对应的输出文件。

一个任务可能只包含一个简单的增量任务操作。

57.4.2. 哪些输入是被视为out-of-date的?

当Gradle有了前一次任务执行的历史,并且从那一次执行开始任务执行的上下文中只有输入文件有更改,那么Gradle 就能够决定哪些输入文件需要被这个任务重新处理。在这种情况下,IncrementalTaskInputs.outOfDate() 操作会对任何新加入的 或者是 已修改的输入文件执行,并且IncrementalTaskInputs.removed()操作会对任何被移除的输入文件执行。

然而,有许多情况下,Gradle还是无法确定哪些输入文件需要被重新处理。比如包括:

  • 从前一次执行以来,没有有效的历史。
  • 你使用一个不同版本的Gradle进行构建。当前,Gradle没有使用来自不同版本的任务历史。
  • 一个添加到任务的 upToDateWhen 标准返回 false
  • 从前一次执行开始,一个输入属性已经被更改。
  • 自前一次执行开始,有一个或多个的文件已经被更改。

满足以上情况的任何一个,Gradle都会考虑使所有的输入文件为outOfDate。然后会为每一个输入文件执行 IncrementalTaskInputs.outOfDate()操作,并且不再执行IncrementalTaskInputs.removed()操作。

你可以通过 IncrementalTaskInputs.isIncremental()检查Gradle是否能够对输入的文件确定增量更改。

57.4.3. 一个增量任务的操作

给定 上面的增量任务实现,我们可以通过示例探讨中种变化场景。注意不同变化的任务(“updateInputs”,“removeInput”等)都是出于演示目的而存在:它们通常不会在构建脚本中。

首先,考虑到 IncrementalReverseTask第一次针对一组输入执行。在这种情况下,所有的输入都被认为是“out of date”:

示例 57.9. 第一次运行增量任务

build.gradle

task incrementalReverse(type: IncrementalReverseTask) {
    inputDir = file('inputs')
    outputDir = file("$buildDir/outputs")
    inputProperty = project.properties['taskInputProperty'] ?: "original"
}

构建布局

incrementalTask/
  build.gradle
  inputs/
    1.txt
    2.txt
    3.txt

gradle -q incrementalReverse的输出结果

> gradle -q incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt

当然,对于没有任何修改的情况下执行任务时,该任务本身是up to date的,并且不会有文件上报到这个任务操作中:

示例 57.10. 在输入不变时运行增量任务

gradle -q incrementalReverse的输出结果

> gradle -q incrementalReverse

当以某种方式修改了一个输入文件,或者新增了一个输入文件时,重新执行这个任务会使这些文件被上报到IncrementalTaskInputs.outOfDate()

示例 57.11. 有输入文件更新时运行增量任务

build.gradle

task updateInputs() << {
    file('inputs/1.txt').text = "Changed content for existing file 1."
    file('inputs/4.txt').text = "Content for new file 4."
}

gradle -q updateInputs incrementalReverse的输出结果

> gradle -q updateInputs incrementalReverse
CHANGED inputs considered out of date
out of date: 1.txt
out of date: 4.txt

When an existing input file is removed, then re-executing the task results that file being reported to IncrementalTaskInputs.removed():

示例 57.12. 当一个输入文件被删除时运行增量任务

build.gradle

task removeInput() << {
    file('inputs/3.txt').delete()
}

gradle -q removeInput incrementalReverse的输出结果

> gradle -q removeInput incrementalReverse
CHANGED inputs considered out of date
removed: 3.txt

当输出文件被删除(或更改)时,Gradle是无法确定哪一个输入文件是out of date的。在这种情况下,所有的输入文件都会被报告到 IncrementalTaskInputs.outOfDate()操作中,并且没有文件会被上报到 IncrementalTaskInputs.removed() 操作:

示例 57.13. 当一个输出文件被删除时运行增量任务

build.gradle

task removeOutput() << {
    file("$buildDir/outputs/1.txt").delete()
}

gradle -q removeOutput incrementalReverse的输出结果

> gradle -q removeOutput incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt

当一个任务的输入属性被修改时,Gradle 无法确定这个属性是如何影响到这个任务的输出的,因此所有的输入文件都会被认为是out of date的。所以类似于更改输出文件的例子,所有的输入文件都会被报告到 IncrementalTaskInputs.outOfDate()操作中,并且没有文件会被上报到IncrementalTaskInputs.removed() 操作:

示例 57.14. 当一个输入属性被更改时运行增量任务

gradle -q -PtaskInputProperty=changed incrementalReverse的输出结果

> gradle -q -PtaskInputProperty=changed incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt

你可能感兴趣的:(Gradle 1.12用户指南翻译——第五十七章. 编写自定义任务类)