处理多个项目可能需要与多个版本的 Java 语言进行交互。即使在单个项目中,代码库的不同部分也可能由于向后兼容性要求而固定到特定的语言级别。这意味着必须在构建项目的每台机器上安装和管理相同工具(工具链)的不同版本。

Java 工具链是一组用于构建和运行 Java 项目的工具,通常由环境通过本地 JRE 或 JDK 安装提供。编译任务可以使用 javac 作为其编译器,测试和执行任务可以使用 java 命令,而 javadoc 将用于生成文档。

默认情况下,Gradle 使用相同的 Java 工具链来运行 Gradle 本身和构建 JVM 项目。但是,这有时可能不是必需的。在不同的开发人员机器和 CI 服务器上使用不同 Java 版本构建项目可能会导致意外问题。此外,你可能希望使用不支持运行 Gradle 的 Java 版本构建项目。

为了提高构建的可重复性并明确构建要求,Gradle 允许在项目和任务级别配置工具链。

项目的工具链

您可以在 java 扩展块中声明 Java 语言版本,以定义要用于项目的工具链

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

执行构建(例如,使用 gradle check)现在将为您和运行您的构建的其他人员处理多项任务

  1. Gradle 配置所有编译、测试和 javadoc 任务以使用定义的工具链。

  2. Gradle 检测 本地安装的工具链

  3. Gradle 选择与要求匹配的工具链(对于上述示例,为任何 Java 17 工具链)。

  4. 如果找不到匹配的工具链,Gradle 可以根据配置的 工具链下载存储库 自动下载匹配的工具链。

工具链支持在 Java 插件中以及为其定义的任务中提供。

对于 Groovy 插件,支持编译,但不支持 Groovydoc 生成。对于 Scala 插件,支持编译和 Scaladoc 生成。

按供应商选择工具链

如果您的构建对所使用的 JRE/JDK 有特殊要求,您可能还想为工具链定义供应商。 JvmVendorSpec 具有 Gradle 识别的知名 JVM 供应商列表。这样做的好处是,Gradle 可以处理 JDK 版本之间在 JVM 如何编码供应商信息方面存在的任何不一致之处。

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}

如果您要定位的供应商不是已知供应商,您仍然可以将工具链限制为与可用工具链的 java.vendor 系统属性匹配的工具链。

以下代码段使用筛选来包含可用工具链的子集。此示例仅包含其 java.vendor 属性包含给定匹配字符串的工具链。匹配以不区分大小写的方式进行。

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.matching("customString")
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.matching("customString")
    }
}

按虚拟机实现选择工具链

如果您的项目需要特定实现,您还可以根据实现进行筛选。当前可供选择的实现有

VENDOR_SPECIFIC

充当占位符,并匹配任何供应商的任何实现(例如 hotspot、zulu、…​)

J9

仅匹配使用 OpenJ9/IBM J9 运行时引擎的虚拟机实现。

例如,要使用通过 AdoptOpenJDK 分发的 IBM JVM,您可以按以下示例中所示指定过滤器。

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.IBM
        implementation = JvmImplementation.J9
    }
}
build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.IBM
        implementation = JvmImplementation.J9
    }
}
Java 主版本、供应商(如果指定)和实现(如果指定)将作为编译和测试执行的输入进行跟踪。

配置工具链规范

Gradle 允许配置影响工具链选择的多个属性,例如语言版本或供应商。即使可以独立配置这些属性,配置也必须遵循特定规则才能形成有效的规范。

在两种情况下,JavaToolchainSpec 被视为有效的

  1. 未设置任何属性,即规范为

  2. 设置了 languageVersion,随后可以选择设置任何其他属性。

换句话说,如果指定了供应商或实现,则必须附带语言版本。Gradle 区分配置语言版本的工具链规范和不配置语言版本的工具链规范。在大多数情况下,没有语言版本的规范将被视为选择当前构建的工具链的规范。

自 Gradle 8.0 起,使用无效JavaToolchainSpec 实例会导致构建错误。

任务的工具链

如果您想调整特定任务使用的工具链,可以指定任务使用的确切工具。例如,Test 任务公开了一个 JavaLauncher 属性,该属性定义用于启动测试的 Java 可执行文件。

