Das Build-Tool Gradle

https://bildung.social/@oerinformatik/

https://oer-informatik.de/build-tool-gradle

Gradle, Build-Tool, Kotlin DSL, build.gradle.kts, Task-Graph, Gradle-Wrapper, Toolchain, JVM Test Suite, JaCoCo, Spring Boot

tl/dr; (ca. 24 min Lesezeit): Gradle baut Java-Projekte über einen Graphen aus Tasks, der nur das ausführt, was sich geändert hat. Der Artikel richtet Gradle mit Wrapper ein, baut ein Hello-World-Projekt ganz ohne Abhängigkeiten, erklärt den Task-Graphen des Java-Plugins und zeigt an einem Spring-Boot-Dienst, wie Stilprüfung, Unit-Tests, eine eigene Suite für Integrationstests und die Abdeckungsprüfung in den Task check eingehängt werden [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über Build-Tools: Grundlagen der Build-Tools / Das Build-Tool Maven / Das Build-Tool Gradle

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste des Artikels Build-Tools: vom Quellcode zum auslieferbaren Artefakt auf.

Als Gradle-Grundlagen kann ich nach Durchlesen des Artikels…

  • … ein Gradle-Projekt mit Wrapper anlegen und den Zweck von gradlew, settings.gradle.kts und build.gradle.kts erklären.

  • … ein Projekt ohne Abhängigkeiten mit dem Plugin application bauen, starten und als ausführbares JAR packen.

  • … mit einer Toolchain festlegen, mit welcher Java-Version gebaut wird.

  • … die wichtigsten Tasks des Java-Plugins (compileJava, classes, jar, test, check, assemble, build) und ihre Abhängigkeiten nennen.

  • … mit gradlew tasks und --dry-run herausfinden, welche Tasks es gibt und welche ein Aufruf ausführt.

  • … Abhängigkeiten mit implementation, testImplementation und runtimeOnly deklarieren und einen Versionskatalog lesen.

Darüber hinaus kann ich als Gradle-Expertenwissen…

  • … Prüfungen (Checkstyle, Unit-Tests, eigene Test-Suite, Abdeckungsprüfung) über dependsOn und shouldRunAfter in den Task-Graphen einhängen.

  • … den Task-Graphen eines Builds als UML-Aktivitätsdiagramm darstellen und dependsOn von mustRunAfter unterscheiden.

Gradle ist neben Maven das zweite große Build-Tool im Java-Umfeld; Android-Projekte werden fast ausschließlich damit gebaut. Wie Maven löst Gradle Abhängigkeiten aus Repositories auf, kompiliert, testet und packt. Was alle Build-Tools gemeinsam haben, erklärt der Artikel Build-Tools: vom Quellcode zum auslieferbaren Artefakt. Zwei Dinge unterscheiden Gradle besonders von Maven:

  • Build-Skript statt XML: Die Konfiguration steht in einem Skript in Kotlin (build.gradle.kts, die Kotlin DSL) oder Groovy (build.gradle). Das ist kürzer als XML und erlaubt bei Bedarf eigene Logik.

  • Task-Graph statt fester Phasen: Gradle berechnet bei jedem Aufruf, welche Tasks nötig sind, und überspringt alles, was sich seit dem letzten Build nicht geändert hat.

Überblick: Quellcode, build.gradle.kts und Bibliotheken aus einem Repository gehen in Gradle, das die Tasks compileJava, bootJar, test, integrationTest und check ausführt; heraus kommen Artefakt und Berichte (mit KI erstellt)

Gradle installieren und der Gradle-Wrapper

Für Gradle brauchst Du ein JDK; Gradle 9 selbst läuft ab Java 17. Gradle musst Du nicht unbedingt installieren: In fast jedem Projekt liegt der Gradle-Wrapper, der die passende Version beim ersten Aufruf selbst herunterlädt. Nur um ein neues Projekt anzulegen, brauchst Du einmalig eine installierte Gradle-Version (Download unter gradle.org/install oder über die Entwicklungsumgebung). Die Installation prüfst Du mit dem folgenden Befehl; die Antwort sollte etwa so aussehen:

gradle --version
------------------------------------------------------------
Gradle 9.9.0
------------------------------------------------------------
Launcher JVM:  25.0.5 (Eclipse Adoptium 25.0.5+11)
OS:           Windows 11 10.0 amd64

Die Beispiele in diesem Artikel sind mit Gradle 9.9.0 und Java 25 getestet. Den Wrapper legst Du in einem Projektordner mit gradle wrapper --gradle-version 9.9.0 an. Danach gibt es im Projekt:

Dateien des Gradle-Wrappers
Datei Zweck
gradlew, gradlew.bat Startskripte für Linux/macOS bzw. Windows
gradle/wrapper/gradle-wrapper.properties enthält die Gradle-Version (distributionUrl=…gradle-9.9.0-bin.zip)
gradle/wrapper/gradle-wrapper.jar kleines Programm, das die Distribution lädt und startet

Alle vier Dateien gehören ins Git-Repository, der Ordner build/ und der Zwischenspeicher .gradle/ dagegen nicht. Von nun an rufst Du nur noch den Wrapper auf:

.\gradlew --version
.\gradlew build

Im weiteren Text steht kurz gradlew für den Aufruf des Wrappers in Deiner Shell.

Ein Projekt anlegen mit gradle init

Gradle kann ein Projektgerüst erzeugen. Für eine Java-Anwendung mit Kotlin DSL und Java 25:

gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --package example.csbrad.hallo --project-name hallo --java-version 25 --no-split-project --use-defaults

Gradle 9.9 legt dabei diese Struktur an:

hallo/
├── settings.gradle.kts         Name des Builds und enthaltene Teilprojekte
├── gradle.properties           Einstellungen für Gradle selbst
├── gradle/
│   ├── libs.versions.toml      Versionskatalog: alle Versionen an einer Stelle
│   └── wrapper/                Wrapper (siehe oben)
├── gradlew, gradlew.bat
└── app/                        Teilprojekt mit der eigentlichen Anwendung
    ├── build.gradle.kts        Build-Skript des Teilprojekts
    └── src/main/java/…, src/test/java/…

Das Gerüst geht davon aus, dass ein Build aus mehreren Teilprojekten bestehen kann, und legt die Anwendung deshalb in das Teilprojekt app. Außerdem trägt es schon zwei Bibliotheken ein (Guava und JUnit) und ein Plugin, das fehlende JDKs automatisch herunterladen kann. Für den Einstieg ist das mehr als nötig; deshalb bauen wir das Hello World im nächsten Abschnitt bewusst von Hand und ohne jede Abhängigkeit.

Hello World ohne Abhängigkeiten

Das kleinste sinnvolle Gradle-Projekt besteht aus zwei Konfigurationsdateien und einer Klasse:

hallo-gradle/
├── settings.gradle.kts
├── build.gradle.kts
└── src/main/java/example/csbrad/hallo/App.java

settings.gradle.kts legt nur den Namen fest. Er wird später Teil des JAR-Namens:

rootProject.name = "hallo-gradle"

Die Klasse App.java gibt einen Gruß aus:

package example.csbrad.hallo;

public class App {

    public static void main(String[] args) {
        System.out.println("Hallo Welt - gebaut mit Gradle!");
    }
}

Das Build-Skript build.gradle.kts bindet das Plugin application ein, legt Koordinaten und Java-Version fest und nennt die Startklasse:

plugins {
    // bringt die Tasks zum Kompilieren, Testen und Packen mit (compileJava, test, jar ...)
    // und zusätzlich run, installDist und distZip
    application
}

group = "example.csbrad"
version = "1.0.0-SNAPSHOT"

java {
    toolchain {
        // mit diesem JDK wird kompiliert, getestet und gestartet
        languageVersion = JavaLanguageVersion.of(25)
    }
}

application {
    // Startklasse für "gradlew run"
    mainClass = "example.csbrad.hallo.App"
}

tasks.jar {
    manifest {
        // Startklasse ins Manifest schreiben: java -jar funktioniert
        attributes("Main-Class" to application.mainClass)
    }
}

Drei Begriffe stecken in diesem Skript:

  • Plugins bringen Tasks und Konventionen mit. application schließt das Plugin java ein und ergänzt Tasks zum Starten und Verteilen.

  • Die Toolchain legt fest, mit welchem JDK Gradle kompiliert, testet und startet, unabhängig davon, mit welchem Java Gradle selbst läuft. Ist kein JDK 25 installiert, bricht der Build mit einer klaren Meldung ab (Cannot find a Java installation on your machine … matching: {languageVersion=25 …}). Das ist das Gegenstück zur Versionsprüfung mit dem Enforcer bei Maven.

  • Tasks sind die Arbeitsschritte, hier wird der vorhandene Task jar um einen Manifest-Eintrag ergänzt.

Gebaut wird mit gradlew build. Gradle zeigt jeden Task an, der dabei läuft:

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :startScripts
> Task :distTar
> Task :distZip
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build

BUILD SUCCESSFUL in 4s
5 actionable tasks: 5 executed

NO-SOURCE bedeutet: Es gibt nichts zu tun (keine Ressourcen, keine Tests). Starten kannst Du das Programm direkt über Gradle mit gradlew run oder wie jedes JAR mit java -jar build/libs/hallo-gradle-1.0.0-SNAPSHOT.jar. Ausgegeben wird in beiden Fällen Hallo Welt - gebaut mit Gradle!.

Rufst Du gradlew build ein zweites Mal auf, ohne etwas zu ändern, ist Gradle in unter einer Sekunde fertig:

> Task :compileJava UP-TO-DATE
> Task :jar UP-TO-DATE
...
> Task :build UP-TO-DATE

BUILD SUCCESSFUL in 905ms
5 actionable tasks: 5 up-to-date

UP-TO-DATE heißt: Ein- und Ausgaben des Tasks sind unverändert, also wird er übersprungen. Das ist der inkrementelle Build, einer der großen Vorteile von Gradle. Das komplette Projekt liegt im Repository zu dieser Artikelreihe.

Kotlin DSL oder Groovy DSL?

Ältere Projekte und viele Anleitungen im Netz nutzen noch die Groovy DSL (build.gradle). Seit Gradle 8.2 ist die Kotlin DSL Standard, weil Entwicklungsumgebungen sie besser prüfen und vervollständigen können. Inhaltlich ist beides gleich, nur die Schreibweise unterscheidet sich:

Kotlin DSL und Groovy DSL im Vergleich
Merkmal Kotlin DSL (build.gradle.kts) Groovy DSL (build.gradle)
Plugin einbinden plugins { application } plugins { id 'application' }
Zeichenketten nur "…" '…' oder "…"
Zuweisung mainClass = "example.csbrad.hallo.App" mainClass = 'example.csbrad.hallo.App'
Abhängigkeit implementation("gruppe:name:1.0") implementation 'gruppe:name:1.0'
Task konfigurieren tasks.jar { … } jar { … }
Manifest-Eintrag attributes("Main-Class" to …) attributes 'Main-Class': …

Tasks und der Task-Graph

Ein Gradle-Build besteht aus Tasks. Jeder Task kann von anderen abhängen (dependsOn). Rufst Du einen Task auf, ermittelt Gradle alle Tasks, von denen er direkt oder indirekt abhängt, ordnet sie in einem gerichteten, kreisfreien Graphen (directed acyclic graph, DAG) und führt sie aus. Für das Plugin java sieht der Graph hinter gradlew build so aus:

UML-Aktivitätsdiagramm des Task-Graphen des Java-Plugins: compileJava und processResources, dann classes; danach einerseits jar und assemble, andererseits compileTestJava und testClasses, test und check; zum Schluss build (mit KI erstellt)

Die Gabelungen im Diagramm bedeuten: Zwischen den Strängen ist die Reihenfolge nicht festgelegt. Gradle darf jar vor oder nach test ausführen. Wichtig sind die Sammel-Tasks (lifecycle tasks), die selbst nichts tun, sondern nur andere Tasks bündeln:

Sammel-Tasks des Java-Plugins
Task bündelt grob vergleichbar mit Maven
classes Kompilieren und Ressourcen kopieren compile
assemble alle Pakete bauen (JAR, Distributionen) package ohne Tests
check alle Prüfungen (Tests, Stilprüfung …) verify ohne package
build assemble und check verify

Welche Tasks ein Projekt kennt, zeigt gradlew tasks (mit --all auch die versteckten). Welche Tasks ein Aufruf ausführen würde, verrät --dry-run, ohne etwas zu tun:

gradlew build --dry-run
:compileJava SKIPPED
:processResources SKIPPED
:classes SKIPPED
:jar SKIPPED
...
:test SKIPPED
:check SKIPPED
:build SKIPPED

Anders als bei Maven gibt es keine festen Phasen, in die sich ein Plugin „einklinken“ muss: Jeder Task kann an beliebiger Stelle in den Graphen gehängt werden. Das ist flexibel, verlangt aber Sorgfalt, damit der Graph verständlich bleibt.

Spring Boot: Tests im Task-Graphen

Wie bei Maven bauen wir einen kleinen REST-Dienst der CSB-Rad GmbH, der Versandkosten nach den festen Konditionen der Firma berechnet: Paket 6,90 €, ab 100 € Warenwert versandkostenfrei, Express-Zuschlag 9,90 €, Spedition 49,00 € je Rad. Die Anfrage GET /api/versandkosten?warenwert=3490.00&raeder=1 liefert {"versandart":"SPEDITION","betrag":49.00}. Der Java-Code ist derselbe wie im Maven-Artikel; hier geht es nur darum, wie Gradle ihn baut und prüft. Das IT-Team der CSB-Rad GmbH hatte im September zwei Pannen, einen Build mit der falschen Java-Version und eine ungetestete Änderung der Versandkostenberechnung. Beides soll der Build künftig verhindern.

Projektstruktur

Gradle trennt Unit-Tests und Integrationstests über eigene Ordner (source sets). Das ist sauberer als die Namenskonvention *IT bei Maven:

versandkosten-gradle/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml                    Versionskatalog
├── config/checkstyle/checkstyle.xml             Stilregeln (Standardort für Gradle)
└── src/
    ├── main/java/example/csbrad/versand/        Anwendung (5 Klassen)
    ├── test/java/example/csbrad/versand/        Unit- und Slice-Tests
    │   ├── VersandkostenRechnerTest.java
    │   └── VersandkostenControllerTest.java
    └── integrationTest/java/example/csbrad/versand/
        └── VersandkostenApiTest.java            Integrationstest

Der Versionskatalog

Versionen und Bibliotheken stehen gesammelt in gradle/libs.versions.toml. Im Build-Skript sprichst Du sie über libs.… an; Bindestriche werden dabei zu Punkten (spring-boot-starter-webmvc wird libs.spring.boot.starter.webmvc). Die Bibliotheken von Spring Boot brauchen keine Versionsnummer, weil ihre Versionen über die Stückliste (Bill of Materials, BOM) von Spring Boot festgelegt werden:

[versions]
spring-boot = "4.1.2"
checkstyle = "14.4.0"
jacoco = "0.8.16"

[libraries]
spring-boot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc" }
spring-boot-starter-webmvc-test = { module = "org.springframework.boot:spring-boot-starter-webmvc-test" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

Das Build-Skript

import org.springframework.boot.gradle.plugin.SpringBootPlugin

plugins {
    java
    checkstyle               // Stilprüfung: checkstyleMain, checkstyleTest ...
    jacoco                   // Testabdeckung: jacocoTestReport, jacocoTestCoverageVerification
    alias(libs.plugins.spring.boot)   // bootRun, bootJar
}

group = "example.csbrad"
version = "1.0.0-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // abgestimmte Versionen aller Spring-Bibliotheken (Bill of Materials)
    implementation(platform(SpringBootPlugin.BOM_COORDINATES))
    implementation(libs.spring.boot.starter.webmvc)

    testImplementation(libs.spring.boot.starter.webmvc.test)
    testRuntimeOnly(libs.junit.platform.launcher)
}

checkstyle {
    toolVersion = libs.versions.checkstyle.get()
    isIgnoreFailures = false
    maxWarnings = 0
}

jacoco {
    toolVersion = libs.versions.jacoco.get()
}

testing {
    suites {
        // Unit- und Slice-Tests in src/test/java
        named<JvmTestSuite>("test") {
            useJUnitJupiter()
        }

        // Integrationstests in src/integrationTest/java
        register<JvmTestSuite>("integrationTest") {
            useJUnitJupiter()
            dependencies {
                implementation(project())
                implementation(platform(SpringBootPlugin.BOM_COORDINATES))
                implementation(libs.spring.boot.starter.webmvc.test)
                runtimeOnly(libs.junit.platform.launcher)
            }
            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(tasks.test)   // erst die schnellen Tests, dann die langsamen
                    }
                }
            }
        }
    }
}

