W3docs

Java Gradle Build-Skript

Aufbau eines Gradle-Build-Skripts für Java — Plugins, Abhängigkeiten, Tasks und DSL-Grundlagen.

Ein Gradle-Build-Skript beschreibt, wie ein Projekt kompiliert, getestet und gepackt wird — nicht als starres XML-Dokument, sondern als Code. Gradle liest eine build.gradle-Datei (Groovy DSL) oder build.gradle.kts-Datei (Kotlin DSL), wandelt die darin enthaltenen Deklarationen in einen Graphen von Tasks um und führt nur die Tasks aus, die Ihr Befehl benötigt — in der richtigen Reihenfolge. Während Maven einen festen Lebenszyklus vorgibt, bietet Gradle einen programmierbaren: Einen Plugin anwenden, eine Abhängigkeit deklarieren und einen Task definieren sind alles gewöhnliche Anweisungen in einer echten Sprache.

Diese Seite setzt voraus, dass Sie bereits wissen, was Gradle grundsätzlich ist — falls nicht, beginnen Sie mit der Gradle-Einführung. Hier zerlegen wir einen echten build.gradle-Block für Block: Plugins, Repositories, Abhängigkeiten und Tasks.

Der Aufbau von build.gradle

Ein minimales Java-Build-Skript besteht aus vier Blöcken: welche Plugins angewendet werden sollen, woher Abhängigkeiten bezogen werden, was diese Abhängigkeiten sind und eine kleine Konfiguration. Hier ist ein vollständiges, idiomatisches Beispiel in der Kotlin DSL:

