20 Verwendung eigener JDKs und Toolchains

20.1 Java Toolchain Konzept

Gradle Toolchains entkoppeln die Java-Version für den Build-Prozess von der JVM, auf der Gradle selbst läuft. Diese Abstraktion löst ein fundamentales Problem in heterogenen Entwicklungsumgebungen: Unterschiedliche Projekte benötigen unterschiedliche Java-Versionen, während Entwickler typischerweise nur eine JDK-Installation als System-Default konfiguriert haben. Toolchains ermöglichen die deklarative Spezifikation der benötigten Java-Version im Build-Skript, unabhängig von der lokalen Umgebung.

Das Toolchain-System unterscheidet zwischen der Gradle-Runtime-JVM und der Projekt-Build-JVM. Gradle kann mit Java 8 laufen, während das Projekt mit Java 17 kompiliert wird. Diese Trennung ermöglicht die Nutzung moderner Java-Features in Projekten, ohne die Gradle-Installation auf allen Entwicklerrechner aktualisieren zu müssen. Die Toolchain-API abstrahiert Compiler, JavaDoc-Tool und Test-Runner, sodass alle Java-bezogenen Tasks die konfigurierte Toolchain verwenden.

Die Konfiguration erfolgt im java-Extension-Block des Build-Skripts. Die Toolchain-Spezifikation definiert Requirements wie Java-Version, Vendor oder Implementation. Gradle sucht automatisch nach einer passenden JDK-Installation oder provisioniert diese bei Bedarf. Diese Auto-Provisioning-Funktionalität macht manuelle JDK-Installation obsolet und garantiert reproduzierbare Builds über Team-Grenzen hinweg.

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}

20.2 Toolchain-Resolution und Auto-Detection

Gradle implementiert einen mehrstufigen Resolution-Mechanismus zur Lokalisierung passender JDK-Installationen. Die Auto-Detection durchsucht bekannte Installationspfade des Betriebssystems, analysiert Umgebungsvariablen wie JAVA_HOME und PATH, und inspiziert Toolchain-spezifische Konfigurationen. Die Detection-Logik ist betriebssystemspezifisch und berücksichtigt typische Installationsmuster von Package-Managern wie SDKMAN, Jabba oder Homebrew.

Die Toolchain-Registry im Gradle-User-Home speichert Informationen über erkannte JDK-Installationen. Diese Registry wird bei jedem Build aktualisiert und cached Detection-Ergebnisse für bessere Performance. Die gradle javaToolchains-Task listet alle erkannten Toolchains mit Details zu Version, Vendor und Capabilities. Diese Übersicht hilft bei der Diagnose von Resolution-Problemen und der Verifizierung verfügbarer JDKs.

Custom Toolchain-Locations werden über die org.gradle.java.installations.paths-Property konfiguriert. Diese Property akzeptiert eine kommaseparierte Liste von Verzeichnissen, die zusätzlich zu den Standard-Locations durchsucht werden. Für CI/CD-Umgebungen mit nicht-standard JDK-Installationen ist diese Konfiguration essentiell. Die Pfade können absolute Verzeichnisse oder Umgebungsvariablen-Referenzen sein.

// gradle.properties
org.gradle.java.installations.paths=/opt/java/jdk-17,/usr/local/java/adoptium-11

// Oder via Command Line
./gradlew build -Porg.gradle.java.installations.paths=$CUSTOM_JDK_HOME

20.3 Auto-Provisioning und Download-Management

Auto-Provisioning erweitert Toolchain-Resolution um automatischen Download fehlender JDKs. Wenn keine lokale Installation den Requirements entspricht, lädt Gradle die passende JDK von konfigurierten Repositories herunter. Diese Funktionalität transformiert JDK-Management von einem manuellen zu einem automatisierten Prozess, analog zu Dependency-Management für Libraries.

Die Provisioning-Konfiguration erfolgt über Toolchain-Resolver-Plugins. Gradle inkludiert Resolver für AdoptiumJDK (ehemals AdoptOpenJDK) und kann durch Custom-Resolver erweitert werden. Resolver implementieren die Download-Logik, Verification und Installation. Die heruntergeladenen JDKs werden im Gradle-User-Home unter jdks gespeichert und zwischen Projekten geteilt.

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

toolchainManagement {
    jvm {
        javaRepositories {
            repository("foojay") {
                resolverClass = FoojayToolchainResolver::class.java
            }
        }
    }
}

Security-Aspekte erfordern besondere Aufmerksamkeit beim Auto-Provisioning. Downloads sollten über HTTPS erfolgen und Checksums verifiziert werden. In Enterprise-Umgebungen wird Auto-Provisioning oft deaktiviert und durch unternehmensinterne JDK-Repositories ersetzt. Die org.gradle.java.installations.auto-download=false-Property deaktiviert Auto-Provisioning global.

20.4 Unternehmens-spezifische JDK-Repositories

Große Organisationen betreiben oft eigene JDK-Repositories aus Compliance-, Security- oder Performance-Gründen. Diese Repositories hosten geprüfte JDK-Versionen, möglicherweise mit unternehmens-spezifischen Patches oder Konfigurationen. Gradle Toolchains unterstützen diese Szenarien durch Custom Toolchain Resolver.

Ein Custom Resolver implementiert das JavaToolchainResolver-Interface und definiert die Logik für Download und Metadata-Resolution. Der Resolver kann mit internen Artifact-Repositories wie Artifactory oder Nexus integrieren. Authentication, Proxy-Konfiguration und Certificate-Handling werden im Resolver implementiert. Diese Kapselung macht die JDK-Beschaffung transparent für Build-Skripte.

