26、文件操作:FileCollection、FileTree、复制与归档、文件监听
文件操作,说白了就是 Gradle 构建过程中最基础也最绕不开的活儿。你想想看,不管是编译、打包还是部署,本质上都是在跟文件打交道。我个人习惯把 Gradle 的文件操作分成四个维度:集合、树、复制归档、监听。今天咱们就一个一个掰开揉碎了讲。
核心知识点速览
- FileCollection:文件集合,无序、可迭代
- FileTree:文件树,保留目录结构
- CopySpec:复制与归档的核心 DSL
- 文件监听:增量构建的底层机制
26.1 FileCollection:文件集合
FileCollection 说白了就是一个文件列表,但它不是简单的 List
// 最基本的创建方式
def configFiles = files('config/application.yml', 'config/logback.xml')
// 使用闭包懒加载
def dynamicFiles = files {
fileTree('src/main/resources').filter { it.name.endsWith('.properties') }
}
// 集合运算
def allFiles = files('lib/a.jar') + files('lib/b.jar')
def filtered = allFiles.filter { f -> f.name.startsWith('log') }
// 遍历
configFiles.each { File file ->
println "处理文件: ${file.name}"
}
我的经验:files() 方法返回的是 ConfigurableFileCollection,它支持增量构建。如果你在 task 里用 files() 声明 inputs,Gradle 会自动跟踪这些文件的变化。我曾经接手过一个项目,里面全是手动 new File() 然后自己判断修改时间,后来全部改成 files(),构建速度提升了 40%。
26.2 FileTree:文件树
FileTree 比 FileCollection 多了一层目录结构信息。说白了,它知道哪个文件在哪个子目录下。你想想看,当你需要递归遍历整个源码目录时,FileTree 就是最佳选择。
// 创建文件树
def sourceTree = fileTree('src/main/java') {
include '**/*.java'
exclude '**/test/**'
}
// 另一种方式
def resourceTree = fileTree(dir: 'src/main/resources',
includes: ['**/*.xml', '**/*.properties'],
excludes: ['**/backup/**'])
// 递归遍历
sourceTree.visit { FileVisitDetails details ->
println "${details.relativePath} -> ${details.file.size()} bytes"
// details.name, details.path, details.isDirectory()
}
// 转换为集合
Set<File> javaFiles = sourceTree.files
注意:FileTree 的 visit() 方法会深度遍历所有子目录。如果你的目录层级特别深(比如超过 20 层),建议用 include 限制范围。我曾经见过一个项目,构建脚本里 fileTree('/') 直接遍历了整个硬盘,结果构建跑了 10 分钟还没结束……
26.3 复制与归档:CopySpec
复制文件这件事,Gradle 提供了非常优雅的声明式 API——CopySpec。你不需要写一堆 FileUtils.copyFile(),只需要声明「从哪来,到哪去,要什么,不要什么」。
// 复制任务
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())
}
}
// 归档任务
task createDistribution(type: Zip) {
archiveFileName = "myapp-${version}.zip"
destinationDirectory = file('build/dist')
from 'build/libs'
from 'config' {
into 'conf'
}
from 'scripts' {
include '*.sh'
fileMode = 0755
}
}
核心要点:CopySpec 支持嵌套。你可以用 from + into 的组合来重新组织目录结构。比如把多个目录的内容合并到一个归档包里,每个来源可以指定不同的目标子目录。
// 更复杂的归档示例
task packageApp(type: Tar) {
compression = Compression.GZIP
archiveExtension = 'tar.gz'
// 主程序
from('build/libs') {
include '*.jar'
into 'lib'
}
// 配置文件
from('config') {
include '*.yml'
into 'conf'
// 过滤敏感信息
filter { line ->
line.contains('password') ? '# 已过滤敏感信息' : line
}
}
// 启动脚本
from('scripts') {
include 'start.sh', 'stop.sh'
into 'bin'
fileMode = 0755
}
}
26.4 文件监听与增量构建
Gradle 最强大的特性之一就是增量构建。它不需要你手动监听文件变化,而是通过声明 inputs 和 outputs 来自动判断。但有时候,你需要更细粒度的控制。
// 声明式增量构建
task processFiles {
inputs.files fileTree('src/data')
inputs.property 'encoding', 'UTF-8'
outputs.dir file('build/processed')
doLast {
// 只有 inputs 发生变化时才会执行
inputs.files.each { File input ->
def output = new File(outputs.files.singleFile, input.name)
output.text = input.text.toUpperCase()
}
}
}
// 手动触发重新处理
task cleanAndProcess(type: Delete) {
delete 'build/processed'
}
// 然后执行:gradle cleanAndProcess processFiles
避坑指南:我曾经犯过一个错误——在 task 里直接修改了 inputs 目录下的文件。结果 Gradle 检测到 inputs 变化,又触发了一次 task,形成了死循环。记住:永远不要在 task 执行时修改自己的 inputs。如果你需要生成中间文件,把它们放到 build 目录下。
26.5 实战:文件操作组合拳
咱们来看一个真实项目中的例子。假设你要做一个代码生成器,需要读取模板文件,替换占位符,然后输出到指定目录。
task generateCode {
// 模板文件
def templates = fileTree('templates') {
include '**/*.template'
}
// 配置数据
def config = [
appName: project.name,
version: project.version,
package: 'com.example'
]
inputs.files templates
inputs.properties config
outputs.dir layout.buildDirectory.dir('generated')
doLast {
def outputDir = outputs.files.singleFile
templates.each { File template ->
def relativePath = template.name.replace('.template', '.java')
def outputFile = new File(outputDir, relativePath)
outputFile.parentFile.mkdirs()
// 替换占位符
def content = template.text
config.each { key, value ->
content = content.replace("\${${key}}", value.toString())
}
outputFile.text = content
}
}
}
总结一下:
- FileCollection 适合处理平铺的文件列表
- FileTree 适合处理带目录结构的文件
- CopySpec 是复制和归档的首选 DSL
- 增量构建靠 inputs/outputs 声明,别自己搞文件监听
嗯,文件操作这块其实不难,关键是养成声明式思维。别想着「我要怎么复制这个文件」,而是想「我要声明哪些文件从哪里到哪里」。Gradle 会帮你搞定剩下的。