bigywater.tech
Java · 431

Gradle 增量构建:任务输入输出决定缓存

Gradle 是否执行一个任务,取决于它声明的输入与输出。这篇用真实项目验证:UP-TO-DATE 比较的是内容哈希、下游只看 ABI、没有声明输入输出会怎样、Java 的增量编译与构建缓存到底省下什么。

0 个执行
没有任何改动的第二次 gradle build:14 个 UP-TO-DATE,0 个执行;只 touch 源文件也一样
2 / 40
只改一个类的方法体,Gradle 增量编译的类数(改公共签名与常量各 4 个)
产物过期
读了 in.txt 却没有声明为任务输入:in.txt 改了,任务仍是 UP-TO-DATE,copy.txt 还是旧内容

任务结果标签与内容哈希

实测 · 本机执行输出
  第一次 gradle build:rc=0,执行 10 个,UP-TO-DATE 4 个,NO-SOURCE 8 个,FROM-CACHE 0 个,共 22 个;耗时 1.0 秒
  第二次 gradle build(没有任何改动):rc=0,执行 0 个,UP-TO-DATE 14 个,NO-SOURCE 8 个,FROM-CACHE 0 个,共 22 个;耗时 0.4 秒
  第二次中 lib 与 app 的 compileJava / jar / test:lib:compileJava=UP-TO-DATE,lib:jar=UP-TO-DATE,app:compileJava=UP-TO-DATE,app:jar=UP-TO-DATE,app:test=NO-SOURCE
  --info 里 compileJava 的跳过原因:Skipping task ':lib:compileJava' as it is up-to-date.
  只 touch 了 Lib.java(内容不变,修改时间推后):lib:compileJava=UP-TO-DATE,lib:jar=UP-TO-DATE,app:compileJava=UP-TO-DATE;执行 0 个,UP-TO-DATE 14 个,NO-SOURCE 8 个,FROM-CACHE 0 个,共 22 个

编译避免:下游只看 ABI

实测 · 本机执行输出
  (a) 只在 Lib.java 末尾加了一行注释:lib:compileJava=EXECUTED,lib:classes=EXECUTED,lib:jar=UP-TO-DATE,app:compileJava=UP-TO-DATE,app:jar=UP-TO-DATE
  (b) 改了 private 方法体(x * 2 → x * 3):lib:compileJava=EXECUTED,lib:jar=EXECUTED,app:compileJava=UP-TO-DATE,app:jar=UP-TO-DATE
  (c) 新增了 public 方法 api2():lib:compileJava=EXECUTED,lib:jar=EXECUTED,app:compileJava=EXECUTED,app:jar=UP-TO-DATE;原因:Input property 'classpath' file <jtmp>/j291a/lib/build/classes/java/main/Lib.class has changed.

声明的输入与输出

实测 · 本机执行输出
  genVersion 第一次:EXECUTED;原因:No history is available.
  genVersion 第二次(没有改动):UP-TO-DATE;原因:(无,被跳过)
  genVersion -Pver=2:EXECUTED;原因:Value of input property 'ver' has changed for task ':genVersion'
  genVersion 手工改了输出文件 ver.txt:EXECUTED;原因:Output property '$1' file <jtmp>/j291a/build/ver.txt has changed.
  genVersion 删除输出文件 ver.txt:EXECUTED;原因:Output property '$1' file <jtmp>/j291a/build/ver.txt has been removed.
  noOutputs(没有声明输出)第一次 EXECUTED、第二次 EXECUTED;原因:Task has not declared any outputs despite executing actions.
  undeclaredInput(读了 in.txt 却没声明为输入):第一次 EXECUTED,copy.txt = 'old';把 in.txt 改成 new 之后第二次 UP-TO-DATE,copy.txt = 'old'(in.txt = 'new',产物过期了)

Java 增量编译

实测 · 本机执行输出
  干净后的完整编译:没有增量编译日志(日志:Full recompilation is required because no incremental change information is available);compileJava=EXECUTED
  (a) C20.f 的方法体改了(签名不变):增量编译 2 个类
  (b) C0.f 的签名改成 long f(long):增量编译 4 个类
  (c) 常量 C0.K 由 1 改成 2(C5、C9 引用):增量编译 4 个类
  (d) 没有再改:compileJava=UP-TO-DATE

构建缓存

实测 · 本机执行输出
  缓存开启后第一次 clean build:执行 10 个,UP-TO-DATE 4 个,NO-SOURCE 8 个,FROM-CACHE 0 个,共 22 个;耗时 0.5 秒
  clean 之后第二次 build:执行 6 个,UP-TO-DATE 6 个,NO-SOURCE 8 个,FROM-CACHE 2 个,共 22 个;耗时 0.4 秒
  第二次的 FROM-CACHE 任务:lib:compileJava,app:compileJava
  改了 C20 后 clean build:执行 8 个,UP-TO-DATE 5 个,NO-SOURCE 8 个,FROM-CACHE 1 个,共 22 个;lib:compileJava=EXECUTED(输入变了,缓存键变了),app:compileJava=FROM-CACHE
  本地构建缓存里的条目数 5

分步演示

Gradle 增量构建
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Gradle 9.8.0(官方发行版,sha256 校验通过,守护进程,隔离的 GRADLE_USER_HOME,--offline),JDK 21.0.12,java / java-library 插件,没有第三方依赖;任务结果与原因取自 --info 日志;实验结束后 Gradle 与其缓存已删除
    • 没有验证:大型项目里增量编译与构建缓存带来的实际时间收益(这里的小项目只有零点几秒)、远程构建缓存、配置缓存(configuration cache)、注解处理器对增量编译的影响、Kotlin / Groovy 编译、测试任务的缓存与 flaky 测试、Windows 上的行尾符与路径对缓存键的影响
    京ICP备2026067502号-1京公网安备11010502063487号