文件操作:文件集合、文件树、复制与归档、文件监听与变更

文件操作,说白了就是 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 倍。嗯,这里要注意:文件集合是惰性求值的,只有当你调用 eachasFileTree 时才会真正去磁盘上读。

核心要点:文件集合支持 +-intersect 操作,但要注意类型安全——混合 FileString 时,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())
    }
}

归档操作就更直接了——ZipTarJar 都是 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 任务的 frominto 都是惰性的。如果你在配置阶段修改了源文件,任务执行时才会生效。我曾经踩过这个坑:在 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)来检测变更。每次构建后,它会记录文件的哈希值和修改时间。下次构建时,对比快照就能知道哪些文件变了。

知识体系总览

下面这张图展示了文件操作的核心层次关系。你可以看到,文件集合是基础,文件树是它的子集,复制归档和文件监听都建立在它们之上。

Gradle 文件操作体系 FileCollection 文件集合:惰性求值、集合运算 FileTree 文件树:目录遍历、Ant 模式匹配 Copy / Zip / Tar / Jar 复制与归档:重命名、过滤、权限 InputChanges / @Incremental 文件监听:增量构建、变更检测 关键特性 • 懒加载:文件集合在需要时才读取磁盘 • 链式过滤:include/exclude/filter 顺序执行 • 声明式 API:from/into/rename 清晰直观 • 增量构建:只处理变更的文件 • 快照机制:哈希+时间戳双重检测 • 权限保留:Unix 文件权限可配置 • 编码支持:UTF-8 中文文件名无乱码 • 任务依赖:自动处理输入输出关系

实战:组合使用

在实际项目中,这些 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 是三个核心原则。掌握了这些,你就能写出既高效又优雅的构建脚本。

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