26、文件操作:FileCollection、FileTree、复制与归档、文件监听

文件操作,说白了就是 Gradle 构建过程中最基础也最绕不开的活儿。你想想看,不管是编译、打包还是部署,本质上都是在跟文件打交道。我个人习惯把 Gradle 的文件操作分成四个维度:集合、树、复制归档、监听。今天咱们就一个一个掰开揉碎了讲。

核心知识点速览

  • FileCollection:文件集合,无序、可迭代
  • FileTree:文件树,保留目录结构
  • CopySpec:复制与归档的核心 DSL
  • 文件监听:增量构建的底层机制
Gradle 文件操作体系 FileCollection 文件集合(无序) FileTree 文件树(保留结构) CopySpec 复制与归档 DSL 文件监听 增量构建机制 files() / fileCollection() + 运算符合并 过滤 / 遍历 fileTree(dir) include / exclude visit() 递归遍历 from / into / rename filter / expand archive: zip/tar inputs / outputs 增量编译 FileChangeListener 核心原则:声明式操作 + 增量构建 = 高效构建 避免手动文件操作,充分利用 Gradle 的缓存和 UP-TO-DATE 机制

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 会帮你搞定剩下的。

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