plugins {
    java
    application
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

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

application {
    mainClass = "com.example.App"
}

tasks.test {
    useJUnitPlatform()
}

Das java-Plugin allein stellt Ihnen compileJava, test, jar und ein Dutzend weitere Tasks kostenlos zur Verfügung. Das application-Plugin fügt run und installDist hinzu. Die Zeile tasks.test { useJUnitPlatform() } verbindet den test-Task so, dass JUnit 5-Tests ausgeführt werden — ohne sie greift Gradle auf die veraltete JUnit 4-Engine zurück und führt stillschweigend nichts aus. Alles danach ist die Konfiguration dessen, was diese Tasks tun.

Plugins, Repositories und der Build-Lebenszyklus

In Gradle ist fast nichts eingebaut — Funktionalitäten kommen als Plugins hinzu. Das java-Plugin ist die Grundlage für JVM-Arbeit; andere bauen darauf auf:

PluginWas es hinzufügt
javacompileJava, test, jar, Source-Sets, die dependencies-Konfigurationen
applicationrun und eine gepackte Distribution mit Start-Skripten
java-libraryDie Unterscheidung api vs. implementation für Bibliotheken
org.springframework.bootbootJar, bootRun für Spring-Boot-Anwendungen
jacocoCode-Coverage-Berichte, die in test integriert sind

repositories { } teilt Gradle mit, woher Abhängigkeiten heruntergeladen werden — mavenCentral() ist die übliche Wahl. Ohne ein Repository kann keine externe Abhängigkeit aufgelöst werden.

Abhängigkeiten und ihre Gültigkeitsbereiche deklarieren

Abhängigkeiten werden mit einer Konfiguration deklariert, die steuert, wo sie im Classpath erscheinen. Die richtige Wahl hält Ihren Compile-Classpath sauber und Ihre Builds schnell:

dependencies {
    // On the compile and runtime classpath, but NOT exposed to consumers
    implementation("org.apache.commons:commons-lang3:3.14.0")

    // Part of this library's public API — leaks to consumers (java-library only)
    api("com.google.guava:guava:32.1.3-jre")

    // Needed to compile, but provided at runtime by the environment
    compileOnly("org.projectlombok:lombok:1.18.30")

    // Only on the test classpath
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")

    // Only at runtime (e.g. a JDBC driver loaded by name)
    runtimeOnly("org.postgresql:postgresql:42.7.1")
}

Das Koordinatenformat ist group:name:version — dieselben Koordinaten, die Maven in seinem pom.xml verwendet. Wenn zwei Abhängigkeiten dasselbe Modul in unterschiedlichen Versionen einbinden, legt Grays Standardstrategie eine einzige, höchste Version auf den Classpath — das ausführbare Beispiel weiter unten modelliert genau dies.

Tasks: die Arbeitseinheit

Jede Aktion, die Gradle ausführt, ist ein Task, und Tasks deklarieren Abhängigkeiten von anderen Tasks. Das Ausführen von gradle build führt nicht nur eine Sache aus; es durchläuft einen Graphen und führt jede Voraussetzung einmal aus. Sie können auch eigene Tasks definieren:

tasks.register("printVersion") {
    group = "help"
    description = "Prints the project version."
    doLast {
        println("Project version is $version")
    }
}

// Make the jar task wait for our custom task
tasks.named("jar") {
    dependsOn("printVersion")
}

Zwei weitere Eigenschaften machen Gradle schnell. Erstens ist es inkrementell: Ein Task, dessen Eingaben und Ausgaben unverändert sind, wird als UP-TO-DATE gemeldet und übersprungen. Zweitens pinnt der Gradle Wrapper (./gradlew, unterstützt durch gradle/wrapper/gradle-wrapper.properties) eine Gradle-Version pro Projekt, sodass jeder Entwickler und jede CI-Maschine mit derselben Toolchain baut — Sie müssen Gradle nie global installieren.

Ein praktisches Beispiel: ein Build, modelliert in reinem Java

Gradle selbst ist auf diesem Runner nicht verfügbar, daher modelliert das folgende Programm die drei Ideen, die ein Build-Skript zum Funktionieren bringen — den Task-Graphen und seine Ausführungsreihenfolge, inkrementelles Up-to-Date-Überspringen und Abhängigkeits-Versionskonfliktauflösung — mit nichts außer dem JDK. Es ist das mentale Modell, das gradle build in der Realität ausführt.

java— editable, runs on the server

Was aus dem Lauf mitgenommen werden sollte:

  • Die Task-Liste für gradle build wird durch eine topologische Sortierung berechnet, nicht von Hand aufgeschrieben. compileJava und processResources kommen vor classes, das wiederum vor jar und test kommt, die vor build kommen — genau die Reihenfolge, die Gradle mit dem jeweiligen :taskName-Präfix ausgibt, denn ein Task kann erst ausgeführt werden, nachdem alles, wovon er dependsOn, abgeschlossen ist.
  • Eine Raute im Graphen führt eine gemeinsame Voraussetzung einmal aus, nicht zweimal. Sowohl jar als auch test hängen von classes ab, dennoch erscheint classes nur einmal in der Reihenfolge — das done-Set verhindert, dass Gradle denselben Code für jeden nachgelagerten Task erneut kompiliert.
  • Der zweite Lauf zeigt Gradles inkrementelles Verhalten: compileJava, processResources und classes sind UP-TO-DATE und werden übersprungen, sodass nur 3 Tasks tatsächlich ausgeführt werden. Deshalb wird ein unverändertes Projekt in Millisekunden neu gebaut — Gradle vergleicht Task-Eingaben und -Ausgaben und vermeidet jede unnötige Arbeit.
  • Die Abhängigkeitsauflösung kollabiert einen Versionskonflikt auf einen Gewinner: slf4j-api wird sowohl bei 2.0.9 (direkt) als auch bei 1.7.36 (transitiv über guava) angefordert, aber der aufgelöste Classpath listet es einmal bei 2.0.9. Gradles Standardstrategie ist höchste-Version-gewinnt, sodass ein einziges, konsistentes Jar auf dem Classpath landet statt zwei konkurrierender Kopien.
  • Die letzte Zeile nennt die Gradle-Version 8.7, als wäre sie aus gradle-wrapper.properties gelesen. In einem echten Projekt speichert der Wrapper diese Version in der Versionskontrolle, sodass ./gradlew build dasselbe Gradle für alle verwendet — der Build ist reproduzierbar, unabhängig davon, was (falls überhaupt etwas) auf dem Rechner installiert ist.

Übungen

Übung
Ein Gradle-Java-Projekt deklariert 'org.slf4j:slf4j-api:2.0.9' direkt, während eine transitive Abhängigkeit 'org.slf4j:slf4j-api:1.7.36' einbindet. Was landet mit Gradles Standard-Auflösungsstrategie auf dem Classpath?
Ein Gradle-Java-Projekt deklariert 'org.slf4j:slf4j-api:2.0.9' direkt, während eine transitive Abhängigkeit 'org.slf4j:slf4j-api:1.7.36' einbindet. Was landet mit Gradles Standard-Auflösungsstrategie auf dem Classpath?
Was this page helpful?