2. Gradle 构建生命周期与 Task 执行时机:理解配置阶段、执行阶段、加密时机选择

在 Gradle 中实现编译时数据加密,首先必须理解构建生命周期的三个核心阶段——初始化、配置、执行。加密操作如果放在错误的阶段,轻则导致每次构建重复执行、破坏增量缓存,重则引发任务依赖混乱、加密失效。本章将深入剖析各阶段特征,并给出加密时机的明确选择原则。

2.1 构建生命周期的三个阶段

Gradle 构建过程由 Gradle Daemon 或 CLI 触发,依次经历以下阶段:

阶段 触发时机 主要行为 可执行 Task?
初始化 (Initialization) 用户执行 gradle build 等命令后 解析 settings.gradle(.kts),确定参与构建的项目 否
配置 (Configuration) 初始化完成后,对每个项目解析 build.gradle(.kts) 执行 build 脚本中的配置逻辑,创建 Task 对象并注册依赖关系 否(Task 代码仅被注册,不执行)
执行 (Execution) 配置完成后,根据依赖图确定需要运行的 Task 集合 按序执行每个 Task 的 doLast / doFirst 闭包 是
加密时机核心原则:加密逻辑必须放置在 执行阶段 的 Task 动作内(doLast 或 doFirst)。严禁在配置阶段直接执行文件 I/O、加解密运算等耗时操作,否则会严重降低构建性能并破坏增量构建机制。

2.2 配置阶段 vs 执行阶段:加密操作的致命陷阱

许多初学者会将加密逻辑直接写在 build.gradle 的顶层脚本中(即配置阶段),例如:

// ❌ 错误做法:配置阶段执行加密
def encrypted = new String(Base64.encoder.encode("myPassword".getBytes()))
// 后续创建 Task 时使用 encrypted 变量
task encryptConfig {
    // ...
}

问题:这段代码在每次配置阶段都会执行一次解密/加密操作,即使对应 Task 没有实际运行(例如仅执行 gradle tasks)。配置阶段完成后,加密结果并不会被缓存,下次构建又会重复计算,导致构建时间膨胀。

正确的做法是将加密逻辑封装在 Task 的动作方法中:

// ✅ 正确做法:加密在执行阶段执行
tasks.register("encryptConfig") {
    doLast {  // 执行阶段才运行
        def inputFile = layout.projectDirectory.file("config/secret.properties")
        def outputFile = layout.buildDirectory.file("encrypted/secret.enc")
        inputFile.get().asFile.withInputStream { is ->
            outputFile.get().asFile.withOutputStream { os ->
                os << encryptStream(is)  // 假设 encryptStream 已实现
            }
        }
    }
}

2.3 生命周期流程图:加密时机选择

Gradle 构建生命周期与加密任务执行时机 1. 初始化阶段 解析 settings.gradle,确定参与项目 2. 配置阶段 执行 build.gradle 脚本,注册 Task 对象 ⚠ 不可在此阶段执行加密 I/O 操作 3. 执行阶段 按 Task 依赖顺序执行 doLast / doFirst 闭包 ✅ 加密任务在此处执行 构建完成 加密任务必须配置在执行阶段,且应避免在配置阶段引用加密结果

2.4 加密 Task 的执行顺序控制

在构建中,加密任务通常需要先于编译任务执行。使用 dependsOn 和 mustRunAfter 控制执行顺序:

tasks.register("encryptResources") {
    doLast {
        // 加密资源文件
        project.fileTree("src/main/resources") {
            include "*.properties"
        }.each { file ->
            // 加密逻辑
        }
    }
}

tasks.named("processResources") {
    dependsOn("encryptResources")
}

// 或更精细控制:encryptResources 必须在 processResources 之前
tasks.named("processResources") {
    mustRunAfter("encryptResources")
}
注意:如果加密脚本在配置阶段读取了加密结果(例如通过 file() 读取加密后的文件),那么该文件在配置阶段必须已存在,否则会抛出 FileNotFoundException。确保所有加密文件只在使用 Task 输出时引用,或者在配置阶段通过 gradle.projectsEvaluated 回调检查。

2.5 调试构建阶段与 Task 执行

使用 Gradle 的日志选项查看阶段详情:

# 查看 Task 执行顺序及耗时
gradle build --info

# 查看依赖图(配置阶段后输出)
gradle build --dry-run

# 仅查看配置阶段(不执行 Task)
gradle build --configure-on-demand --dry-run

在 --info 日志中,你可以看到类似以下的输出,确认加密任务是否在正确阶段执行:

Configuring project :app
Task :encryptResources configured
Task :processResources configured
Task :compileJava configured
...
Executing task :encryptResources (up-to-date)
Executing task :processResources (up-to-date)
Executing task :compileJava

2.6 常见错误与解决方案

错误现象 根源 解决方案
每次 gradle tasks 都执行加密导致极慢 加密逻辑写在配置阶段全局闭包中 将加密移至 Task 的 doLast 或 doFirst
加密结果未随输入文件变化而重新生成 未正确声明 Task 输入/输出 使用 @InputFile 和 @OutputFile 注解,启用增量构建
编译时找不到加密后的文件 Task 顺序未配置,加密任务未先于 processResources 执行 添加 dependsOn 或 mustRunAfter
不同项目间加密顺序错乱 多项目构建时跨项目依赖未指定 使用跨项目 Task 引用:project(":other").tasks.named("encrypt")

2.7 性能优化建议

  • 利用 Gradle 的增量构建:为加密 Task 显式声明 @Input 和 @Output,避免每次构建都重复加密未变更的文件。
  • 避免在配置阶段执行文件 I/O:所有文件读取/写入应放在执行阶段。
  • 使用 Worker API 并行加密:对于大量文件,使用 Worker API 在后台线程池中并行执行加密,不影响主线程。
  • 加密结果缓存:如果加密过程非常耗时,考虑将结果存入构建缓存(Build Cache),这样即使开发者在多台机器上构建,也能复用加密结果。
// 示例:增量构建声明
abstract class EncryptTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getInputFile()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void encrypt() {
        inputFile.get().asFile.withInputStream { is ->
            outputFile.get().asFile.withOutputStream { os ->
                // 加密逻辑
            }
        }
    }
}

tasks.register("encryptConfig", EncryptTask) {
    inputFile.set(layout.projectDirectory.file("config/secret.properties"))
    outputFile.set(layout.buildDirectory.file("encrypted/secret.enc"))
}
调试小技巧:在执行阶段遇到加密失败时,使用 --stacktrace 查看完整的异常栈;如果怀疑配置阶段存在副作用,可以在 build.gradle 中临时加入 println "Configuring at ${System.currentTimeMillis()}" 来观察配置被调用的次数。