Gradle 版本演进全景:构建工具的进化密码

开篇:为什么你需要读懂 Gradle 的版本演进

如果你是一位 JVM 生态的开发者,几乎不可能绕开 Gradle。从 Android Studio 默认的构建工具,到 Spring Boot 项目的脚手架,再到大数据组件的源码编译,Gradle 已经成为现代构建领域的事实标准之一。然而很多开发者对 Gradle 的认知停留在"能跑就行"的阶段,遇到构建慢、依赖冲突、缓存失效等问题时只能反复重启 Daemon 或清空 .gradle 目录。

Gradle 从 5.0 到 9.0 经历了五次大版本跃迁,每一次都伴随着构建模型、依赖体系、性能范式的深刻变化。读懂这些变化,意味着你能用更少的代码描述更复杂的依赖关系,用更短的等待时间完成更大规模的构建,用更安全的方式引入第三方库。本文将从最基础的构建脚本讲起,按版本顺序梳理每个里程碑带来的用户视角变化,并提供可直接复制的代码用例。无论你是刚接触 Gradle 的新手,还是从 Maven 迁移过来的老兵,都能从中找到属于自己的升级路径。

Gradle 的核心心智模型

构建脚本的三阶段生命周期

Gradle 的每一次构建都经历三个阶段:初始化、配置、执行。初始化阶段确定参与构建的项目集合,Gradle 会执行 settings.gradle(.kts) 文件,决定哪些子项目被纳入构建。配置阶段会执行所有参与项目的 build.gradle(.kts) 文件,构建出完整的任务依赖图,但此时任务本身还没有真正执行。执行阶段则按照依赖顺序依次运行被请求的任务。

理解这三个阶段对调试构建问题至关重要。如果你在配置阶段执行了耗时操作(比如读取文件、发起网络请求),那么即使只是运行 gradle help 也会变慢。配置缓存(Configuration Cache)正是为了消除配置阶段的重复开销而生的特性,后文会详细展开。下面这段脚本展示了三阶段的可观察行为:

groovy 复制代码
// settings.gradle
rootProject.name = 'demo'
include 'app', 'core'

// build.gradle
println "配置阶段:项目名 = $project.name"

tasks.register('hello') {
    println "配置阶段:注册 hello 任务"
    doLast {
        println "执行阶段:hello 任务运行"
    }
}

运行 gradle hello 时,你会先看到两行配置阶段的输出,再看到执行阶段的输出。如果再运行一次,配置阶段的输出依然会出现,因为传统模式下每次构建都会重新执行配置阶段。

任务、依赖与项目

任务是 Gradle 构建的最小执行单元,每个任务有输入、输出和动作。依赖则是任务之间的拓扑关系,Gradle 会根据依赖关系决定执行顺序。项目是组织代码的容器,一个 Gradle 构建可以包含多个项目,每个项目可以有自己的构建脚本和依赖声明。

任务之间通过 dependsOn 建立依赖,Gradle 会自动按拓扑顺序执行。任务的输入输出用于增量构建和缓存复用,如果输入没变,任务会被跳过。下面是一个典型的任务依赖示例:

groovy 复制代码
tasks.register('compile') {
    doLast { println '编译源代码' }
}

tasks.register('test') {
    dependsOn 'compile'
    doLast { println '运行测试' }
}

tasks.register('build') {
    dependsOn 'test'
    doLast { println '打包发布' }
}

运行 gradle build 时,Gradle 会先执行 compile,再执行 test,最后执行 build。这种声明式的依赖描述是 Gradle 区别于 Make 等工具的核心特征之一。

插件与约定

插件是 Gradle 复用构建逻辑的主要机制。一个插件可以添加任务、配置依赖、扩展 DSL、注册扩展点。Gradle 内置了大量核心插件,如 java、application、maven-publish 等,社区还有成千上万的第三方插件。插件通过 plugins {} 块应用:

groovy 复制代码
plugins {
    id 'java'
    id 'application'
}

application {
    mainClass = 'com.example.Main'
}