// Abdeckung aus Unit- UND Integrationstests auswerten (test.exec, integrationTest.exec)
val integrationTest = tasks.named<Test>("integrationTest")
val messdaten = fileTree(layout.buildDirectory.dir("jacoco")) { include("*.exec") }

tasks.jacocoTestReport {
    dependsOn(tasks.test, integrationTest)
    executionData.setFrom(messdaten)
}

tasks.jacocoTestCoverageVerification {
    dependsOn(tasks.test, integrationTest)
    executionData.setFrom(messdaten)
    violationRules {
        rule {
            limit {
                counter = "LINE"
                value = "COVEREDRATIO"
                minimum = "0.80".toBigDecimal()
            }
        }
    }
}

// alles in den Task check (und damit in build) einhängen
tasks.check {
    dependsOn(integrationTest)
    dependsOn(tasks.jacocoTestCoverageVerification)
}

Die Abschnitte im Einzelnen:

  • Plugins: java für die Grundtasks, checkstyle und jacoco sind in Gradle eingebaut und brauchen keine Version. Das Plugin von Spring Boot kommt aus dem Gradle Plugin Portal und ergänzt bootJar (ausführbares JAR mit allen Bibliotheken) und bootRun.

  • Abhängigkeiten: platform(…) holt die Stückliste von Spring Boot, implementation entspricht dem Maven-Scope compile, testImplementation dem Scope test. Der junit-platform-launcher wird nur zum Starten der Tests gebraucht (testRuntimeOnly).

  • Test-Suites: Das Plugin JVM Test Suite verwaltet Testarten. Die Suite test gibt es immer; die Suite integrationTest legen wir an. Gradle erzeugt dafür automatisch den Ordner src/integrationTest/java, eigene Abhängigkeiten und den Task integrationTest.

  • Reihenfolge: shouldRunAfter(tasks.test) sorgt dafür, dass die langsamen Integrationstests nach den schnellen Unit-Tests laufen, wenn beide angefordert sind; dependsOn am Task check sorgt dafür, dass sie überhaupt mitlaufen.

