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 生命周期流程图:加密时机选择
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()}" 来观察配置被调用的次数。