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 类型的关系和适用场景,我画了一张图:

Gradle 内置 Task 类型知识体系 内置 Task 类型 Copy Exec Zip Delete 核心特性 • from / into 指定源和目标 • include / exclude 过滤 • rename 重命名 • filter 内容替换 • 继承自 CopySourceProcessing 核心特性 • commandLine 指定命令 • workingDir 工作目录 • environment 环境变量 • 阻塞执行 • 注意 PATH 问题 核心特性 • 继承 Copy 所有能力 • archiveFileName 文件名 • entryCompression 压缩级别 • duplicatesStrategy 冲突策略 • 跨平台一致性 核心特性 • 删除文件/目录 • 支持 fileTree • followSymlinks 控制 • 建议用绝对路径 • 小心误删

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 清理临时文件
  • 通过 dependsOnfinalizedBy 串联整个流程

说白了,Gradle 的内置 Task 类型就像乐高积木。单个看都很简单,但组合起来就能搭出复杂的构建流水线。而且因为它们是类型安全的,IDE 能给你智能提示,写起来比 Groovy DSL 舒服太多了。

我的建议:遇到文件操作、命令执行、打包压缩这类需求,先想想 Gradle 有没有内置类型。不要一上来就写 doLast + 原生 API。内置类型不仅代码更简洁,而且经过了大量项目验证,坑少得多。


公众号:蓝海资料掘金营,微信 deep3321