Gradle动态依赖版本(如1.2.+、[1.0,2.0)、latest.release)导致构建不可复现,因其绕过依赖锁定与缓存校验,每次构建都重新远程解析版本,使同一代码在不同时间产出不同classpath,破坏确定性构建。

Gradle动态依赖版本(比如 1.2.+、[1.0,2.0)、latest.release)会让构建结果不可复现——不是“偶尔失败”,而是“同一份代码、同一台机器、不同时间 sync,产出的 classpath 可能完全不同”。根本原因不在语法错误,而在 Gradle 的依赖解析机制本身是**时间敏感**的。
动态版本会绕过依赖锁定和缓存校验
Gradle 默认不锁定动态版本。哪怕你启用了 dependencyLocking,只要声明里含 + 或区间表达式,该依赖就不会被写入 gradle.lockfile。每次构建时,Gradle 都会重新向仓库发起元数据请求,扫描符合范围的最新可用版本。这意味着:
- 今天解析出
jackson-databind:2.15.2,明天仓库发布2.15.3,后天构建就可能用上它 - 本地缓存中可能同时存着多个匹配版本的 POM 和 JAR,但 Gradle 不保证每次都选同一个
- CI 环境若未预热缓存,首次构建可能拉取到与本地开发环境完全不同的传递依赖树
动态版本破坏语义化版本约束的隐含前提
很多库(如 Spring Boot、Jackson、AGP)依赖语义化版本(SemVer)做兼容性承诺:主版本升级(2.x → 3.x)意味着不兼容变更。但动态版本如 2.+ 会在 2.99.0 发布后自动跳过去——而这个版本很可能已废弃某个 API,或引入 JVM 字节码级不兼容(比如从 Java 17 编译升到 Java 21)。这种问题往往在编译期不暴露,直到运行时抛 NoClassDefFoundError 或 IllegalAccessError。
更隐蔽的是:某些 BOM(如 spring-boot-dependencies)内部用 strictly 锁定子模块版本,但如果你在项目里显式写了 implementation 'com.fasterxml.jackson.core:jackson-databind:2.+',就会覆盖 BOM 的约束,导致整个依赖图失稳。
CI/CD 中动态版本引发“幽灵失败”
你本地 ./gradlew build 成功,CI 却失败?常见于以下组合:
- CI 使用干净 Docker 镜像,无本地缓存 → 每次都触发远程元数据刷新
- 某依赖的仓库临时不可用(如 JCenter 彻底下线),Gradle 自动 fallback 到另一个镜像源,而该镜像源的元数据滞后或不完整
- 团队成员本地用了
--offline,但 CI 没关,结果离线时用的是旧缓存,联网时却拉了新版本
这类失败不会出现在日志开头,而是在编译或测试阶段突然报错,且错误信息与依赖无关(例如 Kotlin 编译器报 Cannot infer type for …),实际根源却是某个 annotation processor 因版本升级悄悄改了 SPI 接口。
真正危险的不是构建失败,而是构建“成功”却产出行为不一致的 APK 或 JAR——动态版本让构建从确定性过程退化为概率性过程。锁死版本、禁用动态符号、用 enforcedPlatform 替代手动范围声明,不是教条,是控制变量的基本要求。


