Wo welche Prüfung im Graphen hängt

Im folgenden UML-Aktivitätsdiagramm sind die Tasks zusammengefasst, die gradlew build für den Versandkosten-Service ausführt. Bauen, Stilprüfung und Tests sind voneinander unabhängig; erst check wartet auf alle Prüfungen:

UML-Aktivitätsdiagramm des Task-Graphen des Versandkosten-Service: Quellcode kompilieren, dann parallel ausführbares JAR bauen, Stil prüfen sowie nacheinander Unit- und Slice-Tests, Integrationstests und Testabdeckung prüfen; danach check und build (mit KI erstellt)
Prüfungen des Versandkosten-Service im Task-Graphen
Task Plugin prüft eingehängt über
Toolchain java JDK 25 vorhanden jeder Kompilier- und Test-Task
checkstyleMain, checkstyleTest, checkstyleIntegrationTest checkstyle Stilregeln je Source Set automatisch an check
test java (JVM Test Suite) Unit- und Slice-Tests automatisch an check
integrationTest JVM Test Suite Integrationstests mit laufendem Server check.dependsOn(…)
jacocoTestCoverageVerification jacoco mindestens 80 % Zeilenabdeckung aus beiden Suiten check.dependsOn(…)

Die Testklassen sind dieselben wie im Maven-Projekt: ein parametrisierter Unit-Test des VersandkostenRechner mit den Grenzwerten 99,99 € und 100,00 €, ein Slice-Test mit @WebMvcTest, der nur die Web-Schicht startet, und ein Integrationstest mit @SpringBootTest(webEnvironment = RANDOM_PORT), der echte HTTP-Anfragen an die laufende Anwendung schickt. Erklärt sind sie im Maven-Artikel. Einziger Unterschied: Die Integrationstest-Klasse heißt hier VersandkostenApiTest, denn Gradle erkennt sie am Ordner, nicht am Namen.

