Groovy in Gradle: DSL-Power für flexible Build-Skripte
Zurück zum Blog

Groovy in Gradle: DSL-Power für flexible Build-Skripte

5 Min. Lesezeit
Lesen auf English

Groovy in Gradle: DSL-Power für flexible Build-Skripte

Wer Gradle nutzt, schreibt Groovy -- oft ohne es bewusst wahrzunehmen. Die build.gradle sieht auf den ersten Blick wie eine deklarative Konfigurationsdatei aus, ist aber tatsächlich ausführbarer Groovy-Code. Diese Tatsache ist einer der grossen Vorteile von Gradle, wird aber in der Praxis erstaunlich selten voll ausgeschöpft. Dabei steckt hinter der vertrauten dependencies { ... }-Syntax ein mächtiges Sprachkonzept, das Build-Skripte wirklich flexibel macht.

Warum Groovy als Build-Sprache funktioniert

Groovy bringt eine Eigenschaft mit, die es für Build-Skripte prädestiniert: die Fähigkeit, domänenspezifische Sprachen (DSLs) zu definieren. Drei Sprachfeatures sind dafür verantwortlich.

Closures sind Codeblöcke, die man wie Objekte herumreichen kann. Wenn man in Gradle dependencies { ... } schreibt, übergibt man eine Closure an die Methode dependencies(). Innerhalb dieser Closure ändert sich der Kontext -- plötzlich stehen Methoden wie implementation oder testImplementation zur Verfügung. Das ist keine Magie, sondern Groovys Delegation-Mechanismus.

Optionale Klammern und Semikolons machen den Code kompakt. implementation 'org.springframework:spring-core:5.1.8.RELEASE' ist gültiger Groovy-Code -- ein Methodenaufruf mit einem String-Argument, ohne Klammern, ohne Semikolon. Das reduziert syntaktisches Rauschen erheblich.

Dynamische Typisierung erlaubt es, zur Laufzeit Methoden und Properties hinzuzufügen. Gradle nutzt das intensiv, um das Build-Modell je nach angewendeten Plugins zu erweitern. Sobald man das java-Plugin anwendet, stehen neue Tasks und Konfigurationen zur Verfügung, die vorher nicht existierten.

Closures und Delegation: Das Herzstück verstehen

Um Gradle-Skripte wirklich zu verstehen, lohnt es sich, Groovys Delegation-Konzept zu kennen. Jede Closure in Groovy hat einen delegate -- ein Objekt, an das Methodenaufrufe delegiert werden, wenn die Closure selbst keine passende Methode hat.

Ein vereinfachtes Beispiel:

class ServerConfig {
    String host = 'localhost'
    int port = 8080

    void host(String h) { this.host = h }
    void port(int p) { this.port = p }
}

def configure(Closure cl) {
    def config = new ServerConfig()
    cl.delegate = config
    cl.resolveStrategy = Closure.DELEGATE_FIRST
    cl()
    return config
}

def server = configure {
    host 'api.encircle360.com'
    port 443
}

Genau so funktioniert Gradle intern. Wenn man repositories { mavenCentral() } schreibt, wird die Closure mit einem RepositoryHandler als Delegate ausgeführt. Die Methode mavenCentral() wird nicht im Build-Skript gesucht, sondern auf dem Delegate aufgerufen. Dieses Prinzip zieht sich durch die gesamte Gradle-API.

Custom Tasks: Wo die DSL-Power praktisch wird

Standard-Konfiguration deckt die meisten Fälle ab. Interessant wird es, wenn man eigene Build-Logik braucht. Hier zeigt sich der Vorteil einer echten Programmiersprache gegenüber XML.

Ein Beispiel aus unserer Praxis: In mehreren Projekten generieren wir aus einer JSON-Schema-Datei Java-Klassen. Statt ein externes Tool manuell aufzurufen, definieren wir das als Gradle-Task:

task generateModels {
    description = 'Generiert Java-Klassen aus JSON-Schema-Dateien'
    group = 'code generation'

    def schemaDir = file('src/main/resources/schemas')
    def outputDir = file("${buildDir}/generated-sources/models")

    inputs.dir schemaDir
    outputs.dir outputDir

    doLast {
        outputDir.mkdirs()
        schemaDir.eachFileMatch(~/.*\.json/) { schemaFile ->
            def schema = new groovy.json.JsonSlurper().parse(schemaFile)
            def className = schema.title ?: schemaFile.name.replace('.json', '').capitalize()

            def javaCode = """
            |package com.encircle360.generated;
            |
            |public class ${className} {
            |${schema.properties.collect { name, prop ->
            |    "    private ${mapType(prop.type)} ${name};"
            |}.join('\n')}
            |}
            """.stripMargin()

            new File(outputDir, "${className}.java").text = javaCode
        }
    }
}

def mapType(String jsonType) {
    switch (jsonType) {
        case 'string': return 'String'
        case 'integer': return 'int'
        case 'boolean': return 'boolean'
        default: return 'Object'
    }
}