应用 java 插件后,Gradle 会自动添加 compileJava、test、jar、build 等任务,并约定 src/main/java、src/test/java 等目录结构。这种"约定优于配置"的思想借鉴自 Maven,但 Gradle 允许你通过 sourceSets 等扩展点自由覆盖约定。

基础功能使用

创建第一个 Gradle 项目

Gradle 提供了 init 任务用于生成项目骨架。在空目录执行 gradle init,Gradle 会交互式询问项目类型(application、library 等)、构建脚本语言(Groovy 或 Kotlin DSL)、测试框架等,然后生成完整的脚手架。生成的项目包含 settings 文件、build 文件、Wrapper 脚本和示例源码。

Wrapper 是 Gradle 的精髓之一。它把 Gradle 发行版的下载和版本管理交给项目自身,确保团队成员和 CI 服务器使用完全相同的 Gradle 版本。生成 Wrapper 的命令是 gradle wrapper --gradle-version=8.5,生成的 gradlew、gradlew.bat 和 gradle-wrapper.jar 应该提交到版本控制。下面是一个典型的项目结构:

bash 复制代码
my-app/
├── settings.gradle
├── build.gradle
├── gradlew
├── gradlew.bat
├── gradle/wrapper/
│   └── gradle-wrapper.properties
└── src/
    ├── main/java/
    └── test/java/

声明依赖

依赖声明是构建脚本最常见的操作。Gradle 通过 configurations 把依赖分组到不同的用途(编译、运行、测试等),java 插件预定义了 implementation、api、testImplementation、runtimeOnly 等配置。下面是一个典型的依赖声明:

groovy 复制代码
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    implementation 'org.slf4j:slf4j-api:2.0.9'
    runtimeOnly 'ch.qos.logback:logback-classic:1.4.11'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

implementation 和 api 的区别是 Gradle 6.0 之后才被广泛强调的:implementation 隐藏依赖的传递性,api 则暴露给下游消费者。合理使用 implementation 可以显著减少重新编译的范围。

配置仓库

仓库是依赖的来源。Gradle 支持本地目录、Maven 仓库、Ivy 仓库等多种类型。最常见的是 mavenCentral(),企业内部通常还有私有的 Nexus 或 Artifactory。从 Gradle 7.0 开始,推荐在 settings.gradle 中集中声明仓库,避免子项目各自为政:

groovy 复制代码
// settings.gradle
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven {
            url 'https://repo.example.com/maven'
            credentials {
                username = findProperty('repoUser')
                password = findProperty('repoPass')
            }
        }
    }
}

这种集中式声明让仓库配置成为构建的"单一事实来源",子项目无法再添加额外仓库,从而保证依赖来源的一致性和可审计性。

自定义任务

除了插件提供的任务,开发者经常需要编写自定义任务。最简单的方式是用 tasks.register 注册一个闭包任务,更复杂的需求则可以定义 Task 类。下面是一个复制文件的自定义任务示例:

groovy 复制代码
tasks.register('copyDocs', Copy) {
    from 'src/main/asciidoc'
    into 'build/docs/html'
    rename '(.+)\\.adoc', '$1.html'
}

tasks.register('release') {
    dependsOn 'build', 'copyDocs'
    doLast {
        println "发布版本 ${project.version}"
    }
}

Copy 是 Gradle 内置的任务类型,它已经实现了增量构建和缓存支持。继承内置任务类型比自己从零实现要省心得多,这也是 Gradle 推荐的实践。

Gradle 5:Kotlin DSL 的正式登场

生产可用的 Kotlin DSL

Gradle 5.0 于 2018 年发布,最重要的里程碑是 Kotlin DSL 正式进入生产可用状态。在此之前,Kotlin DSL 还处于孵化阶段,API 不稳定,性能也不理想。5.0 之后,开发者可以在 .gradle.kts 文件中享受 IDE 的自动补全、类型检查、跳转定义等现代编辑器特性,这对大型项目和团队协作意义重大。

Kotlin DSL 的核心价值在于类型安全。Groovy DSL 灵活但容易写错配置名,编译期不会报错,只有运行时才发现。Kotlin DSL 则把大部分配置暴露为强类型 API,错误在编辑器里就能看到。下面是同一个依赖声明在两种 DSL 下的对比:

kotlin 复制代码
// build.gradle.kts (Kotlin DSL)
plugins {
    java
}

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
groovy 复制代码
// build.gradle (Groovy DSL)
plugins {
    id 'java'
}

dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

Kotlin DSL 的缺点是编译速度较慢,这个问题直到 Gradle 8.0 才得到显著改善。在 5.0 时代,大型项目的 Kotlin DSL 配置阶段可能比 Groovy DSL 慢一倍以上。

依赖版本对齐

Gradle 5.0 引入了依赖版本对齐机制,允许把一组相关依赖绑定到同一版本。这在 Jackson、Spring 等多模块库的场景下非常有用,避免出现 jackson-databind 是 2.15 而 jackson-core 是 2.14 的不一致情况。对齐通过 platform 或 BOM 导入实现:

groovy 复制代码
dependencies {
    implementation platform('com.fasterxml.jackson:jackson-bom:2.15.2')
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    implementation 'com.fasterxml.jackson.core:jackson-core'
    implementation 'com.fasterxml.jackson.core:jackson-annotations'
}

导入 BOM 后,所有 jackson-* 依赖都会自动使用 2.15.2 版本,无需逐个声明。这个特性在 6.0 之后进一步完善,与 Gradle 自有的 platform 概念融合。

任务超时

5.0 还引入了任务超时配置。某些任务(如网络下载、外部命令调用)可能因为环境问题卡死,导致整个构建挂起。设置超时后,任务超时会自动失败并释放资源:

groovy 复制代码
tasks.register('downloadData') {
    timeout = Duration.ofMinutes(10)
    doLast {
        // 下载大文件
    }
}

超时默认是关闭的,需要显式设置。这个特性对 CI 流水线特别有用,避免一个卡死的任务阻塞整个流水线。

Gradle 6:依赖管理的全面革新

Gradle Module Metadata 默认启用

Gradle 6.0 于 2019 年 11 月发布,依赖管理是这次升级的重头戏。其中最底层也最重要的变化是 Gradle Module Metadata(GMM)默认启用。GMM 是 Gradle 自有的元数据格式,扩展了 Maven POM 的能力,支持变体、依赖约束、富版本、能力等高级特性。

GMM 文件名为 .module,与 POM 并存。当使用 maven-publish 插件发布时,Gradle 会同时生成 POM 和 .module 文件。下游消费者如果是 Gradle 6+,会优先读取 .module 文件,从而获得变体感知等高级能力;如果是 Maven 或老版本 Gradle,则回退到 POM。这种向后兼容的设计让 GMM 可以平滑推广。

对普通用户来说,GMM 默认启用意味着你发布的库可以携带更丰富的元数据。例如,你可以声明某个依赖只在编译时需要、某个依赖只在运行时需要,消费者会自动选择正确的变体。下面是一个发布配置示例:

groovy 复制代码
publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
            pom {
                name = 'My Library'
                description = 'A demo library'
            }
        }
    }
}

富版本约束

富版本约束是 6.0 引入的最常用特性之一。传统的版本声明只能写一个固定版本号或区间,富版本则允许表达"偏好"、"严格"、"拒绝"等多种语义。这在管理传递依赖时特别有用,可以避免下游意外升级到不兼容的版本。

groovy 复制代码
dependencies {
    implementation('com.google.guava:guava') {
        version {
            strictly '[32.0,33.0['
            prefer '32.1.3-jre'
            reject '32.0.0-jre'
        }
    }
}

strictly 表示版本必须落在这个区间内,否则构建失败;prefer 表示在多个候选版本中优先选择;reject 表示明确排除某些版本。这些约束会被写入 GMM,对下游消费者生效。

平台与 BOM 集成

平台是 Gradle 自有的 BOM 概念,用于集中声明一组依赖的推荐版本。与 Maven BOM 不同,Gradle 平台可以包含富版本约束、能力声明等更丰富的信息。平台本身也是一个项目,通过 java-platform 插件发布:

groovy 复制代码
// platform 项目的 build.gradle
plugins {
    id 'java-platform'
}

dependencies {
    constraints {
        api 'com.google.guava:guava:32.1.3-jre'
        api 'org.slf4j:slf4j-api:2.0.9'
        api 'org.junit.jupiter:junit-jupiter:5.10.0'
    }
}

消费端通过 platform(project(':my-platform'))platform('com.example:my-platform:1.0') 引入。平台与 BOM 可以互操作,Gradle 既能消费 Maven BOM,也能把自己的平台发布为 BOM 供 Maven 使用。

依赖约束

依赖约束用于影响传递依赖的版本,而不是直接声明依赖。这是解决"传递依赖版本过低"问题的正确方式,而不是直接把传递依赖提升为直接依赖。约束不会被发布为依赖,只会作为版本建议:

groovy 复制代码
dependencies {
    constraints {
        implementation 'commons-codec:commons-codec:1.16.0'
    }
}

如果某个传递依赖引入了 commons-codec,约束会强制使用 1.16.0 版本。如果没有传递依赖引入它,约束不会自动添加这个依赖。这种细粒度的控制是 6.0 之前难以做到的。

组件能力

组件能力用于检测和解决互斥依赖。最经典的例子是日志框架:slf4j 有多个实现(logback、log4j、slf4j-simple),它们互斥,只能选一个。6.0 之前,Gradle 会在 classpath 上同时出现多个实现,导致运行时警告。6.0 之后,插件可以声明能力,Gradle 会自动检测冲突并要求用户选择:

groovy 复制代码
dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    implementation 'ch.qos.logback:logback-classic:1.4.11'
    // 如果同时引入 log4j-slf4j-impl,Gradle 会报能力冲突
}

社区插件可以为自己的依赖声明能力,让 Gradle 帮助检测冲突。这是构建可观测性和正确性的重要进步。

特性变体与测试夹具

特性变体允许一个库暴露多个可选功能,每个功能有自己的依赖。最常见的应用是测试夹具:一个库可以发布主产物和测试夹具产物,下游项目的测试代码可以依赖测试夹具,而主代码不会引入测试夹具的依赖。

groovy 复制代码
plugins {
    id 'java-library'
    id 'java-test-fixtures'
}