在下面的示例中,我们配置所有 Java 编译任务以使用 Java 8。此外,我们引入了一个新的 Test 任务,该任务将使用 JDK 17 运行我们的单元测试。

list/build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(8)
    }
}

tasks.register<Test>("testsOn17") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
list/build.gradle
tasks.withType(JavaCompile).configureEach {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(8)
    }
}

task('testsOn17', type: Test) {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

此外,在 application 子项目中,我们添加了另一个 Java 执行任务以使用 JDK 17 运行我们的应用程序。

application/build.gradle.kts
tasks.register<JavaExec>("runOn17") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }

    classpath = sourceSets["main"].runtimeClasspath
    mainClass = application.mainClass
}
application/build.gradle
task('runOn17', type: JavaExec) {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }

    classpath = sourceSets.main.runtimeClasspath
    mainClass = application.mainClass
}

根据任务的不同,JRE 可能就足够了,而对于其他任务(例如编译),则需要 JDK。默认情况下,如果 Gradle 可以满足要求,它会优先选择已安装的 JDK 而不是 JRE。

可以从 javaToolchains 扩展获取工具链工具提供程序。

提供三种工具

  • JavaCompiler,这是 JavaCompile 任务使用的工具

  • JavaLauncher,这是 JavaExecTest 任务使用的工具

  • JavadocTool,这是 Javadoc 任务使用的工具

与依赖于 Java 可执行文件或 Java 主目录的任务集成

任何可通过 Java 可执行文件的路径或 Java 主目录位置进行配置的任务都可以受益于工具链。

虽然你无法直接连接工具链工具,但它们都具有元数据,这些元数据可提供对其完整路径或其所属 Java 安装的路径的访问权限。

例如,你可以按如下方式为任务配置 java 可执行文件

build.gradle.kts
val launcher = javaToolchains.launcherFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.sampleTask {
    javaExecutable = launcher.map { it.executablePath }
}
build.gradle
def launcher = javaToolchains.launcherFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.named('sampleTask') {
    javaExecutable = launcher.map { it.executablePath }
}

另一个示例,你可以按如下方式为任务配置 Java 主目录

build.gradle.kts
val launcher = javaToolchains.launcherFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.anotherSampleTask {
    javaHome = launcher.map { it.metadata.installationPath }
}
build.gradle
def launcher = javaToolchains.launcherFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.named('anotherSampleTask') {
    javaHome = launcher.map { it.metadata.installationPath }
}

如果你需要特定工具(例如 Java 编译器)的路径,你可以按如下方式获取

build.gradle.kts
val compiler = javaToolchains.compilerFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.yetAnotherSampleTask {
    javaCompilerExecutable = compiler.map { it.executablePath }
}
build.gradle
def compiler = javaToolchains.compilerFor {
    languageVersion = JavaLanguageVersion.of(11)
}

tasks.named('yetAnotherSampleTask') {
    javaCompilerExecutable = compiler.map { it.executablePath }
}
以上示例使用具有 RegularFilePropertyDirectoryProperty 属性的任务,这些属性允许延迟配置。分别执行 launcher.get().executablePathlauncher.get().metadata.installationPathcompiler.get().executablePath 将为你提供给定工具链的完整路径,但请注意,这可能会急切地实现(并提供)工具链。

自动检测已安装的工具链

默认情况下,Gradle 会自动检测本地 JRE/JDK 安装,因此用户无需进行进一步配置。以下是 JVM 自动检测支持的常见包管理器、工具和位置的列表。

JVM 自动检测知道如何与以下内容配合使用

在所有检测到的 JRE/JDK 安装中,将根据 工具链优先级规则 选择一个。

无论你使用工具链自动检测还是配置 自定义工具链位置,不存在或没有 bin/java 可执行文件的安装都会被忽略并显示警告,但不会生成错误。

如何禁用自动检测

为了禁用自动检测,可以使用 org.gradle.java.installations.auto-detect Gradle 属性

  • 使用 -Porg.gradle.java.installations.auto-detect=false 启动 gradle

  • 或者将 org.gradle.java.installations.auto-detect=false 放入 gradle.properties 文件中。

自动配置

如果 Gradle 无法找到与构建要求匹配的本地可用工具链,它可以自动下载一个(只要配置了工具链下载存储库;有关详细信息,请参阅相关部分)。Gradle 在Gradle 用户目录中安装下载的 JDK。

