處理多個專案時可能需要與多個版本的 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 可以處理 JVM 編碼供應商資訊時,不同 JDK 版本之間的任何不一致性。

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 擴充功能取得。

有三個工具可用

  • JavaCompilerJavaCompile 任務使用的工具

  • JavaLauncherJavaExecTest 任務使用的工具

  • JavadocToolJavadoc 任務使用的工具

與依賴 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 會將下載的 JDK 安裝在 Gradle 使用者主目錄 中。

Gradle 僅下載 GA 版本的 JDK。不支援下載早期存取版本。

一旦安裝在 Gradle 使用者主目錄 中,已提供的 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'
}

一般來說,在套用工具鏈解析器外掛時,也需要設定外掛提供的工具鏈下載解析器。我們舉個例子來說明。考慮 build 套用的兩個工具鏈解析器外掛

  • 一個是前面提到的 Foojay 外掛,它透過提供的 FoojayToolchainResolver 下載工具鏈。

  • 另一個包含名為 MadeUpResolver虛構解析器。

以下範例透過設定檔中的 toolchainManagement 區塊,在 build 中使用這些工具鏈解析器

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

這有助於除錯哪些工具鏈可供 build 使用、如何偵測這些工具鏈,以及 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 在主要 Java 版本 17 以外的版本上執行。否則,該安裝會具有優先順序。

當我們套用上述規則來對這組內容進行排序時,我們會得到下列順序

  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 編譯的 JVM 中執行時,可能會錯誤地偵測到工具鏈,musl 是 C 標準函式庫的 替代實作。針對 musl 編譯的 JVM 有時會覆寫 LD_LIBRARY_PATH 環境變數來控制動態函式庫解析。這可能會影響 Gradle 啟動的分岔 java 程序,導致意外行為。

因此,不建議在使用 musl 函式庫的環境中使用多個 java 工具鏈。這是大多數 Alpine 發行版的情況,請考慮使用其他發行版,例如 Ubuntu。如果您使用單一工具鏈,即執行 Gradle 的 JVM,來建置和執行您的應用程式,您可以安全地忽略此限制。