dependencies {
    testFixturesImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

应用 java-test-fixtures 插件后,src/testFixtures/java 目录下的代码会被编译为测试夹具产物并发布。下游项目可以这样消费:

groovy 复制代码
dependencies {
    implementation 'com.example:my-lib:1.0'
    testImplementation(testFixtures('com.example:my-lib:1.0'))
}

这种模式比传统的"把测试工具类复制到每个项目"或"发布单独的 -test.jar"要优雅得多。

更快的增量编译

6.0 对增量 Java 编译做了改进,能够识别"实现细节"类,避免不必要的重编译。例如,类 A 被类 B 使用,类 B 被类 C 使用,当 A 改变时,传统增量编译会重编译 A、B、C 三个类。6.0 之后,如果 B 只是把 A 当作实现细节(比如只在方法内部使用 A,而不在签名中暴露 A),则 C 不需要重编译。

对于深层依赖链的大型项目,这个优化可以显著减少增量编译的范围。底层实现基于对字节码的分析,识别哪些类是 API 暴露的、哪些是内部实现。这个特性是自动启用的,无需配置。

Gradle 7:性能与安全的双重跃迁

文件系统监视默认启用

Gradle 7.0 于 2021 年 5 月发布,最直观的变化是增量构建更快了。这得益于文件系统监视(File System Watching)默认启用。在此之前,Gradle 每次构建都要扫描所有输入文件计算快照,对于大型项目这可能耗时数秒。启用文件系统监视后,Gradle 会维护一个后台进程监听文件变化,下次构建时直接查询变化记录,跳过全量扫描。

文件系统监视在 Linux、macOS、Windows 上都可用,7.0 还增加了对 Apple Silicon(M1)的原生支持。用户无需任何配置就能享受这个加速,这是 7.0 最受欢迎的改进之一。底层实现使用了操作系统原生的文件监听 API(如 Linux 的 inotify、macOS 的 FSEvents)。

如果遇到文件系统监视的兼容性问题,可以通过 org.gradle.vfs.watch=false 关闭。但绝大多数情况下,这个特性是稳定且有益的。

配置缓存(实验性)

配置缓存是 7.0 引入的最具前瞻性的特性。它缓存配置阶段的结果,下次构建时如果构建脚本和输入没变,直接复用缓存的任务图,跳过整个配置阶段。对于配置阶段耗时较长的大型项目,这可以节省数秒甚至数十秒。

7.0 时代配置缓存还是实验性的,需要显式启用:

properties 复制代码
# gradle.properties
org.gradle.configuration-cache=true

启用后,第一次构建会正常执行配置阶段并写入缓存,第二次构建如果脚本没变则直接复用缓存。配置缓存对构建脚本和插件有严格要求:不能在任务执行阶段访问 Project 对象、不能使用外部状态等。不兼容的插件会导致缓存失效或构建失败。

配置缓存的底层原理是把任务图序列化到磁盘,下次构建反序列化后直接执行任务。这要求任务闭包不捕获不可序列化的状态,这也是为什么很多老插件需要改造才能兼容。这个特性在 8.0 和 9.0 逐步成熟,最终在 9.0 成为首选执行模式。

版本目录

版本目录是 7.0 引入的依赖集中管理方案,解决了多项目构建中依赖版本散落各处的问题。它用一个 TOML 文件集中声明所有依赖的坐标和版本,子项目通过类型安全的方式引用:

toml 复制代码
# gradle/libs.versions.toml
[versions]
guava = "32.1.3-jre"
junit = "5.10.0"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

[plugins]
java-library = { id = "java-library", version = "none" }
groovy 复制代码
// build.gradle
dependencies {
    implementation libs.guava
    testImplementation libs.junit.jupiter
}

Kotlin DSL 还能享受 IDE 的自动补全和类型检查。版本目录在 7.0 是实验性的,7.4 之后稳定,8.0 之后还支持在 plugins 块中使用别名。这是现代 Gradle 项目管理依赖的推荐方式。

Java 工具链

工具链让构建脚本声明项目需要的 JDK 版本,Gradle 会自动从本地查找或从远程下载匹配的 JDK。这解决了"开发机器 JDK 版本不一致"的痛点,也允许构建用 Java 17 编译,同时用 Java 11 运行测试。

groovy 复制代码
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Gradle 会检测本地的 JDK 安装(包括 SDKMAN!、jabba、asdf-vm 等包管理器安装的),如果找不到匹配版本,会从 AdoptOpenJDK 下载。工具链在 7.0 引入,后续版本不断增强,8.0 之后还支持指定 vendor、自动配置 Daemon JVM 等。

依赖验证

依赖验证是 7.0 的安全特性,允许构建声明依赖的校验和和签名,Gradle 会在解析依赖时验证。这可以防止依赖被篡改或供应链攻击。启用方式是生成验证元数据文件:

bash 复制代码
gradle --write-verification-metadata sha256 help

这会生成 gradle/verification-metadata.xml 文件,记录所有依赖的 SHA-256 校验和。之后任何依赖的变更都会被检测,未签名的依赖会被拒绝。对于安全敏感的项目(如金融、医疗),这是必备特性。

单一依赖锁文件

依赖锁定用于固定传递依赖的版本,确保可重现构建。7.0 之前,每个配置一个锁文件,管理起来很麻烦。7.0 改为单一锁文件 gradle/dependencies.lock,所有配置的锁定状态集中管理。启用方式:

properties 复制代码
# gradle.properties
dependencyLocking.lockAllConfigurations()
bash 复制代码
gradle dependencies --write-locks

锁文件应该提交到版本控制,团队成员和 CI 都会使用相同的传递依赖版本。这与版本目录互补:版本目录管理直接依赖,锁文件管理传递依赖。

Gradle 8:开发者体验的精雕细琢

Kotlin DSL 编译提速

Gradle 8.0 于 2023 年 2 月发布,Kotlin DSL 用户最直接的受益是编译速度提升约 20%。这得益于对 plugins {} 块的解释器优化:8.0 之前,plugins 块需要调用 Kotlin 编译器解析,8.0 引入了一个轻量级解释器,能直接处理标准格式的 plugins 块,跳过编译器调用。

要享受这个加速,plugins 块必须使用受约束的语法:

kotlin 复制代码
plugins {
    java
    id("com.example.plugin") version "1.0"
    kotlin("jvm") version "1.9.0"
}

如果使用了版本目录别名(如 alias(libs.plugins.example))或类型安全访问器,解释器会回退到编译器模式,无法享受加速。这种限制在后续版本逐步放宽。

8.0 还把 Kotlin DSL 的 API 级别从 1.4 提升到 1.8,开发者可以在构建脚本中使用 Kotlin 1.8 的所有语言特性(如 sealed interface、context receiver 等)。同时,Kotlin DSL 脚本的编译目标从 Java 8 字节码改为运行 Gradle 的 JVM 版本,如果团队用 Java 17 跑 Gradle,构建脚本就能用 Java 17 的库。

配置缓存的成熟

8.0 时代配置缓存从实验性走向稳定,最显著的改进是首次构建也能并行执行任务。在此之前,配置缓存只在缓存命中时启用并行,首次构建(缓存未命中)仍然串行执行配置阶段。8.0 之后,即使缓存未命中,任务也会在配置阶段完成后立即并行执行。

properties 复制代码
# gradle.properties
org.gradle.configuration-cache=true
org.gradle.parallel=true

8.1 引入了配置缓存加密,用机器特定的密钥加密缓存文件,防止敏感数据(如仓库凭证)泄露。8.10 引入了字符串去重,大幅减小缓存文件体积,AndroidX 团队报告缓存体积减少 3.75 倍。8.11 引入了并行加载和存储,进一步缩短缓存读写时间。

这些渐进式改进让配置缓存在 8.x 时代逐步成为可生产使用的特性,为 9.0 的"首选执行模式"奠定了基础。

buildSrc 的进化

buildSrc 是 Gradle 组织构建逻辑的特殊项目,它的代码会被自动编译并加入构建脚本的 classpath。8.0 对 buildSrc 做了多项改进,让它更接近普通 included build 的行为。

最实用的改进是 buildSrc 的任务可以直接从命令行运行:

bash 复制代码
gradle buildSrc:build
gradle buildSrc:test

之前要运行 buildSrc 的测试很麻烦,现在和普通项目一样。8.0 还允许 buildSrc 包含其他构建(在 buildSrc/settings.gradle 中声明 includeBuild),让构建逻辑的模块化更灵活。另外,buildSrc 不再自动运行 test 任务,只有真正需要 buildSrc 输出时才编译,减少了不必要的开销。

缓存清理与保留

8.0 之前,Gradle 用户主目录(~/.gradle)会无限增长,缓存文件可能占用数十 GB 空间。8.0 引入了可配置的缓存清理和保留策略:

properties 复制代码
# gradle.properties
org.gradle.cache.cleanup=true
groovy 复制代码
// settings.gradle
cacheCleanup {
    policy = CleanupPolicy.ON_BUILD_COMPLETION
    every = Duration.ofDays(7)
    retention {
        files(DownloadedFiles.ALL) {
            maxAge = Duration.ofDays(30)
        }
    }
}

默认保留 7 天未使用的缓存,超过则清理。这个特性对开发机器和 CI 都很友好,避免磁盘被缓存撑爆。

Java Toolchains 改进

8.0 对工具链做了多项增强。最实用的是工具链供应商匹配,可以指定只使用特定厂商的 JDK:

groovy 复制代码
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}

