双十一备战的那些天,我接手了一个运行了 5 年的电商核心交易系统。这个系统由 32 个 Maven 模块组成,代码量超过 80 万行,每次全量构建需要 4 分 20 秒。当时我们面临一个棘手的问题:团队准备引入 Kotlin 协程处理高并发订单,但 Maven 对多语言混合项目的构建配置越来越臃肿,而且依赖冲突排查几乎成了每周的例行公事。
我决定将这个系统迁移到 Gradle 8.10(2024 年 8 月 14 日发布的最新版本)。选择这个版本的原因在于它针对大型企业级项目做了多项性能优化,特别是增量构建的精度提升和配置阶段的加速。迁移前我们做了详细对比测试,在完全相同的硬件环境(16 核 CPU/32GB 内存/SSD 硬盘)下,Maven 3.9.6 与 Gradle 8.10 的表现差异显著。
Maven 的依赖管理机制在处理传递依赖时经常让我们陷入困境。有一次订单服务的接口突然变慢,QPS 从 1200 掉到 400,排查了两天才发现是 Maven 依赖调解机制选择了错误版本的 Netty 库。Gradle 的依赖约束(Dependency Constraints)和依赖锁定(Dependency Locking)机制从根本上解决了这个问题。我在 build.gradle.kts 中这样定义关键依赖:
迁移过程中最耗时的部分是重写构建逻辑。Maven 的 XML 配置在复杂场景下可读性很差,比如我们原来的 pom.xml 里有 200 多行插件配置来处理多环境打包。Gradle 的 Kotlin DSL 让这些配置变得简洁且类型安全。我花了三天时间将核心构建逻辑迁移完毕,结果令人惊喜:全量构建时间从 260 秒降至 95 秒,降幅达 63%。原因在于 Gradle 的增量构建机制会检查任务的输入和输出是否变化,而 Maven 的增量编译支持相对有限。
对于多模块项目,Gradle 的复合构建(Composite Builds)特性帮我们解决了另一个痛点。我们的支付模块需要同时修改并调试 5 个相关模块,在 Maven 中需要安装到本地仓库才能引用,而 Gradle 允许直接包含其他构建:
迁移后我们遇到的第一个实际问题是在 CI 环境中构建缓存命中率只有 30%。经过排查发现是 build.gradle.kts 中使用了 System.currentTimeMillis() 作为任务输入,导致每次构建都不同。解决方案是改用 Gradle 的 Provider API 进行延迟配置,避免在配置阶段执行耗时操作。调整后缓存命中率提升到 85%,CI 构建时间从平均 3 分钟降至 50 秒。
当我们的微服务架构扩展到 47 个模块时,依赖管理变成了噩梦。不同模块中 Spring Boot 版本不一致,某个模块的 Jackson 库还是 2.13 版本,而新模块已经用到 2.16。我决定引入 Gradle 8.10 的版本目录(Version Catalogs)功能来统一管理依赖。
我先在 gradle/libs.versions.toml 中定义了所有依赖的中央目录:
这个目录文件成为团队依赖管理的唯一真相来源。有一次新同事在订单模块引入了旧版本的 MyBatis,CI 流水线立即报错,因为版本目录中没有定义该版本。相比之前每个模块自由指定版本的情况,现在依赖冲突减少了 90%。
Kotlin DSL 的优势在配置多模块构建时尤为明显。我们的商品服务包含 8 个子模块,以前用 Groovy DSL 时经常出现配置错误,因为缺少编译时检查。迁移到 Kotlin DSL 后,我在根项目的 build.gradle.kts 中这样统一配置子模块:
版本目录的 bundles 功能让我们能定义依赖组合。比如我们的微服务基础包包含 Web、JSON 处理和网络库,现在只需一行代码即可引入:
在实际使用中,我发现版本目录的一个实用技巧是结合 dependencyVerification 来防止依赖篡改。我们在 CI 中启用了依赖校验,任何依赖的哈希值变化都会阻断构建。去年有一次我们的构建突然失败,排查发现是某个依赖库在 Maven 中央仓库被重新上传了相同版本但内容不同的包,依赖校验机制及时阻止了这次潜在的安全风险。
对于大型团队,我建议将版本目录文件单独维护在一个 Git 仓库中,通过 Gradle 的 includeBuild 引入。这样依赖升级可以独立发布,各业务线按需更新。我们团队采用这种方式后,Spring Boot 版本升级从原来需要修改 47 个模块的 pom.xml 变成只需修改一个 TOML 文件,升级时间从 2 天缩短到 2 小时。
今年初我们的 CI 构建时间随着项目规模增长逐渐失控,从提交代码到拿到构建结果需要 8-12 分钟。我深入分析构建日志发现,配置阶段就占了 35% 的时间,而且每个开发者的本地构建几乎无法复用他人的缓存。针对这些问题,我基于 Gradle 8.10 的特性进行了系统调优。
配置缓存(Configuration Cache)是 Gradle 8.10 中接近稳定状态的核心特性。它的原理是将配置阶段的结果序列化存储,后续构建直接复用。启用方式很简单,在 gradle.properties 中添加:
但实际启用时我们遇到了兼容性问题。我们的自定义插件中有一个任务在配置阶段执行了项目评估,导致缓存失效。解决方案是重构插件代码,使用 Gradle 的 Provider API 延迟配置:
经过两周的插件适配,我们成功启用了配置缓存。效果立竿见影:配置阶段时间从平均 12 秒降至 0.8 秒,因为 Gradle 直接复用了之前的配置结果。
远程构建缓存的部署对大型团队收益更大。我们搭建了一个基于 Gradle 企业版(现在叫 Develocity)的缓存节点,所有 CI 机器和开发者共享缓存。在 settings.gradle.kts 中配置:
调优过程中最关键的发现是缓存命中率与任务输入的相关性。我们通过构建扫描(Build Scan)分析发现,测试任务的缓存命中率只有 40%,原因在于测试依赖的配置文件包含了构建时间戳。解决方案是分离动态内容和静态内容:
另一个重要优化是调整 Gradle 的并行执行和内存配置。我们在 gradle.properties 中设置了:
经过三个月的持续调优,构建性能数据有了质的飞跃:本地全量构建从 210 秒降至 85 秒,CI 环境从 480 秒降至 190 秒,缓存命中率从 30% 提升到 78%。最让我满意的是开发体验的改善,现在修改一行代码后,增量构建通常在 10 秒内完成。
调优过程中我学到的最重要经验是:构建性能优化不是一次性的工作,需要建立监控机制。我们现在每次构建都会生成构建扫描,每周分析缓存命中率和构建时间趋势。有一次我们发现构建时间突然增加了 20%,通过扫描分析发现是新引入的代码生成插件没有正确声明输入输出,修复后构建时间恢复正常。
去年双十一大促前,我负责的一个订单核心服务(Spring Boot 2.7 + Gradle 7.6)在 CI 上构建耗时从平时的 2 分钟飙到了 8 分钟。当时我第一反应是依赖下载慢,但排查下来发现是大量任务在重复执行,完全没有利用到增量构建。升级到 Gradle 8.10(2024 年 8 月 14 日发布)后,配合配置缓存,构建时间直接降到了 1 分 20 秒。
Gradle 的增量构建不是玄学,它基于 Task 的输入(Inputs)和输出(Outputs) 的快照(Snapshot)来判断。如果前后两次执行,输入没变、输出存在且内容一致,这个 Task 就会被标记为 UP-TO-DATE,直接跳过。
我在一个数据处理模块里写了一个自定义 Task,用来生成 Proto 文件对应的 Java 代码。最初我没加 @Input 和 @Output,导致每次 ./gradlew build 都重新生成,哪怕源文件没动。
为什么这么做:不声明 IO,Gradle 会认为这个 Task 每次都需要执行。在我们的订单系统里,Proto 文件改动频率极低,但每次构建都要跑一遍,白白浪费了 15 秒。加上注解后,除了 Proto 文件变更,这个 Task 基本都是 UP-TO-DATE。
去年有个支付模块,引入了两个 SDK:A 依赖 okhttp:4.9.0,B 依赖 okhttp:4.11.0。Gradle 默认会选最高版本 4.11.0,结果运行时报了 NoSuchMethodError。排查发现 4.11.0 里有个方法签名改了,而 A 没适配。
我没有直接用 exclude,而是用了 依赖约束(Constraints) 强制统一版本:
为什么不这么做会怎样:如果只用 exclude 把 B 里的 okhttp 踢掉,哪天 B 更新了,你可能忘了这茬,又引入了一个奇怪的版本。用 constraints 能把决策原因(because)写进代码,后续维护的人一看就懂。
我们公司有 30 多个微服务,每个都要配置 Docker 镜像构建、Git 版本号注入。以前是复制粘贴 build.gradle 代码,改一个全改。后来我把这些逻辑抽成了一个自定义插件。
这是我在 Gradle 8.10 下写的一个简化版插件,用来统一处理版本号和 Jar 包 Manifest:
然后在业务模块的 build.gradle.kts 里一行搞定:
为什么这么做:以前改个 sourceCompatibility 要改 30 个文件。现在改插件,所有项目自动生效。Gradle 8.10 对 buildSrc 的编译速度也做了优化,这种插件开发体验已经很接近写业务代码了。
---
上个月,我们的一个核心交易服务在测试环境突然启动失败,报 ClassNotFoundException。本地跑得好好的,CI 上却炸了。我当时盯着 Jenkins 日志看了半天,最后发现是 依赖锁定(Dependency Locking) 在 CI 环境下失效了。
那个服务用了 com.google.guava:guava。本地我跑 ./gradlew dependencies 看到的是 31.1-jre,但 CI 日志里显示下载的是 32.0.1-android。这导致了一个针对 JRE 的方法找不到。
我当时的 build.gradle.kts 是这样写的:
gradle.lockfile,里面确实有 com.google.guava:guava:31.1-jre。./gradlew build --no-build-cache。rm -rf ~/.gradle/caches。但我忽略了一个细节:Gradle 8.10 的锁定机制默认是“建议性”的。如果本地没有生成 gradle.lockfile(或者 CI 拉代码时没拉这个文件),Gradle 会忽略锁定,直接解析最新版本。原来是我们的 Git 配置里,把 *.lockfile 加入了 .gitignore,导致锁定文件根本没提交上去!
我做了两件事:
.gitignore,移除对 gradle.lockfile 的忽略。CI 脚本调整:
为什么这么做:如果不强制提交锁定文件,团队里谁要是忘了生成锁文件,或者像我一样被 .gitignore 坑了,CI 就会退化成“看天吃饭”的动态依赖解析。对于金融类系统,这种不确定性是不可接受的。discardIfMissing=false 这个参数(虽然文档里藏得深)能确保没有锁文件时构建直接失败,把问题暴露在开发阶段。
---
今年我们开始把部分边缘服务从 Spring Boot 的 FatJar 迁移到 GraalVM Native Image。启动时间从 4.5 秒降到了 0.15 秒,内存占用从 512MB 降到了 90MB。在这个过程中,Gradle 8.10 的 Native Image 插件 给了我们很大支持,但也暴露了一些配置上的痛点。
Gradle 8.10 对 GraalVM 的支持主要通过 org.graalvm.buildtools.native 插件。我在一个高并发的网关服务里集成了它:
为什么这么做:直接手写 native-image 命令太痛苦了,参数多且容易出错。用 Gradle 插件可以把这些配置版本化管理。不过,目前这个插件的构建速度还是慢,我们那个网关服务生成 Native Image 需要 3 分钟,比普通 Jar 包慢了 10 倍。这也是 2024-2026 年 Gradle 发展趋势 里提到的“性能持续优化”的重点方向。
我们在 CI/CD 里最头疼的是“配置阶段”太慢。Gradle 8.10 正在推进 配置缓存(Configuration Cache) 的稳定化。开启后,配置阶段的结果会被缓存,下次直接复用。
我在 gradle.properties 里开启了实验性支持:
为什么不这么做会怎样:如果不开启配置缓存,我们那个有 50 个子模块的大项目,每次 CI 光是解析 build.gradle.kts 就要花掉 40 秒。开启后,如果代码没改,配置阶段直接降到 2 秒。不过目前很多第三方插件(包括一些老旧的 GraalVM 插件版本)还不兼容配置缓存,这也是我为什么还在用 warn 模式,而不是直接 fail。
我们团队有 20 多个开发,CI 机器有 10 台。以前大家各下各的依赖,缓存不共享。Gradle 8.10 增强了远程构建缓存的支持。我正在测试接入一个共享的文件系统缓存:
为什么这么做:有一次一个新同事拉代码,光下载依赖就花了 15 分钟。有了远程缓存,大家共用一个缓存池,很多依赖和 Task 结果直接从内网缓存服务器拉,耗时降到了 1 分钟以内。这也是为了适配 大规模 CI/CD 的必经之路。
未来几年,Gradle 肯定会进一步整合 Kotlin DSL 作为默认选项,并深度优化 Native Image 的构建体验。作为开发者,现在开始习惯用 build.gradle.kts 和 Version Catalogs 来管理依赖,能少走很多弯路。
去年我接手了一个在线教育直播平台的后端重构。当时团队有 12 个人,工程拆了 40 多个模块,用的还是 Gradle 6.x。那会儿我们每次在 CI 上跑构建,平均要 8 分钟,本地更是经常卡到 10 分钟以上。最离谱的一次,我改了一行配置,结果全量重新编译,直接把 Jenkins 节点跑崩了。
我排查了整整一个下午,发现是有人在 subprojects 里写了个很重的 allprojects { ... } 配置,导致每次都会触发所有子项目的重新评估。我把这部分逻辑拆成了 按需 apply 的插件,同时把 buildSrc 里的依赖声明统一迁移到了 Version Catalogs。改完之后,本地增量构建直接降到了 40 秒左右,CI 也稳定在 3 分钟以内。
说实话,并不是所有项目都值得折腾 Gradle 8.x:
* 适合上的场景:模块多、依赖复杂、对构建速度敏感,或者你打算写自定义插件来规范团队流程。
* 没必要硬上的场景:单体应用、小团队、或者团队里没人愿意花时间理解 configuration cache 的报错。我见过有人为了“显得技术新”硬上 Kotlin DSL,结果全组人看不懂 build.gradle.kts,最后又退回去了。
如果你现在正准备从 Maven 迁移,别一上来就追求“一步到位”。先拿一个最边缘的子模块试水,把依赖管理跑通,再慢慢推。Gradle 的学习曲线确实陡,但一旦你理解了它的生命周期,那种“掌控感”是 Maven 给不了的。别被那些花里胡哨的新特性迷住眼,能稳定、快速交付的构建,才是好构建。