Gradle 仅下载 GA 版本的 JDK 版本。不支持下载早期访问版本。

一旦安装在Gradle 用户目录中,已配置的 JDK 就会成为自动检测可见的 JDK 之一,并且可以像系统上安装的任何其他 JDK 一样被任何后续构建使用。

由于自动配置仅在自动检测找不到匹配的 JDK 时才启动,因此自动配置只能下载新的 JDK,并且绝不会参与更新任何已安装的 JDK。即使有较新的次要版本可用,自动配置也永远不会重新访问和自动更新任何自动配置的 JDK。

工具链下载存储库

通过应用特定设置插件,将工具链下载存储库定义添加到构建中。有关编写此类插件的详细信息,请参阅工具链解析器插件页面。

工具链解析器插件的一个示例是Disco Toolchains 插件,它基于foojay Disco API。它甚至有一个约定变体,只需应用即可自动处理所有必需的配置

settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
}
settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

通常,在应用工具链解析器插件时,还需要配置它们提供的工具链下载解析器。我们通过一个示例来说明。考虑构建应用的两个工具链解析器插件

  • 一个是上面提到的 Foojay 插件,它通过它提供的 FoojayToolchainResolver 下载工具链。

  • 另一个包含一个名为 MadeUpResolver虚构解析器。

以下示例通过设置文件中的 toolchainManagement 块,在构建中使用这些工具链解析器

settings.gradle.kts
toolchainManagement {
    jvm { (1)
        javaRepositories {
            repository("foojay") { (2)
                resolverClass = org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java
            }
            repository("made_up") { (3)
                resolverClass = MadeUpResolver::class.java
                credentials {
                    username = "user"
                    password = "password"
                }
                authentication {
                    create<DigestAuthentication>("digest")
                } (4)
            }
        }
    }
}
settings.gradle
toolchainManagement {
    jvm { (1)
        javaRepositories {
            repository('foojay') { (2)
                resolverClass = org.gradle.toolchains.foojay.FoojayToolchainResolver
            }
            repository('made_up') { (3)
                resolverClass = MadeUpResolver
                credentials {
                    username "user"
                    password "password"
                }
                authentication {
                    digest(BasicAuthentication)
                } (4)
            }
        }
    }
}
1 toolchainManagement 块中,jvm 块包含 Java 工具链的配置。
2 javaRepositories 块定义命名的 Java 工具链存储库配置。使用 resolverClass 属性将这些配置链接到插件。
3 工具链声明顺序很重要。Gradle 从提供匹配项的第一个存储库下载,从列表中的第一个存储库开始。
4 你可以使用与依赖管理中使用的 认证和授权选项 相同的选项来配置工具链存储库。
toolchainManagement 中的 jvm 块仅在应用工具链解析器插件后才解析。

查看和调试工具链

Gradle 可以显示所有检测到的工具链的列表,包括其元数据。

例如,要显示项目的所有工具链,请运行

gradle -q javaToolchains
gradle -q javaToolchains 的输出
> gradle -q javaToolchains

 + Options
     | Auto-detection:     Enabled
     | Auto-download:      Enabled

 + AdoptOpenJDK 1.8.0_242
     | Location:           /Users/username/myJavaInstalls/8.0.242.hs-adpt/jre
     | Language Version:   8
     | Vendor:             AdoptOpenJDK
     | Architecture:       x86_64
     | Is JDK:             false
     | Detected by:        Gradle property 'org.gradle.java.installations.paths'

 + Microsoft JDK 16.0.2+7
     | Location:           /Users/username/.sdkman/candidates/java/16.0.2.7.1-ms
     | Language Version:   16
     | Vendor:             Microsoft
     | Architecture:       aarch64
     | Is JDK:             true
     | Detected by:        SDKMAN!

 + OpenJDK 15-ea
     | Location:           /Users/user/customJdks/15.ea.21-open
     | Language Version:   15
     | Vendor:             AdoptOpenJDK
     | Architecture:       x86_64
     | Is JDK:             true
     | Detected by:        environment variable 'JDK16'

 + Oracle JDK 1.7.0_80
     | Location:           /Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre
     | Language Version:   7
     | Vendor:             Oracle
     | Architecture:       x86_64
     | Is JDK:             false
     | Detected by:        MacOS java_home