compileJava.dependsOn generateModels
sourceSets.main.java.srcDir "${buildDir}/generated-sources/models"

Drei Dinge sind hier bemerkenswert. Erstens: Der Task definiert inputs und outputs. Gradle nutzt diese Information für inkrementelle Builds -- wenn sich die Schema-Dateien nicht geändert haben, wird der Task übersprungen. Zweitens: Innerhalb von doLast steht die volle Groovy-Standardbibliothek zur Verfügung. JSON parsen, Dateien durchlaufen, Strings interpolieren -- alles direkt im Build-Skript. Drittens: Die letzten zwei Zeilen integrieren den Task in den normalen Build-Ablauf. compileJava hängt vom Generierungs-Task ab, und die generierten Quellen werden automatisch zum Source-Set hinzugefügt.

Plugin-Konfiguration gezielt nutzen

Plugins sind in Gradle Task-Container, die vorkonfigurierte Build-Logik mitbringen. Die Groovy-DSL macht ihre Konfiguration besonders ergonomisch, weil sich verschachtelte Closures natürlich lesen lassen.

Ein typisches Beispiel mit dem spring-boot-Plugin und zusätzlicher Konfiguration:

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.1.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'com.gorylenko.gradle-git-properties' version '2.0.0'
}

springBoot {
    buildInfo()
}

bootJar {
    archiveFileName = "${project.name}.jar"
    launchScript()
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR2'
    }
}

configurations.all {
    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}

Jeder dieser Blöcke -- springBoot, bootJar, dependencyManagement -- ist eine Closure, die an ein spezifisches Konfigurationsobjekt delegiert wird. Man muss keine API-Dokumentation auswendig lernen; die Struktur ergibt sich aus dem DSL-Design des jeweiligen Plugins.

Tipps für saubere Build-Skripte

So mächtig die Groovy-DSL ist, sie verleitet auch dazu, zu viel Logik direkt in die build.gradle zu packen. Ein paar Grundregeln, die sich bei uns bewährt haben:

Build-Logik auslagern. Sobald ein Custom Task mehr als 20 Zeilen hat, gehört er in eine eigene Datei unter buildSrc/ oder in ein eigenes Plugin. buildSrc ist ein spezielles Verzeichnis, das Gradle automatisch kompiliert und dem Build-Classpath hinzufügt.

ext-Block für Variablen nutzen. Versionsnummern und andere projektweite Konstanten definiert man zentral:

ext {
    springBootVersion = '2.1.6.RELEASE'
    lombokVersion = '1.18.8'
    junitVersion = '5.5.1'
}

In Multi-Project-Builds wandert das in die build.gradle des Root-Projekts, sodass alle Module dieselben Versionen verwenden.

apply from für Wiederverwendung. Gemeinsame Konfiguration lässt sich in separate .gradle-Dateien auslagern und per apply from: 'gradle/publishing.gradle' einbinden. Das hält die Hauptdatei schlank.

Task-Abhängigkeiten explizit machen. Gradle baut einen Directed Acyclic Graph (DAG) aller Tasks auf. Statt sich auf implizite Reihenfolgen zu verlassen, sollte man Abhängigkeiten mit dependsOn, mustRunAfter oder finalizedBy explizit deklarieren.

Ein Wort zu Kotlin DSL

Seit Gradle 4.x existiert eine alternative DSL auf Basis von Kotlin. Sie bietet statische Typisierung und damit bessere IDE-Unterstützung mit Auto-Completion und Compile-Time-Checks. Für Teams, die bereits Kotlin im Projekt einsetzen, kann das ein interessanter Weg sein.

Stand heute -- Mitte 2019 mit Gradle 5.x -- ist die Groovy-DSL aber nach wie vor die pragmatische Wahl. Die überwiegende Mehrheit der Dokumentation, Stack-Overflow-Antworten und Plugin-Beispiele verwendet Groovy. Die Kotlin-DSL hat in manchen Bereichen noch Ecken und Kanten, besonders bei der Konfiguration von Drittanbieter-Plugins. Wer heute produktiv arbeiten will, fährt mit Groovy am besten.

Fazit

Die Groovy-DSL in Gradle ist mehr als eine Konfigurationssprache -- sie ist eine vollwertige Programmierumgebung, die sich wie Konfiguration liest. Closures und Delegation sorgen dafür, dass Build-Skripte deklarativ wirken, obwohl dahinter imperativer Code steht. Custom Tasks, dynamische Konfiguration und die nahtlose Integration in den Build-Lifecycle machen Gradle zu einem Werkzeug, das mit den Anforderungen eines Projekts mitwächst.

Wer die Grundkonzepte von Groovy versteht -- insbesondere Closures und Delegation --, schreibt nicht nur bessere Build-Skripte, sondern versteht auch, warum die vorhandene Konfiguration so funktioniert, wie sie funktioniert. Und das ist letztlich der Unterschied zwischen einem Build-Tool, das man benutzt, und einem, das man beherrscht.

Teilen

Patrick Hütter

Geschrieben von

Patrick Hütter

Gründer & Software-Architekt

Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.