## 2. 自定义 Gradle Task 实现文件加密:输入输出文件、doLast/doFirst、增量构建
### 2.1 本章原理概述
实现编译时文件加密的核心思路是利用 Gradle Task 的文件操作能力,在构建生命周期内对指定文件进行加密转换。本章将聚焦于如何编写一个自定义 Task,使其接受明文输入文件、输出加密后的文件,并利用 Gradle 提供的增量构建特性避免重复加密未变更的文件。
关键原理:
- **Input/Output 声明**:通过 `@InputFile`/`@OutputFile` 注解或 `@InputFiles`/`@OutputFiles` 集合声明,使 Gradle 能够追踪文件状态(内容 hash、时间戳等),实现增量构建。
- **doLast/doFirst 生命周期钩子**:允许在 Task 动作执行前后插入额外逻辑,例如资源清理、状态记录、签名校验。
- **增量构建判定**:当输入文件未变更且输出文件存在且内容匹配时,Gradle 自动跳过 Task,避免重复计算。
每一个自定义 Task 都应正确声明输入/输出,否则 Gradle 将认为该 Task 永远不会 UP-TO-DATE,导致每次构建都完整执行。
### 2.2 实现流程
| 步骤 | 操作内容 | 关键代码/配置 |
|------|----------|---------------|
| 1 | 创建自定义 Task 类,继承 `DefaultTask` | `class FileEncryptTask extends DefaultTask { ... }` |
| 2 | 声明输入文件属性(`@InputFile`)和输出文件属性(`@OutputFile`) | `@InputFile File inputFile; @OutputFile File outputFile;` |
| 3 | 定义加密算法(如 AES 简单示例),通过 `@TaskAction` 标注执行方法 | `@TaskAction void encrypt() { ... }` |
| 4 | 在 `build.gradle` 中注册 Task 实例并配置输入/输出 | `tasks.register('encryptConfig', FileEncryptTask) { ... }` |
| 5 | 使用 `doFirst`/`doLast` 添加额外操作(如日志、备份) | `doFirst { println "Encrypting $inputFile ..." }` |
| 6 | 验证增量构建:修改输入文件再次构建,观察 Task 状态 | `./gradlew encryptConfig` |
### 2.3 完整代码实现
**FileEncryptTask 类定义**(放在 `buildSrc/src/main/groovy/com/example/FileEncryptTask.groovy` 或直接写在 `build.gradle` 中):
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class FileEncryptTask extends DefaultTask {
@InputFile
File inputFile
@OutputFile
File outputFile
@TaskAction
void encrypt() {
// 简单的 AES 加密示例(仅演示,生产环境需使用安全密钥管理)
def key = new SecretKeySpec("1234567890123456".bytes, "AES")
def cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, key)
def plainBytes = inputFile.bytes
def encryptedBytes = cipher.doFinal(plainBytes)
outputFile.bytes = encryptedBytes
logger.lifecycle("Encrypted {} -> {} ({} bytes)", inputFile.name, outputFile.name, encryptedBytes.length)
}
}
**在 `build.gradle` 中注册 Task 并配置输入输出**:
tasks.register('encryptConfig', FileEncryptTask) {
inputFile = file('src/main/resources/config.properties')
outputFile = file("${buildDir}/encrypted/config.properties.enc")
doFirst {
println "=== 开始加密任务 ==="
// 可在此添加输入文件存在性检查
if (!inputFile.exists()) {
throw new GradleException("Input file ${inputFile} does not exist!")
}
}
doLast {
println "=== 加密任务完成 ==="
// 可在此输出摘要或触发后续 Task
}
}
### 2.4 增量构建行为验证
运行构建命令:
# 首次执行(会加密)
./gradlew encryptConfig
# 输出:> Task :encryptConfig
# 第二次执行(未修改输入文件,跳过)
./gradlew encryptConfig
# 输出:> Task :encryptConfig UP-TO-DATE
# 修改输入文件后再执行(重新加密)
echo "# new config" >> src/main/resources/config.properties
./gradlew encryptConfig
# 输出:> Task :encryptConfig
### 2.5 doFirst 与 doLast 的典型用途
| 钩子 | 典型场景 | 示例 |
|------|----------|------|
| `doFirst` | 输入校验、备份原文件、打印开始日志 | `doFirst { backup(inputFile) }` |
| `doLast` | 输出结果验证、生成校验和、触发下游 Task | `doLast { checkIntegrity(outputFile) }` |
⚠️ 注意:`doFirst`/`doLast` 内部修改 `inputFile` 或 `outputFile` 的引用不会影响增量构建追踪,因为它们执行在 Task 动作之外,Gradle 只检查 `@Input`/`@Output` 注解的属性的值。
### 2.6 常见错误与调试
- **错误 1:Task 始终不缓存**
原因:未正确声明 `@OutputFile`,或将输出文件写到了非 `@OutputFile` 的临时目录。
解决:检查属性是否标注 `@OutputFile` 或 `@OutputDirectory`,并确保文件写入位置与声明一致。
- **错误 2:增量子系统认为输出不变**
原因:修改了输出文件内容,但 hash 未正确更新(例如使用 `new FileOutputStream` 追加而不是覆盖)。
解决:使用 `.bytes = ...` 或带 `replaceExisting` 选项的方式写文件。
- **错误 3:加密密钥硬编码安全隐患**
原因:密钥直接写在代码中。
解决:通过 `@Input` 属性的方式注入密钥文件或使用系统属性。
### 2.7 性能与稳定性优化
1. **使用 `@Incremental` 注解**(Gradle 4.6+):当输入文件集合时(如 `@InputFiles`),可声明为增量输入以获取变更的具体文件列表。
2. **避免在 `doFirst`/`doLast` 中执行耗资源操作**:这些钩子每次构建都会执行,即使 Task 被标记为 UP-TO-DATE。
3. **输出文件路径使用构建缓存兼容结构**:将输出放在 `buildDir` 下,方便团队共享缓存。
4. **对加密算法设置可配置参数**:通过 `@Option` 或扩展属性允许用户调整算法、密钥长度。
### 2.8 内联 SVG 流程图
### 2.9 调试技巧
- 使用 `--info` 日志查看 Task 的 `UP-TO-DATE` 判定原因:
./gradlew encryptConfig --info
输出示例:
Task ':encryptConfig' is UP-TO-DATE because:
No changes in input files.
Output file build/encrypted/config.properties.enc exists.
- 若需要强制重新执行(忽略增量),使用 `--rerun-tasks`:
./gradlew encryptConfig --rerun-tasks
### 2.10 部署与集成
在模块化多项目构建中,可将自定义 Task 类放入 `buildSrc` 复用,并在需要加密的子项目中注册:
// settings.gradle
include 'service-a', 'service-b'
// service-a/build.gradle
tasks.register('encryptServiceAConfig', FileEncryptTask) {
inputFile = file('src/main/resources/conf/secret.properties')
outputFile = file("${buildDir}/encrypted/secret.properties.enc")
}
通过 `dependsOn` 将加密 Task 链接到 `processResources` 之前,确保加密后的文件可用于运行时:
processResources.dependsOn encryptServiceAConfig
### 2.11 扩展思考
- **多文件加密**:使用 `@InputFiles @OutputDirectory` 批量处理目录下所有文件。
- **密钥管理**:将密钥存储为 `@InputFile` 指向的安全文件,并通过 Gradle 加密扩展插件(如 `gradle-encrypt-plugin`)预处理。
- **并行加密**:在 `@TaskAction` 中使用 `fileTree` 结合 `parallelStream` 并行加密多个文件,但需注意 `@OutputFile` 声明方式。