这有助于调试哪些工具链可用于构建,如何检测这些工具链,以及 Gradle 了解这些工具链的哪种元数据。

如何禁用自动配置

为了禁用自动配置,你可以使用 org.gradle.java.installations.auto-download Gradle 属性

  • 使用 -Porg.gradle.java.installations.auto-download=false 启动 gradle

  • 或将 org.gradle.java.installations.auto-download=false 放入 gradle.properties 文件中。

自定义工具链位置

如果自动检测本地工具链不足或被禁用,还有其他方法可以让你让 Gradle 了解已安装的工具链。

如果你的设置已经提供了指向已安装 JVM 的环境变量,你还可以让 Gradle 知道要考虑哪些环境变量。假设环境变量 JDK8JRE17 指向有效的 Java 安装,以下内容指示 Gradle 解析这些环境变量,并在寻找匹配工具链时考虑这些安装。

org.gradle.java.installations.fromEnv=JDK8,JRE17

此外,你可以使用 org.gradle.java.installations.paths 属性提供特定安装的路径的逗号分隔列表。例如,在 gradle.properties 中使用以下内容将让 Gradle 知道在检测工具链时要查看哪些目录。Gradle 将把这些目录视为可能的安装,但不会进入任何嵌套目录。

org.gradle.java.installations.paths=/custom/path/jdk1.8,/shared/jre11

Gradle 不会将自定义工具链优先于 自动检测的 工具链。如果你在构建中启用了自动检测,自定义工具链将扩展工具链位置的集合。Gradle 根据 优先级规则 选择工具链。

工具链安装优先级

Gradle 将对所有匹配构建工具链规范的 JDK/JRE 安装进行排序,并选择第一个。排序基于以下规则

  1. 当前运行 Gradle 的安装优先于任何其他安装

  2. JDK 安装优先于 JRE 安装

  3. 某些供应商优先于其他供应商;其排序(从最高优先级到最低优先级)

    1. ADOPTIUM

    2. ADOPTOPENJDK

    3. AMAZON

    4. APPLE

    5. AZUL

    6. BELLSOFT

    7. GRAAL_VM

    8. HEWLETT_PACKARD

    9. IBM

    10. JETBRAINS

    11. MICROSOFT

    12. ORACLE

    13. SAP

    14. TENCENT

    15. 其他所有内容

  4. 较高的主版本优先于较低的主版本

  5. 较高的次要版本优先于较低的次要版本

  6. 安装路径按其字典序优先(用于确定相同类型、相同供应商和相同版本的安装之间的优先级的最后手段标准)

所有这些规则都作为多级排序标准应用,按所示顺序。我们通过一个示例来说明。工具链规范请求 Java 版本 17。Gradle 检测到以下匹配安装

  • Oracle JRE v17.0.1

  • Oracle JDK v17.0.0

  • Microsoft JDK 17.0.0

  • Microsoft JRE 17.0.1

  • Microsoft JDK 17.0.1

假设 Gradle 在 17 以外的其他主要 Java 版本上运行。否则,该安装将具有优先级。

当我们应用上述规则对这组内容进行排序时,最终将得到以下排序

  1. Microsoft JDK 17.0.1

  2. Microsoft JDK 17.0.0

  3. Oracle JDK v17.0.0

  4. Microsoft JRE v17.0.1

  5. Oracle JRE v17.0.1

Gradle 优先使用 JDK 而非 JRE,因此 JRE 排在最后。Gradle 优先使用 Microsoft 供应商而非 Oracle,因此 Microsoft 安装排在第一位。Gradle 优先使用较高的版本号,因此 JDK 17.0.1 排在 JDK 17.0.0 之前。

因此,Gradle 按此顺序选择第一个匹配项:Microsoft JDK 17.0.1。

插件作者的工具链

在创建使用工具链的插件或任务时,提供合理默认值并允许用户覆盖这些默认值至关重要。

对于 JVM 项目,通常可以安全地假设已将java插件应用于项目。java插件会自动应用于核心 Groovy 和 Scala 插件,以及 Kotlin 插件。在这种情况下,使用通过java扩展定义的工具链作为工具属性的默认值是合适的。这样,用户只需要在项目级别配置一次工具链即可。