Der vollständige Build

gradlew build führt alle Tasks aus, Gradle zeigt sie in der Reihenfolge der Ausführung:

> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileIntegrationTestJava
> Task :integrationTestClasses
> Task :checkstyleIntegrationTest
> Task :checkstyleMain
> Task :compileTestJava
> Task :testClasses
> Task :checkstyleTest
> Task :test
> Task :integrationTest
> Task :jacocoTestCoverageVerification
> Task :check
> Task :build

BUILD SUCCESSFUL in 25s
14 actionable tasks: 14 executed

Danach liegen in build/ u. a. das ausführbare JAR (build/libs/versandkosten-gradle-1.0.0-SNAPSHOT.jar), die Testberichte (build/reports/tests/test/index.html, …/integrationTest/index.html) und die Checkstyle-Berichte. Gestartet wird der Dienst mit gradlew bootRun oder java -jar.

Schlägt eine Prüfung fehl, bricht Gradle ab, sobald ein Task scheitert. Ein vergessener Import stoppt den Build bei checkstyleMain:

> Task :checkstyleMain
[ant:checkstyle] [ERROR] .../VersandkostenController.java:5:8: Unused import - java.util.List. [UnusedImports]
> Task :checkstyleMain FAILED

* What went wrong:
Execution failed for task ':checkstyleMain'.
   > Checkstyle rule violations were found. See the report at: file:///.../build/reports/checkstyle/main.html
