Gradle学习(十四)——任务详解
之前的文章已经将了一下任务的基础知识,包括创建简单的任务,以及任务的依赖,本文将进一步的讲解一些任务的详细知识
任务结果
当Gradle执行任务时,就会因为不同的结果给这些任务打上不同的标签,这些标签表示任务是否有动作需要执行,任务是否应该执行了动作,任务是否执行了这些动作,这些动作是否发生了变化。
结果标签 | 结果描述 | 产生结果的前提 |
---|---|---|
没有标签或者EXECUTED | 任务执行了动作 | 1.任务有动作,Gradle也确定该动作必须被执行 2.任务没有动作但是有依赖,这些依赖都要被执行 |
UP-TO-DATE | 任务输出没有改变 | 1.当任务有输入输出但是没有变化 2当任务有action,但是任务告诉Gradle输出没有变化 3.当任务没有动作,而是有依赖,但是所有的依赖都是UP-TO-DATE,SKIP或者从缓存取出的 4.任务没有动作也没有依赖 |
FROM-CACHE | 任务输出可以在之前的执行缓存中被找到 | 任务在构建缓冲中可以找到相应的输出 |
SKIPPED | 任务没有执行动作 | 1.在命令行显示指定了被跳过 2.任务的onlyIf判断返回false |
NO-SOURCE | 任务不需要执行动作 | 任务有输入输出,但是没有sources,比如JavaCompile任务的source就是.java文件 |
定义任务
之前的章节我们已经学会如何使用keyword方式定义任务,还有一些别的方式来定义任务,因为keyword方式不支持表达式。
task(hello) {
doLast {
println 'hello'
}
}
task(copy, type: Copy) {
from file('src')
into buildDir
}
你也可以通过string来定义任务
task('hello') {
doLast {
println 'hello'
}
}
task('copy', type: Copy) {
from file('src')
into buildDir
}
还可以通过tasks来创建任务
tasks.create('hello') {
doLast {
println 'hello'
}
}
tasks.create(name: 'copy', type: Copy) {
from file('src')
into buildDir
}
检索任务
你有时候需要检索在构建文件中定义的任务,比如对任务进行配置或者添加依赖,有很多种方式可以检索。
首先,所有的任务对project对象来说就是可用属性,你可以像使用属性一样来检索这些任务。
task hello
println hello.name
println project.hello.name
也可以通过tasks集合来检索
task hello
println tasks.hello.name
println tasks['hello'].name
也可以通过tasks.getByPath()
来访问任意工程的任务,可以使用任务名,相对路径,绝对路径来调用getByPath()
方法。
task hello
project(':projectA') {
task hello
}
println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path
然后执行hello任务:
:hello
:hello
:projectA:hello
:projectA:hello
配置任务
我们从一个copy任务的示例来看如何配置Gradle的任务
task myCopy(type:Copy)
这是一个没有任何默认行为的copy任务,我们有各种各样的方法可以配置它。
myCopy.from 'resource'
myCopy.into 'build'
myCopy.include '**/*.txt', '**/*.xml', '**/*.properties'
首先就是最普通的方式,像配置java或者groovy对象那样调用方法进行配置,myCopy
需要重复好多次,可读性并不好。
还有一种方法可以不用重复myCopy
,看起来可读性更好些:
myCopy {
from 'resource'
into 'build'
include '**/*.txt', '**/*.xml', '**/*.properties'
}
这种方式其实是调用了tasks.getByName(String var1, Closure var2)
方法,你看到的是它的缩写,这个方法完成了使用Closure
对任务进行了配置。
你也可以在定义的时候就进行配置:
task myCopy(type:Copy) {
from 'resource'
into 'build'
include '**/*.txt', '**/*.xml', '**/*.properties'
}
给任务添加依赖
有各种各样的方式可以给任务添加依赖。常用的是通过任务名进行添加依赖,不仅可以在相同项目中可以通过任务名添加依赖,也可以添加其他项目的任务作为依赖。添加其他项目的任务作为依赖时,必须要把其他工程的项目名作为前缀,下面是个跨项目依赖的例子:
project(':projectA') {
task taskX(dependsOn:':projectB:taskY') {
doLast {
println it.name
}
}
}
project(':projectB') {
task taskY {
doLast {
println it.name
}
}
}
然后执行taskX任务:
% gradle taskX -q
taskY
taskX
除了使用任务名外,你还可以通过Task
对象进行添加依赖:
task taskX {
doLast {
println it.name
}
}
task taskY {
doLast {
println it.name
}
}
taskX.dependsOn taskY
然后执行taskX任务:
% gradle taskX -q
taskY
taskX
再扩展一下,你甚至可以定义闭包作为依赖,这个闭包应该返回一个task或者多个task列表,这些task会被当做任务的依赖,示例如下:
task taskX {
doLast {
println it.name
}
}
taskX.dependsOn {
tasks.findAll {
it.name.startsWith('lib')
}
}
task lib1 {
doLast {
println it.name
}
}
task lib2 {
doLast {
println it.name
}
}
然后执行taskX任务:
± % gradle taskX -q
lib1
lib2
taskX
任务排序
控制两个任务的执行顺序而不是显示的声明他们之间的依赖关系,在某些用例下它是非常有用的。任务排序和任务依赖的不同在于,任务排序不会影响哪个任务将被执行,仅仅只是影响将被任务的排序。
任务排序适用的场景:
- 强制任务的执行顺序:比如build必须在clean任务后面执行
- 在build之后才执行校验:比如需要在release构建之后才进行认证校验
- 在长校验之前先进行短校验:比如先单元测试再集成测试
- 一个任务是其他所有任务的总结:比如测试报告任务需要其他测试任务都做完的情况下
有两个排序规则:must run after
和should run after
当你运行must run after
规则时,taskB.mustRunAfter(taskA)
表示taskB和taskA同事运行时,taskB必然在taskA之后运行。
而should run after
就没那么严格了,它在两种情况下就会被忽略掉:
第一种情况就是任务排序构成了循环,比如taskB.shouldRunAfter taskA
之后又taskA.shouldRunAfter taskB
。
第二种情况声明的依赖和should run after
的顺序不一样,should run after
就会被忽略掉
使用should run after
应该在有更好,没有也行的情况下才使用。
看下面的例子:
task taskA {
doLast {
println it.name
}
}
task taskB {
doLast {
println it.name
}
}
taskB.mustRunAfter taskA
然后执行taskA和taskB任务:
± % gradle taskB taskA -q
taskA
taskB
should run after
的情况:
task taskA {
doLast {
println it.name
}
}
task taskB {
doLast {
println it.name
}
}
taskB.shouldRunAfter taskA
然后执行taskA和taskB任务:
± % gradle taskB taskA -q
taskA
taskB
上面的例子中,如果单独执行taskB任务:
± % gradle taskB -q
taskB
可以看到taskA任务并没有执行
如果有--continue
选项,如果A失败B也会继续执行。
task taskA {
doLast {
throw new Exception()
println it.name
}
}
task taskB {
doLast {
println it.name
}
}
taskB.shouldRunAfter taskA
然后执行taskA和taskB任务:
± % gradle --continue -q taskA taskB
taskB
FAILURE: Build failed with an exception
···
和依赖关系冲突的情况下,shouldRunAfter
会被忽略
task taskA {
doLast {
println it.name
}
}
task taskB {
doLast {
println it.name
}
}
taskA.dependsOn taskB
taskB.shouldRunAfter taskA
然后执行
± % gradle -q taskA taskB
taskB
taskA
如果是mustRunAfter
和依赖冲突,那么就会直接报错
任务描述
你可以为任务增加描述信息,他会在gradle tasks
显示出来。
task myCopy(type: Copy) {
description 'Copy the resource directory to the build directory'
from 'resource'
into 'build'
include '**/*.txt', '**/*.xml', '**/*.properties'
}
替代任务
有时候你需要替代任务,必须你想自定义java插件的某个任务,可以参照下面这个例子:
task copy(type:Copy){
doLast {
println 'copy1'
}
}
task copy(overwrite: true) {
doLast {
println 'copy2'
}
}
任务输出:
± % gradle copy -q
copy2
如果直接定义名字相同的任务,Gradle会抛出任务已经被定义的异常,因此你需要增加overwrite:true
的设置。
跳过任务
Gradle提供了多种方法可以跳过任务的执行
使用predicate
你可以用onlyIf()
方法给任务附加一个predicate,只有当predicate为true时,任务的动作才会执行.predicate可以用closure去实现,closure将会作为一个参数传入到OnlyIf方法中,只有当闭包返回true,任务的action才会执行,否则将会跳过,示例如下:
task hello {
doLast {
println 'hello world'
}
}
hello.onlyIf {
!project.hasProperty('skipHello')
}
然后执行任务:
± % gradle hello -PskipHello -q
任务跳过,没有任何输出
使用StopExecutionException
如果跳过的逻辑很难用predicate协程表达式,那么可以考虑下StopExecutionException,这个异常可以通过一个action抛出来,然后该任务之后的action都会跳过,但是会执行下一个任务:
task compile {
doLast {
println 'compile'
}
}
compile.doFirst {
if(true){
throw new StopExecutionException()
}
}
task myTask(dependsOn: compile) {
doLast {
println 'myTask'
}
}
然后执行任务:
± % gradle myTask -q
myTask
这个特性非常有用,因为它的力度是基于action的,灵活使用可以写出很多有趣的代码
启用禁用任务
每个任务的enabled
标签默认都是true,如果设为false,它所以的action都不会被执行,任务结果也会变成SKIPPED
task disabledMe {
doLast {
println '反正写什么也不会被执行'
}
}
disabledMe.enabled = false
然后执行任务:
± % gradle disabledMe -q
任务规则
有时候你需要任务可以包含无穷的参数,而你不想为每个都单独写一个task的话,你可以试试任务规则:
tasks.addRule('Pattern:ping<ID>') { String taskName ->
if (taskName.startsWith('ping')) {
task(taskName) {
doLast {
println "Panding: " + (taskName - 'ping')
}
}
}
}
你可以这样执行任务:
± % gradle pingServer1 -q
Panding: Server1
addRule
的第一个参数是描述参数,用于gradle tasks
的显示,在命令行下可以看到,目前idea的gradle插件还不支持。
任务规则不仅仅可以在命令行下调用,而且还可以与其他规则建立以来关系
tasks.addRule('Pattern:ping<ID>') { String taskName ->
if (taskName.startsWith('ping')) {
task(taskName) {
doLast {
println "Panding: " + (taskName - 'ping')
}
}
}
}
task groupPing {
dependsOn pingServer1,pingServer2
}
然后执行任务:
± % gradle groupPing -q
Panding: Server1
Panding: Server2
Finalizer任务
被终结的任务执行后,Finalizer任务必然会跟着执行
task taskC {
doLast {
println it.name
}
}
task taskD {
doLast {
println it.name
}
}
taskC.finalizedBy taskD
执行任务:
± % gradle taskC -q !1
taskC
taskD
即使被终结的任务失败了,Finalizer任务一样会执行
task taskC {
doLast {
println it.name
throw new RuntimeException()
}
}
task taskD {
doLast {
println it.name
}
}
taskC.finalizedBy taskD
执行任务:
± % gradle taskC -q
taskC
taskD
FAILURE: Build failed with an exception.
...
仅仅有一种情况下Finalizer任务不会被执行,就是被终极的任务根本没有工作,比如up to date
或者依赖失败了。
Finalizer任务在无论构建是否成功都必须要清除一些资源的情况下是非常有用的,比如测试中需要的web容器,它在集成测试前需要被开启,而无论测试是否成功,都需要被关闭。
生命周期任务
生命周期任务本身自己没有action可以运行,它有一下几个特点
- 一个工作流(比如用
check
运行所以的check任务) - 一个可以构建是事物(比如运行
debug32MainExecutable
为native组件生产32位下的debug可执行程序) - 一个可以运行相同逻辑任务的任务(比如
compileAll
运行所以的编译任务)
很多Gradel插件都有自己的生命周期任务,可以方便的指定具体的操作。如果你自己开发Gradle插件时,一定要记得把相同逻辑的任务作出生命周期任务,以便于操作。
除非生命周期任务有自己的action,否则他的执行与否要看他所有的依赖是否需要执行,如果有一个需要执行,那么生命周期任务就会被执行。如果所有的依赖都是up-to-date,skipped或者从构建缓存拉取的,那么这个任务就是up-to-date的。