8.8 引入了 Daemon JVM 工具链,允许 Gradle Daemon 运行在与 CLI 不同的 JVM 上。8.13 进一步支持 Daemon JVM 自动配置,本地找不到匹配 JDK 时自动下载。8.14 支持 GraalVM 工具链,能选择支持 Native Image 的 JDK。

这些改进让 Gradle 在多 JDK 环境下的行为更可预测,对需要在不同 Java 版本间切换的项目特别有用。

编译器守护进程保活

8.3 引入了 Java 编译器守护进程保活,在 Linux 和 macOS 上把编译时间缩短最多 30%。之前每次编译任务都会启动新的 javac 进程,保活后编译器进程在构建之间保持存活,避免重复启动开销。8.4 把这个特性扩展到 Windows。

这个特性是自动启用的,无需配置。对于以 Java 编译为主的大型项目(如 Spring 框架本身),效果立竿见影。

Gradle 9:面向未来的构建范式

配置缓存成为首选执行模式

Gradle 9.0.0 于 2025 年 7 月发布,最重要的变化是配置缓存成为首选执行模式。虽然还没有默认启用,但 Gradle 会在构建结束时主动建议启用,并在 gradle init 生成的新项目中默认开启。9.0 的目标是让配置缓存在 10.0 成为默认且唯一的执行模式。