BUILD FAILED in 11s

Ändert jemand im Rechner den Vergleich >= 0 zu > 0 (versandkostenfrei erst über 100 €), scheitert der Task test, und Gradle nennt den fehlgeschlagenen Testfall mit seinen Parametern:

> Task :test FAILED
VersandkostenRechnerTest > berechnetVersandkostenNachKonditionen(BigDecimal, int, boolean, Versandart, BigDecimal) > "100.00" Euro, "0" Räder, Express "false" -> "PAKET" "0.00" Euro FAILED
    org.opentest4j.AssertionFailedError at VersandkostenRechnerTest.java:30
11 tests completed, 1 failed

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///.../build/reports/tests/test/index.html

Alle Quelltexte liegen im Repository zu dieser Artikelreihe.

Übungsaufgaben

Die Aufgaben beziehen sich auf die Beispielprojekte im Repository zu dieser Artikelreihe. Zum Ausprobieren brauchst Du ein JDK 25 und Internetzugang für den ersten Build.

Aufgabe 1.1: Tasks und ihre Aufgaben (Basiskompetenz)

Ordne den Tasks des Versandkosten-Service der CSB-Rad GmbH ihre Aufgabe und das Plugin zu, das sie mitbringt. (6 Punkte)

Tasks zum Ausfüllen
Task Aufgabe Plugin
compileJava
bootJar
checkstyleTest
integrationTest
jacocoTestCoverageVerification
build
Tasks, ausgefüllt
Task Aufgabe Plugin
compileJava kompiliert den Quellcode aus src/main/java java
bootJar baut das ausführbare JAR mit allen Bibliotheken Spring Boot
checkstyleTest prüft die Stilregeln für den Testcode in src/test/java checkstyle
integrationTest führt die Tests aus src/integrationTest/java aus JVM Test Suite (eigene Suite)
jacocoTestCoverageVerification prüft, ob mindestens 80 % der Zeilen von Tests ausgeführt wurden jacoco
build Sammel-Task: führt assemble und check aus java (Basis-Plugin)

