9、文件操作API:文件集合(FileCollection)、文件树(FileTree)、复制与归档任务

说实话,Gradle 里文件操作这块,是我早期踩坑最多的地方。你想想看,一个构建工具,天天跟文件打交道——源码、资源、配置文件、打包产物……要是文件操作搞不明白,那构建脚本写起来真是寸步难行。

我个人习惯把 Gradle 的文件 API 分成三个层次:文件集合文件树复制与归档任务。咱们一层一层剥开看。

9.1 文件集合(FileCollection)——最基础的文件容器

FileCollection 说白了就是一个文件列表。它不关心目录结构,只关心「有哪些文件」。

我刚开始用 Gradle 时,以为文件操作就是写写 file()files()。后来才发现,FileCollection 的延迟计算特性才是精髓。

核心特性:FileCollection 是懒加载的。你定义的时候,文件可能还不存在。只有在真正使用的时候,它才会去解析路径。

看个例子:

// 创建一个文件集合
def myFiles = files('src/main/java', 'src/main/resources')

// 可以追加
myFiles += files('src/test/java')

// 可以过滤
def javaFiles = myFiles.filter { it.name.endsWith('.java') }

// 可以遍历
myFiles.each { file ->
    println "文件: ${file.name}"
}

嗯,这里要注意:files() 返回的是 ConfigurableFileCollection,它是可变的。但如果你用 fileCollection() 从任务获取,那通常是只读的。

我的经验:在项目中,我习惯用 FileCollection 来管理「需要处理的源文件列表」。比如自定义代码生成任务,输入文件集合定义好,后面怎么过滤、怎么转换都方便。

9.2 文件树(FileTree)——带目录结构的文件集合

FileTree 是 FileCollection 的子接口。它多了一个东西:目录结构信息。你可以按目录遍历,可以匹配 Ant 风格的路径模式。

说白了,FileCollection 是「平铺」的,FileTree 是「树形」的。

// 从目录创建文件树
def tree = fileTree('src/main/java') {
    include '**/*.java'
    exclude '**/test/**'
}

// 从 ZIP 文件创建文件树
def zipTree = zipTree('build/dist/myapp.zip')

// 遍历文件树(带目录信息)
tree.visit { fileDetails ->
    println "${fileDetails.relativePath} - ${fileDetails.file.size()} bytes"
}

我曾经在做一个多模块项目时,需要把每个模块的特定资源文件提取出来。用 FileTree 的 matching 方法配合 include/exclude,一行代码就搞定了过滤逻辑。

避坑指南:我曾经犯过一个错误——在配置阶段就遍历 FileTree。那时候文件还没生成呢,遍历结果当然是空的。记住:文件树的解析是在执行阶段,不要在配置阶段依赖它的内容。

9.3 复制任务(Copy)——最常用的文件操作任务

Copy 任务是我用得最多的 Gradle 内置任务。它的设计很优雅:声明式配置,你只需要告诉它「从哪来」「到哪去」「要什么」。

task copyResources(type: Copy) {
    from 'src/main/resources'
    into 'build/resources'
    include '**/*.properties'
    exclude '**/secret.properties'
    
    // 还可以重命名
    rename { fileName ->
        fileName.replace('.properties', '.config')
    }
    
    // 或者过滤内容
    filter { line ->
        line.replace('${version}', project.version.toString())
    }
}

你想想看,如果用 Java 的 Files.copy() 实现同样的功能,得写多少代码?Gradle 的 Copy 任务把这些都封装好了。

核心机制:Copy 任务内部使用了 CopySpec 接口。这个接口支持嵌套,你可以把多个复制规范组合在一起。
task deploy(type: Copy) {
    // 第一组:配置文件
    into 'build/deploy/config'
    from('src/main/config') {
        include '*.xml'
    }
    
    // 第二组:可执行文件
    into 'build/deploy/bin'
    from('build/libs') {
        include '*.jar'
    }
    
    // 第三组:文档
    into 'build/deploy/docs'
    from('src/docs') {
        include '*.md'
    }
}

我个人特别喜欢这种嵌套写法。它让复杂的复制逻辑变得清晰可读。每个 from 块都可以有自己的 include/exclude,互不干扰。

9.4 归档任务(Archive)——打包的艺术

归档任务是 Copy 任务的「升级版」。它把复制结果直接打包成 ZIP、TAR、JAR 等格式。

Gradle 内置了三种归档任务:

任务类型 默认扩展名 典型用途
Zip .zip 通用打包
Tar .tar.gz Linux 环境
Jar .jar Java 库/应用
task dist(type: Zip) {
    archiveFileName = "myapp-${version}.zip"
    destinationDirectory = file('build/dist')
    
    from 'build/libs'
    from('src/main/scripts') {
        into 'bin'
        fileMode = 0755
    }
    from('src/main/config') {
        into 'conf'
    }
}

为什么说归档任务是 Copy 的升级版?因为它继承了 CopySpec 的所有能力——过滤、重命名、嵌套目录结构,全部支持。你只需要在最后指定一下归档格式就行。

我的习惯:在项目中,我通常把归档任务和构建任务解耦。构建任务只负责生成产物,归档任务单独配置。这样 CI/CD 流水线里可以灵活选择打包方式。

9.5 知识体系总览

下面这张图是我梳理的文件操作 API 核心逻辑,你看一眼就能明白它们之间的关系:

Gradle 文件操作 API 核心结构 FileCollection(文件集合) FileTree(文件树) • 懒加载 • 可变/不可变 • 目录结构感知 • Ant 风格匹配 CopySpec(复制规范) • 嵌套规范 • 过滤/重命名 Copy 任务(复制) Archive 任务(归档) • 声明式配置 • 执行阶段复制 • Zip / Tar / Jar • 继承 CopySpec

9.6 实战:组合使用

光讲理论没意思。我分享一个真实项目中的场景:

当时我们需要构建一个微服务部署包。要求是:把编译好的 JAR、配置文件、启动脚本打包成一个 ZIP,同时把配置文件中的占位符替换成实际值。

task buildDeployPackage(type: Zip) {
    archiveFileName = "service-${version}.zip"
    destinationDirectory = file("${buildDir}/deploy")
    
    // 1. 复制 JAR 包
    from tasks.named('bootJar') {
        into 'lib'
    }
    
    // 2. 复制启动脚本并设置权限
    from('src/deploy/scripts') {
        into 'bin'
        include 'start.sh', 'stop.sh'
        fileMode = 0755
    }
    
    // 3. 复制配置文件并替换占位符
    from('src/deploy/config') {
        into 'conf'
        include '*.yml'
        filter { line ->
            line
                .replace('${app.version}', version)
                .replace('${app.port}', port.toString())
        }
    }
    
    // 4. 复制文档
    from('docs') {
        into 'docs'
        include 'README.md'
    }
}

你看,一个任务就把所有事情搞定了。这就是 Gradle 文件操作 API 的魅力——组合能力强,表达力高

注意:使用 filter 时要注意编码问题。我曾经在 Windows 上构建时,因为默认编码不是 UTF-8,导致配置文件中的中文乱码。建议显式指定编码:filteringCharset = 'UTF-8'

9.7 小结

文件操作 API 是 Gradle 构建脚本的「基础设施」。掌握了 FileCollection、FileTree、Copy 和 Archive,你就能应对绝大多数文件处理场景。

我个人觉得,理解「延迟计算」和「声明式配置」这两个理念,比记住具体 API 更重要。理念通了,API 只是工具。

嗯,文件操作这块就聊到这儿。记住:能用 CopySpec 组合解决的问题,不要写循环。Gradle 的设计者已经帮你考虑好了。


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