// buildSrc/src/main/kotlin/CompanyToolchainResolver.kt
abstract class CompanyToolchainResolver : JavaToolchainResolver {
    override fun resolve(request: JavaToolchainRequest): Optional<JavaToolchainDownload> {
        val version = request.javaToolchainSpec.languageVersion.get().asInt()
        val platform = request.buildPlatform
        
        val downloadUrl = "https://jdk.company.com/downloads/" +
            "jdk-${version}-${platform.operatingSystem}-${platform.architecture}.tar.gz"
        
        return Optional.of(JavaToolchainDownload.fromUri(URI.create(downloadUrl)))
    }
}

Die Integration in bestehende Software-Distribution-Systeme reduziert Redundanz. Wenn Unternehmen bereits Mechanismen für Software-Deployment haben, können Toolchain-Resolver diese nutzen statt eigene Download-Logik zu implementieren. Environment-Module, Puppet, oder Container-Images können JDKs bereitstellen, während Gradle Toolchains diese erkennen und verwenden.

20.5 Task-spezifische Toolchain-Konfiguration

Nicht alle Tasks in einem Projekt benötigen dieselbe Java-Version. Test-Tasks könnten mit verschiedenen Java-Versionen laufen, um Kompatibilität zu verifizieren. JavaDoc-Generation könnte eine spezifische JDK-Version für optimale Output-Qualität benötigen. Gradle ermöglicht Task-level Toolchain-Konfiguration für diese Szenarien.

Compile-Tasks können unterschiedliche Source- und Target-Compatibility verwenden, während sie mit einer modernen JDK kompilieren. Die --release-Flag des Java-Compilers garantiert API-Kompatibilität mit älteren Java-Versionen. Diese Strategie kombiniert moderne Compiler-Optimierungen mit Rückwärtskompatibilität. Die Toolchain stellt sicher, dass der korrekte Compiler verwendet wird, unabhängig vom System-JDK.

tasks.withType<JavaCompile>().configureEach {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
    options.release = 11  // Target Java 11 API
}

tasks.withType<Test>().configureEach {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

tasks.register<Test>("testOnJava17") {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Custom Tasks können Toolchain-aware implementiert werden. Die JavaLauncher und JavaCompiler Services werden als Task-Inputs injiziert. Diese Services kapseln die Toolchain-Resolution und bieten typsichere APIs für Java-Execution. Die Provider-API stellt lazy Resolution sicher, wodurch Toolchains nur bei tatsächlicher Task-Execution aufgelöst werden.

20.6 Migration zu Toolchains

Die Migration bestehender Projekte zu Toolchains erfolgt schrittweise. Der erste Schritt identifiziert alle Java-relevanten Konfigurationen im Projekt. Properties wie sourceCompatibility, targetCompatibility und explizite executable-Konfigurationen in Tasks müssen durch Toolchain-Konfiguration ersetzt werden. Die Migration kann zunächst mit einer Toolchain erfolgen, die dem aktuellen System-JDK entspricht, um Verhaltensänderungen zu minimieren.

Cross-Platform-Kompatibilität verbessert sich durch Toolchain-Migration erheblich. Windows-Entwickler mit Oracle JDK, Linux-Server mit OpenJDK und Mac-Entwickler mit Temurin JDK können dasselbe Projekt ohne Anpassungen bauen. Die Toolchain-Abstraktion eliminiert Plattform-spezifische Pfade und Konfigurationen aus Build-Skripten.

Die Dokumentation der Toolchain-Requirements wird Teil der Projekt-Dokumentation. Die README sollte keine JDK-Installationsanleitungen mehr enthalten, sondern nur den Gradle-Befehl zum Build. Die gradle.properties dokumentiert spezielle Toolchain-Konfigurationen wie Custom-Repositories oder deaktiviertes Auto-Provisioning. Diese Vereinfachung reduziert Onboarding-Zeit für neue Team-Mitglieder und eliminiert eine häufige Fehlerquelle in Build-Umgebungen.

Ja. Hier ein Vorschlag für eine Übungsaufgabe:


20.6.1 Aufgabe: Toolchain mit Foojay-Resolver nutzen

  1. Legen Sie ein neues Gradle-Projekt an und ergänzen Sie die folgenden Plugins im build.gradle.kts:

  2. Definieren Sie im java-Block eine Toolchain, die Java 21 erzwingt.

  3. Überprüfen Sie die Konfiguration mit dem Kommando:

    ./gradlew javaToolchains

    Achten Sie darauf, dass Gradle nicht Ihr lokales JDK nimmt, sondern die Toolchain aus Foojay auflöst.

  4. Schreiben Sie eine kleine Klasse HelloToolchain.java im Verzeichnis src/main/java, die eine Ausgabe wie Running with Java 21! produziert.

  5. Führen Sie das Programm mit

    ./gradlew run

    aus und kontrollieren Sie die Java-Version in der Ausgabe.


20.6.2 Lösungsansatz für die Aufgabe

my-toolchain-project/
├── build.gradle.kts
├── settings.gradle.kts
└── src
    └── main
        └── java
            └── HelloToolchain.java

settings.gradle.kts

rootProject.name = "my-toolchain-project"

build.gradle.kts

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

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

application {
    mainClass.set("HelloToolchain")
}

src/main/java/HelloToolchain.java

public class HelloToolchain {
    public static void main(String[] args) {
        System.out.println("Running with Java " + System.getProperty("java.version") + "!");
    }
}

Prüfen der Toolchain

./gradlew javaToolchains

Starten der Anwendung

./gradlew run

Die Ausgabe sollte in etwa so aussehen:

Running with Java 21.0.2!