Je Zeile 1 Punkt.

Aufgabe 2.1: Einen Task-Graphen lesen (Kernkompetenz)

Eine Auszubildende der CSB-Rad GmbH ruft im Versandkosten-Service gradlew check --dry-run auf und erhält:

:compileJava SKIPPED
:processResources SKIPPED
:classes SKIPPED
:jar SKIPPED
:compileIntegrationTestJava SKIPPED
:processIntegrationTestResources SKIPPED
:integrationTestClasses SKIPPED
:checkstyleIntegrationTest SKIPPED
:checkstyleMain SKIPPED
:compileTestJava SKIPPED
:processTestResources SKIPPED
:testClasses SKIPPED
:checkstyleTest SKIPPED
:test SKIPPED
:integrationTest SKIPPED
:jacocoTestCoverageVerification SKIPPED
:check SKIPPED
  1. Was bedeutet SKIPPED hier? Was wäre bei einem normalen Aufruf zu erwarten? (1 Punkt)

  2. Der Task bootJar fehlt in der Liste, jar ist dabei. Erkläre beides. (2 Punkte)

  3. Begründe anhand des Build-Skripts, warum integrationTest nach test steht. (1 Punkt)

  4. Die Auszubildende ruft gradlew check zweimal hintereinander auf, ohne etwas zu ändern. Was zeigt Gradle beim zweiten Mal an, und warum geht es so schnell? (2 Punkte)

  1. Mit --dry-run führt Gradle keinen Task aus, sondern listet nur auf, welche er ausführen würde. Bei einem normalen Aufruf laufen diese Tasks wirklich.

  2. check ist ein Prüf-Task; das ausführbare JAR braucht keine Prüfung, deshalb hängt bootJar nur an assemble. jar erscheint, weil die Suite integrationTest mit implementation(project()) das Hauptprojekt als Abhängigkeit nutzt, und das wird als normales JAR bereitgestellt.

  3. In der Suite steht shouldRunAfter(tasks.test): Sind beide Tasks angefordert, laufen die schnellen Unit-Tests zuerst.

  4. Gradle zeigt bei allen Tasks UP-TO-DATE. Ein- und Ausgaben sind unverändert, also überspringt Gradle die Tasks (inkrementeller Build).