9.0 引入了优雅降级机制:当遇到不兼容配置缓存的插件或特性时,Gradle 会自动回退到传统模式,而不是直接失败。这包括 Maven Publish、Ivy Publish 等核心插件的部分功能,以及 Eclipse、IDEA 等 IDE 插件。降级原因会写入配置缓存报告,方便排查。

properties 复制代码
# gradle.properties
org.gradle.configuration-cache=true
# 如果不想看到启用提示,可以显式关闭
# org.gradle.configuration-cache=false

9.0 还清理了大量与配置缓存不兼容的废弃 API,强制插件作者迁移到安全替代方案。短期内这可能导致一些老插件无法使用,但长期看是必要的阵痛。

Java 17 与 Kotlin 2 / Groovy 4

9.0 要求 Java 17 或更高版本来运行 Gradle Daemon。这是一个重大变化,许多仍在用 Java 8 或 11 的项目需要先升级运行环境。需要注意的是,这个要求只针对运行 Gradle 本身,编译和测试代码仍然可以用工具链指定更老的 Java 版本。

groovy 复制代码
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(8) // 编译目标可以是 Java 8
    }
}

9.0 嵌入了 Kotlin 2.2.x 运行时,使用 Kotlin 语言版本 2.2。这意味着构建脚本和插件可以用上 K2 编译器和 Kotlin 2 的新特性。同时 Groovy 升级到 4.0,带来新的语言特性和性能改进。这两个升级主要影响插件作者,普通用户的构建脚本通常不需要改动。

语义化版本控制

从 9.0 开始,Gradle 采用语义化版本控制(SemVer),版本号格式为 MAJOR.MINOR.PATCH。之前的小版本号省略 PATCH 段(如 8.5 而不是 8.5.0),9.0 之后所有新版本都遵循 SemVer。这个变化对用户来说意味着版本号更规范,自动化脚本解析版本号更可靠。

bash 复制代码
# Gradle 8.x 写法
./gradlew wrapper --gradle-version=8.5

# Gradle 9.x 写法
./gradlew wrapper --gradle-version=9.0.0

另外,标记为 @Incubating 的 API 不再被视为公共 API 的一部分,可能在次要版本中变更。这给了 Gradle 团队更多迭代空间,也提醒用户谨慎依赖孵化期 API。

Kotlin DSL 编译避免

9.0 对 Kotlin DSL 的脚本编译避免做了重大改进。之前 Gradle 用内部机制检测构建逻辑的 ABI 变化,对内联函数等场景处理不佳。9.0 改用 Kotlin 内置的 ABI 指纹,检测精度大幅提升。

对于 Gradle 自身的构建,非 ABI 变更的构建逻辑修改可以让配置时间减少最多 60%。这意味着你修改一个注释或方法体,不会触发所有 Kotlin DSL 脚本的重新编译。这个改进对大型 Kotlin DSL 项目特别友好。

可重现的归档输出

9.0 让归档任务(jar、zip 等)的输出默认可重现。之前归档文件中条目的顺序可能因文件系统而异,导致同样的输入产生不同的输出,影响构建缓存命中。9.0 之后,归档顺序固定(按字典序),时间戳固定,确保可重现构建。

