文件操作:文件集合、文件树、复制与归档、文件监听与变更
文件操作,说白了就是 Gradle 构建中最基础也最绕不开的活儿。你想想看,无论是编译、打包还是部署,本质上都是在跟文件打交道。我个人习惯把 Gradle 的文件 API 分成四个层次:文件集合、文件树、复制归档、文件监听。掌握了这些,你就能像操作 Linux 管道一样,流畅地编排构建流程。
文件集合:最基础的抽象
Gradle 的 FileCollection 接口,本质上就是一个文件列表的抽象。它不像 Java 的 File 那样死板,而是支持懒加载、过滤和变换。
// 创建一个文件集合
FileCollection collection = files('build.gradle', 'settings.gradle')
// 还可以用闭包动态生成
FileCollection srcDirs = files {
file('src/main/java').listFiles()
}
// 集合操作:合并、相减、交集
FileCollection all = collection + srcDirs
FileCollection diff = all - files('build.gradle')
我在项目中遇到过一个问题:有人直接用 fileTree 遍历整个项目目录,结果构建慢得离谱。后来我改成用 files 配合闭包,只加载需要的文件,速度提升了 3 倍。嗯,这里要注意:文件集合是惰性求值的,只有当你调用 each 或 asFileTree 时才会真正去磁盘上读。
核心要点:文件集合支持 +、-、intersect 操作,但要注意类型安全——混合 File 和 String 时,Gradle 会自动转换。
文件树:目录结构的灵魂
文件树 FileTree 是文件集合的子接口,专门处理目录结构。它支持 Ant 风格的匹配模式,比如 **/*.java 表示所有子目录下的 Java 文件。
// 创建文件树
FileTree tree = fileTree('src') {
include '**/*.java'
exclude '**/test/**'
}
// 遍历文件树
tree.each { file ->
println "找到文件: ${file.name}"
}
// 转换为文件集合
FileCollection asCollection = tree.asFileTree
你想想看,为什么 Gradle 不直接用 Java 的 Files.walk?因为文件树支持过滤链——你可以先 include 再 exclude,最后再 filter,每一步都是懒执行的。我曾经在构建脚本里写过这样的链式操作,排查问题时发现某个 filter 写错了,导致整个目录被排除。避坑指南:include 和 exclude 的顺序会影响结果,Gradle 会按声明顺序依次应用。
个人经验:文件树的 matching 方法比 include/exclude 更灵活,支持 Predicate 接口。我一般用它来做复杂的条件过滤。
复制与归档:构建的核心操作
复制文件是构建中最常见的操作。Gradle 的 Copy 任务提供了声明式的 API,支持重命名、过滤和变换。
task copyResources(type: Copy) {
from 'src/main/resources'
into 'build/resources'
include '**/*.properties'
exclude '**/secret.properties'
// 重命名文件
rename { fileName ->
fileName.replace('.properties', '.conf')
}
// 过滤内容
filter { line ->
line.replace('${version}', project.version.toString())
}
}
归档操作就更直接了——Zip、Tar、Jar 都是 Copy 任务的子类。我记得有一次,团队里有人用 Ant 的 zip 任务来打包,结果中文文件名乱码。换成 Gradle 的 Zip 任务后,设置 encoding = 'UTF-8' 就解决了。
task packageApp(type: Zip) {
archiveFileName = "app-${version}.zip"
destinationDirectory = file('build/dist')
from 'build/libs'
from 'config' {
into 'conf'
}
// 保留文件权限
filePermissions {
user {
read = true
execute = true
}
}
}
注意:Copy 任务的 from 和 into 都是惰性的。如果你在配置阶段修改了源文件,任务执行时才会生效。我曾经踩过这个坑:在 doFirst 里生成文件,但 Copy 任务在配置阶段就已经计算好了文件列表,导致新生成的文件没被打包进去。
文件监听与变更:增量构建的基石
Gradle 的增量构建能力,很大程度上依赖于文件变更检测。你可以通过 InputChanges 来获取变更的文件列表,实现高效的增量处理。
task processFiles(type: IncrementalTask) {
inputs.files fileTree('src')
outputs.dir file('build/processed')
doLast {
// 获取变更的文件
def changes = inputs.getChanges()
changes.each { change ->
println "文件 ${change.file.name} 发生了 ${change.changeType}"
}
}
}
为什么需要文件监听?你想想看,如果每次构建都全量处理所有文件,项目大了以后构建时间会爆炸。Gradle 通过 InputChanges 只处理变更的文件,配合 @Incremental 注解,可以实现毫秒级的增量编译。
我个人习惯在自定义任务中使用 @Incremental 注解,配合 @InputFiles 和 @OutputDirectory,让 Gradle 自动管理增量逻辑。避坑指南:增量任务必须声明输入和输出,否则 Gradle 会退化为全量执行。
核心机制:Gradle 通过文件快照(snapshot)来检测变更。每次构建后,它会记录文件的哈希值和修改时间。下次构建时,对比快照就能知道哪些文件变了。
知识体系总览
下面这张图展示了文件操作的核心层次关系。你可以看到,文件集合是基础,文件树是它的子集,复制归档和文件监听都建立在它们之上。
实战:组合使用
在实际项目中,这些 API 往往是组合使用的。比如,一个典型的构建流程可能是:先通过文件树找到所有 Java 文件,编译后复制到输出目录,最后打包成 Jar。
task buildAndPackage {
dependsOn compileJava
doLast {
// 1. 文件树:找到所有 class 文件
FileTree classes = fileTree('build/classes') {
include '**/*.class'
}
// 2. 文件集合:合并资源文件
FileCollection allFiles = files(classes, fileTree('src/main/resources'))
// 3. 复制到临时目录
copy {
from allFiles
into 'build/temp'
}
// 4. 打包成 Jar
jar {
from 'build/temp'
archiveFileName = 'app.jar'
}
}
}
嗯,这里要注意:不要在一个任务里同时做复制和打包。更好的做法是拆分成多个任务,让 Gradle 的增量构建机制自动优化。我见过有人把所有逻辑塞进一个 doLast,结果每次构建都全量执行,白白浪费了 Gradle 的增量能力。
避坑指南:我曾经在 Copy 任务里用了 eachFile 来修改文件内容,结果发现每次构建都会重新处理所有文件。后来改用 filter 配合 @Incremental,才实现了真正的增量处理。
文件操作看似简单,但用好 Gradle 的 API 能大幅提升构建效率。记住:懒加载、增量构建、声明式 API 是三个核心原则。掌握了这些,你就能写出既高效又优雅的构建脚本。