Aufgabe 2.2: Von Maven zu Gradle (Kernkompetenz)

Das Werkstattportal der CSB-Rad GmbH wird bisher mit Maven gebaut. Ausschnitt aus der pom.xml:

<groupId>example.csbrad</groupId>
<artifactId>werkstattportal</artifactId>
<version>2.3.0</version>

<properties>
  <maven.compiler.release>25</maven.compiler.release>
</properties>

<dependencies>
  <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.5.0-jre</version>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.3</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Schreibe die entsprechenden Teile einer build.gradle.kts (ohne Versionskatalog). Das Projekt soll mit dem Plugin java gebaut werden, die Tests sollen mit JUnit laufen. (6 Punkte)

plugins {
    java
}

group = "example.csbrad"
version = "2.3.0"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.guava:guava:33.5.0-jre")
    testImplementation(platform("org.junit:junit-bom:6.1.3"))   // legt alle JUnit-Versionen fest
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}

Bewertungshinweise: Plugin und Koordinaten (1 Punkt), Toolchain (1 Punkt), Repository (1 Punkt), Abhängigkeiten mit richtiger Konfiguration (implementation, testImplementation) (2 Punkte), JUnit-Plattform für den Test-Task (1 Punkt). Statt der Stückliste junit-bom darf die Version auch direkt an junit-jupiter stehen; der junit-platform-launcher braucht dann aber ebenfalls eine passende Version (z. B. 6.1.3). Der Name des Artefakts (werkstattportal) steht in settings.gradle.kts als rootProject.name.

Aufgabe 3.1: Einen Smoke-Test einhängen (Profilkompetenz)