groovy 复制代码
tasks.named('jar') {
    reproducibleFileOrder = true
    preserveFileTimestamps = false
}

这个特性对发布到 Maven 仓库的库特别重要,可重现的产物让下游的构建缓存更有效。

升级建议与实践

版本选择策略

对于新项目,直接使用最新的 Gradle 9.x 是合理选择,能享受配置缓存、Kotlin 2、更好的性能等所有现代特性。新项目没有历史包袱,gradle init 生成的脚手架已经默认启用配置缓存。

对于存量项目,建议按版本逐步升级,不要一次跨越多个大版本。每次升级前先运行 gradle help --scan 生成构建扫描报告,查看废弃 API 的使用情况。升级路径通常是 6.x → 7.x → 8.x → 9.x,每个大版本先升级到该系列的最后一个次要版本(如 7.6、8.14),再升级到下一个大版本。

配置缓存迁移

配置缓存是 9.0 的核心,建议尽早迁移。迁移步骤:先在 gradle.properties 中启用 org.gradle.configuration-cache.problems=warn,运行常用任务查看不兼容警告;逐个修复不兼容的插件和自定义任务;最后把 problems 改为 fail,确保严格兼容。

常见的不兼容问题包括:在任务执行阶段访问 Project 对象、使用外部全局状态、任务闭包捕获不可序列化的对象。修复方式通常是把这些状态作为任务输入声明,或改用 Provider 和 Property API。

依赖管理现代化

把传统的 ext 版本声明或 dependencies.gradle 文件迁移到版本目录。版本目录是 7.0 引入、8.0 稳定的特性,是当前推荐的依赖管理方式。迁移后所有依赖坐标集中在 libs.versions.toml 文件中,IDE 支持良好,团队协作更顺畅。

同时检查 implementation 和 api 的使用是否合理。把不必要的 api 改为 implementation,可以减少传递依赖的暴露,加快下游编译速度。这个区分从 6.0 开始被强调,是依赖卫生的重要实践。

工具链与 Wrapper

为所有项目配置 Java 工具链,避免依赖开发机器的 JDK 版本。工具链从 7.0 引入,9.0 已经非常成熟,支持自动下载、vendor 匹配、GraalVM 等高级特性。

始终使用 Wrapper 提交 Gradle 版本到版本控制,团队成员和 CI 都用相同的版本。升级时通过 ./gradlew wrapper --gradle-version=x.y.z 命令更新,不要手动修改 gradle-wrapper.properties。9.0 之后版本号遵循 SemVer,记得写完整的 x.y.z 形式。

构建工具的演进从未停止,Gradle 10 已经在酝酿中,配置缓存默认启用、更激进的并行执行、更智能的增量检测都在路线图上。理解每个版本的演进逻辑,不仅能让你更好地使用当前版本,也能在未来升级时快速适应。构建是一门手艺,而掌握工具的演进史,是成为优秀构建工程师的必经之路。

相关推荐
玛丽莲茼蒿1 小时前
很难出错的Spring5(十)—— IOC进阶
java·开发语言·jvm
减瓦1 小时前
Jackson 使用指南
java·spring boot·json
Devin~Y1 小时前
互联网大厂 Java 面试实录:Spring Boot、MyBatis、Redis、Kafka、Spring Security、RAG 与 MCP 全链路问答
java·redis·kafka·mybatis·spring security·spring mvc·sprint boot
weixin_437662981 小时前
redisson
java
码农进化录1 小时前
Java 程序员的 AI 进化论 | AI 加 Postman 跑接口测试,省了三天活
java·后端·openai
阿米亚波2 小时前
【C++ STL】std::array
java·开发语言·c++
奶糖 肥晨2 小时前
mac系统中Java项目环境配置与问题解决全记录| 项目构建篇:Maven编译与打包
java·macos·maven
万亿少女的梦1682 小时前
基于Spring Boot的乐助在线助农系统设计与实现
java·spring boot·mysql·敏感词过滤·助农系统
唐青枫3 小时前
Java Undertow 实战指南:从轻量 Web Server 到 Spring Boot 嵌入式容器
java