以下示例展示了如何在允许用户为每个任务单独配置工具链的同时,将默认工具链用作约定。

build.gradle.kts
abstract class CustomTaskUsingToolchains : DefaultTask() {

    @get:Nested
    abstract val launcher: Property<JavaLauncher> (1)

    init {
        val toolchain = project.extensions.getByType<JavaPluginExtension>().toolchain (2)
        val defaultLauncher = javaToolchainService.launcherFor(toolchain) (3)
        launcher.convention(defaultLauncher) (4)
    }

    @TaskAction
    fun showConfiguredToolchain() {
        println(launcher.get().executablePath)
        println(launcher.get().metadata.installationPath)
    }

    @get:Inject
    protected abstract val javaToolchainService: JavaToolchainService
}
build.gradle
abstract class CustomTaskUsingToolchains extends DefaultTask {

    @Nested
    abstract Property<JavaLauncher> getLauncher() (1)

    CustomTaskUsingToolchains() {
        def toolchain = project.extensions.getByType(JavaPluginExtension.class).toolchain (2)
        Provider<JavaLauncher> defaultLauncher = getJavaToolchainService().launcherFor(toolchain) (3)
        launcher.convention(defaultLauncher) (4)
    }

    @TaskAction
    def showConfiguredToolchain() {
        println launcher.get().executablePath
        println launcher.get().metadata.installationPath
    }

    @Inject
    protected abstract JavaToolchainService getJavaToolchainService()
}
1 我们在任务上声明一个JavaLauncher属性。该属性必须标记为@Nested输入,以确保任务对工具链更改做出响应。
2 我们从java扩展获取工具链规范,以将其用作默认值。
3 使用 JavaToolchainService,我们可以获得与工具链匹配的 JavaLauncher 提供程序。
4 最后,我们将启动程序提供程序作为我们属性的约定进行连接。

在应用了 java 插件的项目中,我们可以按如下方式使用任务

build.gradle.kts
plugins {
    java
}

java {
    toolchain { (1)
        languageVersion = JavaLanguageVersion.of(8)
    }
}

tasks.register<CustomTaskUsingToolchains>("showDefaultToolchain") (2)

tasks.register<CustomTaskUsingToolchains>("showCustomToolchain") {
    launcher = javaToolchains.launcherFor { (3)
        languageVersion = JavaLanguageVersion.of(17)
    }
}
build.gradle
plugins {
    id 'java'
}

java {
    toolchain { (1)
        languageVersion = JavaLanguageVersion.of(8)
    }
}

tasks.register('showDefaultToolchain', CustomTaskUsingToolchains) (2)

tasks.register('showCustomToolchain', CustomTaskUsingToolchains) {
    launcher = javaToolchains.launcherFor { (3)
        languageVersion = JavaLanguageVersion.of(17)
    }
}
1 默认情况下,由 java 扩展定义的工具链用于解析启动程序。
2 没有其他配置的自定义任务将使用默认的 Java 8 工具链。
3 其他任务通过使用 javaToolchains 服务选择不同的工具链来覆盖启动程序的值。

当任务需要访问工具链而没有应用 java 插件时,可以直接使用工具链服务。如果向服务提供了未配置的工具链规范,它将始终返回正在运行 Gradle 的工具链的工具提供程序。这可以通过在请求工具时传递一个空 lambda 来实现:javaToolchainService.launcherFor({})

您可以在编写任务文档中找到有关定义自定义任务的更多详细信息。

工具链限制

当 Gradle 在针对 musl(C 标准库的备用实现)编译的 JVM 中运行时,可能会错误地检测工具链。针对 musl 编译的 JVM 有时会覆盖 LD_LIBRARY_PATH 环境变量以控制动态库解析。这可能会影响 Gradle 启动的分叉 Java 进程,从而导致意外行为。

因此,不建议在具有 musl 库的环境中使用多个 Java 工具链。大多数 Alpine 发行版都是这种情况——考虑改用其他发行版,例如 Ubuntu。如果您正在使用单个工具链(运行 Gradle 的 JVM)来构建和运行您的应用程序,则可以安全地忽略此限制。