理解Gradle中的任务
Gradle的构建过程基于任务(Task)的概念,而每个任务都可以包含一个或多个动作(Action)。
任务是构建中执行的一些独立的工作单元,例如编译类、创建JAR、生成Javadoc或将存档发布到仓库。
我们使用gradle taskName或gradlew taskName来执行任务,比如build:
$ ./gradlew build
项目中所有可用任务都是来自Gradle插件和构建脚本。
比如在子项目构建脚本中创建的任务:
可用任务清单
我们通过运行tasks任务来列出项目所有可用任务。
$ ./gradlew tasks
让我们以一个非常基本的Gradle项目为例,该项目具有以下结构:
settings.gradle.kts文件定义了根项目名称和app子项目:
rootProject.name = "HelloGradle"
include("app")
目前app子项目的构建文件暂时是一个空文件。
为了看app子项目中的任务,运行./gradlew :app:tasks
结果中只能看到少量的辅助任务(Help Tasks),它们是Gradle核心提供的用于分析构建的任务。其他任务,如构建项目或编译代码的任务,则是由插件来添加的。
我们接下来在app构建脚本里添加application插件:
//app/build.gradle.kts
plugins {
id("application")
}
application插件会添加一些生命周期任务。 现在再运行./gradlew app:tasks,我们看到多了一些和构建相关的任务,比如assemble 和 build任务:
任务结果
当Gradle执行一个任务时,它会在控制台中显示任务的结果标签。
这些标签描述了一个任务是否有要执行的动作,Gradle是否执行了这些动作。动作包括但不限于编译代码、压缩文件和发布存档。
(no label) or EXECUTED
任务执行了动作。
任务有动作,Gradle执行了它们。
任务没有动作,有一些依赖项,Gradle执行了一个或多个依赖项。
UP-TO-DATE
Gradle会检查任务的输入和输出,如果输入没有变化,并且输出已经存在且是最新的,那么任务就不会被执行,并标记为UP-TO-DATE。
任务有输出和输入,但都没有改变。
任务有动作,但是告诉Gradle它不会改变输出。
任务没有动作,有一些依赖项,但所有依赖都是UP-TO-DATE, SKIPPED 或 FROM-CACHE的。
任务没有动作,也没有任何依赖项。
FROM-CACHE
Gradle检测到任务的输出已经在构建缓存中时,会直接从缓存中加载,比如build cache。
SKIPPED
任务不执行他的动作
任务被跳过可能是因为某个先决条件未满足,或者任务被明确地配置为跳过,比如使用命令行选项--exclude-task排除。
NO_SOURCE
任务不需要执行他的动作
任务有输入和输出,但没有源文件。
FROM-CACHE和UP-TO-DATE都是Gradle优化构建过程的手段,都有助于减少不必要的任务执行,提高构建速度。可能会弄不明白什么时候是UP-TO-DATE或者FROM-CACHE,在本文后面介绍缓存任务的时候,我们再做进一步说明。
任务组group和描述description
任务组和描述用于组织和描述任务。
Groups
任务组被用来对任务进行分类,当运行./gradlew tasks时,所有任务会被列在各自的组中,这样更容易理解它们的目的和与其他任务的关系,使用group属性设置组。
Descriptions
描述提供了任务功能的简要解释。当运行./gradlew tasks时,描述会显示在每个任务的旁边,帮助开发者了解它的用途以及如何使用它,使用description属性设置描述。
我们回头去看一下前面gradle-project执行tasks的输出结果:
:run任务属于Application分组, 对其描述是 "Runs this project as a JVM application"。
这个任务在代码中的定义会像这样:
//app/build.gradle.kts
tasks.register("run") {
group = "Application"
description = "Runs this project as a JVM application."
}
私有和隐藏任务
Gradle不支持将任务标记为“私有”。
当我们运行:tasks时,默认只会显示那些分配了任务组的任务,即所谓的可见任务,而那些没有分配group的任务,就是隐藏任务, 需要注意,隐藏任务依旧可以被Gradle执行,只是不显示而已。
如下所示,我们创建了一个任务helloTask,执行./gradlew :app:tasks,任务列表里并没有找到helloTask任务。
//app/build.gradle.kts
tasks.register("helloTask") {
println("Hello")
}
给它分配一个group
tasks.register("helloTask") {
group = "Other"
description = "Hello task"
println("Hello")
}
任务就出现在了指定的Other分组下 :
执行./gradlew tasks --all 可以显示所有任务,包括隐藏的。
比如上面的helloHiddenTask,我们没有设置group属性,也显示在了Other分组下。
分组任务
如果我们想要自定义执行tasks时向用户显示哪些任务,可以对任务分组并设置每个组的可见性。
示例gradle-project虽然只是一个简单的Java应用,列出的可用的任务却非常多,使用构建的开发人员很少直接需要其中的许多任务。
我们可以通过配置tasks任务,限制任务显示到一个特定的分组。
我们修改一下构建脚本,创建一个自己的分组,使用displayGroup属性来指定要显示的任务组。
//app/build.gradle.kts
val myBuildGroup = "my app build" // Create a group name
tasks.register<TaskReportTask>("tasksAll") { // Register the tasksAll task
group = myBuildGroup
description = "Show additional tasks."
setShowDetail(true)
println("register tasksAll")
}
tasks.named<TaskReportTask>("tasks") {
displayGroup = myBuildGroup
}
在Gradle中,我们执行tasks任务时,会使用此类型的一个实例TaskReportTask,其中displayGroup属性用来控制要显示的任务组,默认值是null, 可以使用命令行选项 '--group'设置,设置后就只显示这个分组的任务。
任务类别
Gradle中的任务分为两类:
1. Actionable tasks(可操作任务)
2. Lifecycle tasks(生命周期任务)
可执行任务定义了Gradle应该执行的具体操作。例如,:compileJava 任务,它编译项目的Java代码。这些操作包括创建JAR文件、压缩文件、发布归档文件等。当你应用了一个插件,如java-library,Gradle会自动添加与该插件相关的可执行任务。
生命周期任务定义了一系列的目标(targets),你可以调用这些目标来执行一系列的操作。例如,:build 就是一个常见的生命周期任务,用于构建整个项目。这类任务本身不执行具体的操作(actions)。相反,它们捆绑了可执行任务,当调用生命周期任务时,会触发与之关联的可执行任务。 基础Gradle插件(base Gradle plugin)只添加生命周期任务。这意味着如果你没有添加任何插件,Gradle仍然会提供这些基本的生命周期任务。
我们再看一下之前例子的:tasks结果
如果我们执行:build任务,会看到有好几个任务都被执行了,包括:app:compileJava任务。
可以表述为可执行任务:compileJava捆绑到了生命周期任务:build中。
增量任务
Gradle任务的一个关键特征是它们的增量性。
Gradle可以重用之前构建的结果。因此,如果我们之前构建过项目,并且只进行了小幅更改,那么重新运行:build将不需要Gradle执行大量工作。
例如,如果我们只修改项目中的测试代码,保持生产代码不变,执行构建将仅重新编译测试代码。Gradle将生产代码的任务标记为UP-TO-DATE,表明自上次成功构建以来,它保持不变:
缓存任务
Gradle可以使用构建缓存来重用过去构建的结果。
要启用此功能,请使用--build-cache命令行参数或在gradle.properties文件中设置org.gradle.caching=true来激活构建缓存。
此优化有可能显著加速项目的构建:
当Gradle可以从缓存中获取一个任务的输出时,它会给这个任务贴上FROM-CACHE的标签。如果经常在分支之间切换,构建缓存很方便。Gradle支持本地和远程构建缓存。
我们通过实际执行其中的compileJava源码编译任务来加深对任务结果为UP-TO-DATE和FROM-CACHE的理解:
我们把之前的gradle-project例子修改一下
//app/build.gradle.kts
//添加执行入口类
application {
mainClass = "gradle.project.App"
}
//app/src/main/java/gradle/project/App.java
package gradle.project;
public class App {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
现在app子项目是一个可执行的Java 应用程序,App类是应用的执行入口。
先./gradlew clean删除各子项目build目录,确保项目是干净的状态。
执行compileJava任务编译java, --build-cache告诉Gradle本次使用构建缓存,需要--info选项显示一些额外的构建信息:
./gradlew --build-cache compileJava --info
Settings evaluated using settings file '/Users/roy/Downloads/gradle-project/settings.gradle'.
Using local directory build cache for the root build (location = /Users/xxx/.gradle/caches/build-cache-1, removeUnusedEntriesAfter = 7 days).
Projects loaded. Root project using build file '/Users/xxx/Downloads/gradle-project/build.gradle'.
> Task :app:compileJava
Stored cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
执行成功后,app子项目里多了build目录,输出的信息里也看到了build cache目录默认的位置Gradle User Home/caches/build-cache-1/,在其中缓存了任务的输出。
再次执行
./gradlew --build-cache compileJava --info
> Task :app:compileJava UP-TO-DATE
Build cache key for task ':app:compileJava' is 3804aa4dacefba7c96c077f8de82ae3d
Skipping task ':app:compileJava' as it is up-to-date.
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
因为我们没有做任何改动,build目录里已经最新的输出了,所以compileJava任务会跳过,此时任务被标记为UP-TO-DATE。
执行./gradlew clean删除app的build目录,然后再执行
./gradlew --build-cache compileJava --info
> Task :app:compileJava FROM-CACHE
Build cache key for task ':app:compileJava' is db35214e1886f8b0ebbcc16e2fa7a618
Task ':app:compileJava' is not up-to-date because:
Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main has been removed.
Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main/gradle has been removed.
Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main/gradle/project has been removed.
Output property 'options.generatedSourceOutputDirectory' file /Users/xx/Downloads/gradle-project/app/build/generated/sources/annotationProcessor/java/main has been removed.
Loaded cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
BUILD SUCCESSFUL in 1s
1 actionable task: 1 from cache
任务标记为FROM-CACHE,并且从缓存中加载了上次执行的输出,日志中也有提示:
Loaded cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
另外,app的build目录也有了,里面的输出应该就是直接从build cache中拿过来的。
再再一次执行./gradlew --build-cache compileJava --info, 任务又是UP-TO-DATE了。
> Task :app:compileJava UP-TO-DATE
所以,对于要执行的任务,只要项目里已经存在最新输出,它就是UP-TO-DATE;否则如果任务启用构建缓存,并且在缓存里有最新输出,就是FROM-CACHE。
可缓存任务和不可缓存任务
在Gradle中并非所有任务都可以或应该被缓存,除了少数内置任务是可缓存外,大部分任务由于各种原因(如不可预测的输出、外部依赖、任务配置等)可能不适合缓存。这些任务通常被标记为non-cacheable。
使用--build-cache选项可以让Gradle启用构建缓存功能。当这个选项被启用时,Gradle会尝试缓存可缓存任务的输出,并在后续构建中重用这些输出。
对于non-cacheable任务,Gradle会忽略构建缓存机制,并总是执行这些任务。通常是因为这些任务的输出可能依赖于不可预测或不可重复的因素,或者任务本身的配置不允许缓存。
开发者也可以显式地配置这类任务使其可缓存。
要确定一个任务是否可缓存,你可以查看任务的输出。如果任务有一个buildCacheable属性,并且它被设置为true,那么该任务就是可缓存的。如果任务没有明确的buildCacheable属性设置,或者它被设置为false,那么该任务就是non-cacheable的。
构建缓存的有效性还取决于构建环境的稳定性和一致性。如果构建环境经常变化(例如,使用了不同的构建机器或文件系统),那么构建缓存的效果可能会受到限制。因此,在使用构建缓存时,确保构建环境的一致性是非常重要的。
开发任务
在开发Gradle任务时,我们有两个选择:
1.使用现有的Gradle任务类型,比如Zip,Copy或Delete
2.创建自己的任务类型,比如MyResolveTask或者CustomTaskUsingToolchains
任务类型就是是Gradle Task类的子类。
对于Gradle任务,有三种状态需要考虑:
1.注册任务 - 在构建逻辑中使用一个任务(由您实现或由Gradle提供)。
2.配置任务 - 定义任务的输入和输出。
3.实现任务 - 创建一个自定义任务类(即自定义类型)
注册通常使用register()方法完成。
配置任务通常使用named()方法完成。
实现任务通常是通过扩展Gradle的DefaultTask类来完成的:
①: 注册Copy类型的myCopy任务。
②: 根据Copy API为注册的myCopy任务配置它所需的输入和输出。
③: 实现一个名为MyCopyTask的自定义任务类型,它扩展了DefaultTask并定义了copyFiles任务操作。
1.注册任务
我们可以通过在构建脚本和插件中注册任务来定义Gradle要执行的操作。
使用字符串作为任务名来定义任务:
//build.gradle.kts
tasks.register("myCopy") {
doFirst {
// Task action = Execution code
// Run before exiting actions
}
doLast {
// Task action = Execution code
// Run after existing actions
}
}
register()方法会将myCopy任务添加到TaskCollection中。
2.配置任务
Gradle任务必须经过配置来完成他们的操作。比如一个任务需要ZIP一个文件,我们就必须要配置文件名和文件位置。这可以参考Gradle Zip任务提供的API,学习如何正确进行配置。
在上图示例中,我们注册了一个myCopy任务
tasks.register<Copy>("myCopy")
我们可以在注册的时候就立即用代码块配置任务:
tasks.register<Copy>("myCopy") {
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
因为这个任务是Copy类型, 是Gradle支持的任务类型,所以可以使用Copy API,如from、to。
之后在需要的地方,都可以通过named()方法查找对应名字的任务来配置:
//build.gradle.kts
tasks.named<Copy>("myCopy") {
from("resources")
into("target")
include("**/*.txt", "**/*.xml", "**/*.properties")
}
注意,在named()调用时,如果指定的任务还没有注册,就会构建失败。
3.实现任务
Gradle提供了很多任务类型,包括 Delete, Javadoc, Copy, Exec, Tar和Pmd等等,如果都满足不了我们的构建逻辑需求,我们可以实现一个自定义的任务类型。
要创建一个任务类型,需要扩展DefaultTask,然后定义为一个抽象类(不用是具体实现类):
//app/build.gradle.kts
abstract class MyCopyTask extends DefaultTask {}
Gradle 会通过解析 build.gradle 文件中的配置动态创建任务实例,这些实例是任务类的具体实现,它们包含了在配置中设置的所有参数和指定的动作。
task("taskName")与tasks.register("taskName")
在 Gradle 中,task("taskName") 和 tasks.register("taskName") 都被用来创建新的任务,但它们属于不同的 API 并具有一些细微的差别和用法上的考虑。
传统 DSL(领域特定语言)方式:
task("taskName") 是 Gradle 的传统 DSL(领域特定语言)方法,用于在 build.gradle 文件中声明一个任务。这种方式相对直观和简单,适用于简单的任务定义。
任务注册(Tasks API)方式:
tasks.register("taskName") 是 Gradle Tasks API 的一部分,用于以编程方式注册任务。它提供了更多的灵活性和控制,尤其是在需要基于其他任务或项目配置动态创建任务时。
主要区别:传统 DSL 方法在配置阶段就执行了任务的配置代码,而 Tasks API 则允许延迟配置,直到执行阶段才执行配置代码。这有助于避免在配置阶段发生不必要的副作用。
随着 Gradle 的不断进化,Tasks API 被认为是更现代和推荐的方式来创建和注册任务。
tasks.register("taskName") 实现延迟配置的原理主要基于 Gradle 的任务生命周期和任务注册机制。
在 Gradle 构建的生命周期中,任务(Task)的创建和配置是分开的。传统的 task("taskName") 语法在项目的配置阶段(configuration phase)就立即创建并配置了任务。这意味着,即使在任务从未被执行的情况下,其配置代码也会被执行。这有时可能会导致不必要的副作用,比如提前计算了某些值,或者执行了只在任务执行时才需要的逻辑。
相比之下,tasks.register("taskName") 使用了一种不同的方法。这个方法实际上并没有立即创建任务,而是注册了一个任务工厂(TaskFactory)。这个工厂会在任务首次执行时(execution phase)被调用,从而创建任务实例并执行其配置,这就是所谓的“延迟配置”(lazy configuration)。
具体来说,当你调用 tasks.register("taskName") 时,Gradle 会创建一个 TaskRegistration 对象,该对象封装了任务的配置逻辑(即你传递给 register 方法的闭包)。这个 TaskRegistration 对象会被添加到 Gradle 的任务容器中,但不会立即创建任务实例。
当 Gradle 执行阶段到来,并且需要执行名为 "taskName" 的任务时,Gradle 会从任务容器中检索相应的 TaskRegistration 对象,并调用其工厂方法来创建任务实例。此时,闭包中的配置逻辑才会被执行,从而配置新创建的任务实例。