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:
| Plugin | Was es hinzufügt |
|---|---|
java | compileJava, test, jar, Source-Sets, die dependencies-Konfigurationen |
application | run und eine gepackte Distribution mit Start-Skripten |
java-library | Die Unterscheidung api vs. implementation für Bibliotheken |
org.springframework.boot | bootJar, bootRun für Spring-Boot-Anwendungen |
jacoco | Code-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.
Was aus dem Lauf mitgenommen werden sollte:
- Die Task-Liste für
gradle buildwird durch eine topologische Sortierung berechnet, nicht von Hand aufgeschrieben.compileJavaundprocessResourceskommen vorclasses, das wiederum vorjarundtestkommt, die vorbuildkommen — genau die Reihenfolge, die Gradle mit dem jeweiligen:taskName-Präfix ausgibt, denn ein Task kann erst ausgeführt werden, nachdem alles, wovon erdependsOn, abgeschlossen ist. - Eine Raute im Graphen führt eine gemeinsame Voraussetzung einmal aus, nicht zweimal. Sowohl
jarals auchtesthängen vonclassesab, dennoch erscheintclassesnur einmal in der Reihenfolge — dasdone-Set verhindert, dass Gradle denselben Code für jeden nachgelagerten Task erneut kompiliert. - Der zweite Lauf zeigt Gradles inkrementelles Verhalten:
compileJava,processResourcesundclassessindUP-TO-DATEund werden übersprungen, sodass nur3Tasks 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-apiwird sowohl bei2.0.9(direkt) als auch bei1.7.36(transitiv über guava) angefordert, aber der aufgelöste Classpath listet es einmal bei2.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 ausgradle-wrapper.propertiesgelesen. In einem echten Projekt speichert der Wrapper diese Version in der Versionskontrolle, sodass./gradlew builddasselbe Gradle für alle verwendet — der Build ist reproduzierbar, unabhängig davon, was (falls überhaupt etwas) auf dem Rechner installiert ist.