Cem Sarıkaya möchte, dass der Build des Versandkosten-Service der CSB-Rad GmbH zusätzlich das fertige, ausführbare JAR einmal startet und prüft, ob es antwortet (Smoke-Test). Dafür soll es einen Task smokeTest geben. Er braucht das Ergebnis von bootJar, soll aber erst laufen, nachdem die Unit-Tests durch sind, falls diese ebenfalls angefordert wurden. gradlew check soll den Smoke-Test künftig immer mit ausführen.

  1. Zeichne den Teil des Task-Graphen für gradlew check mit den Tasks compileJava/classes, bootJar, test, smokeTest und check als UML-Aktivitätsdiagramm. Weitere Prüfungen kannst Du weglassen. (4 Punkte)

  2. Ergänze das Build-Skript um die Registrierung von smokeTest und die nötigen Abhängigkeiten bzw. Reihenfolgen (die eigentliche Prüfung darf als doLast { … } angedeutet werden). (4 Punkte)

  3. Erkläre den Unterschied zwischen dependsOn und mustRunAfter an diesem Beispiel. (2 Punkte)

UML-Aktivitätsdiagramm der Beispiellösung: Quellcode kompilieren, dann parallel ausführbares JAR bauen und Unit-Tests ausführen, danach JAR starten und abfragen (smokeTest), zum Schluss check (mit KI erstellt)
val smokeTest = tasks.register("smokeTest") {
    dependsOn(tasks.bootJar)      // braucht das ausführbare JAR
    mustRunAfter(tasks.test)      // erst nach den Unit-Tests, falls diese laufen
    doLast {
        // JAR starten, /api/versandkosten?warenwert=1 abfragen, Prozess beenden
    }
}

tasks.check {
    dependsOn(smokeTest)
}
  1. dependsOn(tasks.bootJar) erzwingt, dass bootJar immer ausgeführt wird, wenn smokeTest läuft, auch bei gradlew smokeTest allein. mustRunAfter(tasks.test) legt nur die Reihenfolge fest: Wird test im selben Aufruf ausgeführt, läuft smokeTest danach; bei gradlew smokeTest allein laufen die Unit-Tests nicht mit.

Leitfragen

  1. Wozu dient der Gradle-Wrapper, und welche Dateien gehören ins Git-Repository?

Der Wrapper lädt eine festgelegte Gradle-Version, damit alle gleich bauen. gradlew, gradlew.bat und der Ordner gradle/wrapper gehören ins Repository, build/ und .gradle/ nicht. siehe Abschnitt „Gradle installieren und der Gradle-Wrapper“

  1. Welche Dateien braucht ein minimales Gradle-Projekt, und wie wird es gestartet?

settings.gradle.kts mit dem Projektnamen, build.gradle.kts mit Plugin application, Toolchain und Startklasse sowie den Quellcode in src/main/java. Starten mit gradlew run oder nach gradlew build mit java -jar build/libs/….jar. siehe Abschnitt „Hello World ohne Abhängigkeiten“

  1. Wozu dient eine Toolchain?

Sie legt fest, mit welchem JDK kompiliert, getestet und gestartet wird, unabhängig vom JDK, mit dem Gradle läuft. Fehlt es, bricht der Build mit einer klaren Meldung ab. siehe Abschnitt „Hello World ohne Abhängigkeiten“

  1. Was ist der Task-Graph, und was bedeutet UP-TO-DATE?

Gradle ermittelt aus den Abhängigkeiten der Tasks einen gerichteten, kreisfreien Graphen und führt nur die nötigen Tasks aus. UP-TO-DATE heißt, dass Ein- und Ausgaben unverändert sind und der Task übersprungen wird. siehe Abschnitt „Tasks und der Task-Graph“

  1. Wie findest Du heraus, welche Tasks es gibt und welche ein Aufruf ausführt?

gradlew tasks (mit --all vollständig) listet die Tasks auf; gradlew <task> --dry-run zeigt, welche Tasks ein Aufruf ausführen würde, ohne sie auszuführen. siehe Abschnitt „Tasks und der Task-Graph“

  1. Wie werden Integrationstests und Abdeckungsprüfung in den Build eingehängt?

Eine eigene JVM Test Suite integrationTest mit Ordner src/integrationTest/java, die mit shouldRunAfter nach test läuft; check hängt per dependsOn von integrationTest und jacocoTestCoverageVerification ab. siehe Abschnitt „Das Build-Skript“


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Das Build-Tool Gradle” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/build-tool-gradle veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-advanced/build-tool-maven. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: