5、Task类型详解:Copy、Exec、Zip、Delete等内置Task类型的使用
Gradle 内置了好几种常用的 Task 类型。说实话,刚接触那会儿我也觉得没必要记,反正都能用 doLast 搞定。但后来项目一复杂,脚本越来越长,我才发现——用好这些内置类型,能让你的构建脚本干净得像刚擦过的白板。
这一节,我们就来逐个拆解 Copy、Exec、Zip、Delete 这几个高频 Task 类型。我会结合自己踩过的坑,告诉你什么时候用、怎么用、别踩什么雷。
5.1 Copy:文件复制,比你想象的更强大
复制文件,听起来简单吧?但如果你只是写 copy { from ... into ... },那你就亏了。Copy Task 的真正价值在于它的 过滤、重命名、转换 能力。
核心用法:
tasks.register<Copy>("copyConfig") {
from("src/main/config")
into("build/config")
include("**/*.properties")
exclude("**/secret.properties")
// 重命名文件
rename { fileName ->
fileName.replace(".properties", ".conf")
}
// 或者用正则
rename("(.+)\\.properties", "$1.conf")
}
我个人习惯在 多环境部署 时用 Copy。比如开发环境和生产环境配置不同,我会把公共配置放一个目录,环境特定配置放另一个目录,然后用 Copy 合并到输出目录。这样既清晰又不会漏文件。
小技巧:Copy 支持 filter 方法,可以在复制过程中替换文件内容中的占位符。比如 filter { it.replace("{{version}}", project.version.toString()) }。这比写一堆 doLast 优雅多了。
5.2 Exec:执行外部命令,但别滥用
Exec 是 Gradle 里的“瑞士军刀”。你可以用它执行任何系统命令——shell 脚本、Python 脚本、甚至启动 Docker 容器。
tasks.register<Exec>("runPythonScript") {
workingDir = file("scripts")
commandLine("python3", "deploy.py", "--env", "prod")
// 或者用操作系统 shell
// commandLine("sh", "-c", "echo hello && pwd")
}
嗯,这里要注意:Exec 是阻塞的。它会等命令执行完才继续。如果你的命令跑很久,整个构建就会卡住。我曾经在 CI 上吃过这个亏——一个 Exec 调用了耗时的数据迁移脚本,结果构建超时了。
避坑指南:Exec 不会自动设置 PATH 环境变量。如果你在本地能跑的命令,在 Gradle 里报“command not found”,多半是 PATH 问题。可以显式设置 environment("PATH", System.getenv("PATH"))。我当初排查这个问题花了整整一下午。
另外,Exec 的 commandLine 参数是 列表形式,不要写成字符串。写成 commandLine("ls -la") 会报错,因为 Gradle 把它当成了一个命令名。正确写法是 commandLine("ls", "-la")。
5.3 Zip:打包压缩,一步到位
Zip Task 是 Gradle 内置的压缩工具。它继承自 Copy,所以 Copy 的所有能力(过滤、重命名、包含排除)它都有。区别在于:Zip 的输出是一个 .zip 文件。
tasks.register<Zip>("packageApp") {
archiveFileName.set("my-app-${version}.zip")
destinationDirectory.set(layout.buildDirectory.dir("dist"))
from("build/libs") {
include("*.jar")
}
from("config") {
include("*.yml")
into("config") // 在 zip 包内创建子目录
}
}
我记得有一次,项目需要把前端构建产物和后端 jar 包打成一个 zip 包。用 Zip Task 配合 from 的多个来源,十几行代码就搞定了。如果手写脚本,至少得几十行,还得处理路径问题。
常用属性:
| 属性 | 说明 |
|---|---|
| archiveFileName | 输出文件名,支持占位符如 ${version} |
| destinationDirectory | 输出目录 |
| entryCompression | 压缩级别:STORED(不压缩)或 DEFLATED(默认) |
| duplicatesStrategy | 遇到同名文件时的处理策略:INCLUDE、EXCLUDE、WARN、FAIL |
你想想看,如果不用 Zip Task,你可能会写一个 Exec 调用系统 zip 命令。但那样就失去了跨平台性——Windows 上没 zip 命令,Linux 上可能版本不同。用 Gradle 内置的 Zip,所有平台行为一致,省心。
5.4 Delete:清理构建产物
Delete 是最简单的内置 Task 之一。它的作用就是删除文件或目录。但别小看它,用不好会出大问题。
tasks.register<Delete>("cleanBuild") {
delete("build", "dist", "*.log")
// 也可以使用 project.fileTree
delete(fileTree("src") {
include("**/*.tmp")
})
// 设置 followSymlinks = false 避免删除符号链接指向的真实文件
followSymlinks = false
}
我曾经犯过的错:有一次我写 delete("build"),结果因为工作目录没设置对,它删掉了项目根目录下的 build 文件夹——里面是同事的本地构建缓存。从那以后,我养成了一个习惯:Delete 的路径一定要用绝对路径或基于 project.rootDir 的相对路径。比如 delete(layout.buildDirectory) 就比 delete("build") 安全得多。
另外,Delete 默认不会删除符号链接指向的真实文件。如果你有符号链接指向了系统目录(比如 /etc),followSymlinks = false 能救你一命。
5.5 知识体系总览
为了让你更直观地理解这几个 Task 类型的关系和适用场景,我画了一张图:
5.6 组合使用:一个真实案例
光讲单个 Task 没意思,我们来看一个组合使用的场景。假设你要做一个 构建并打包部署包 的任务:
tasks.register<Copy>("prepareFiles") {
from("src/main/resources")
into(layout.buildDirectory.dir("staging"))
include("**/*.properties")
filter { it.replace("{{version}}", project.version.toString()) }
}
tasks.register<Exec>("runTests") {
dependsOn("prepareFiles")
commandLine("npm", "test")
workingDir = file("frontend")
}
tasks.register<Zip>("createDeployPackage") {
dependsOn("runTests")
archiveFileName.set("deploy-${version}.zip")
destinationDirectory.set(layout.buildDirectory.dir("dist"))
from(layout.buildDirectory.dir("staging"))
from("frontend/dist") {
into("static")
}
}
tasks.register<Delete>("cleanStaging") {
delete(layout.buildDirectory.dir("staging"))
}
// 让打包完成后自动清理
tasks.named("createDeployPackage") {
finalizedBy("cleanStaging")
}
这个例子展示了:
- Copy 做文件准备和内容替换
- Exec 执行测试命令
- Zip 打包最终产物
- Delete 清理临时文件
- 通过
dependsOn和finalizedBy串联整个流程
说白了,Gradle 的内置 Task 类型就像乐高积木。单个看都很简单,但组合起来就能搭出复杂的构建流水线。而且因为它们是类型安全的,IDE 能给你智能提示,写起来比 Groovy DSL 舒服太多了。
我的建议:遇到文件操作、命令执行、打包压缩这类需求,先想想 Gradle 有没有内置类型。不要一上来就写 doLast + 原生 API。内置类型不仅代码更简洁,而且经过了大量项目验证,坑少得多。
公众号:蓝海资料掘金营,微信 deep3321