19 Verwaltung mehrerer Projekte mit Gradle

19.1 Multi-Projekt-Build-Struktur

Gradle organisiert zusammengehörende Projekte in einer hierarchischen Struktur aus Root-Projekt und Subprojekten. Diese Struktur ermöglicht die gemeinsame Verwaltung von Modulen, die zusammen ein größeres System bilden. Ein typisches Enterprise-System besteht aus mehreren Services, gemeinsamen Libraries und Support-Modulen, die koordiniert gebaut und versioniert werden müssen. Das Root-Projekt fungiert als Container und Koordinator, während Subprojekte die eigentliche Funktionalität implementieren.

Die Projektstruktur wird in der settings.gradle oder settings.gradle.kts Datei im Root-Verzeichnis definiert. Diese Datei deklariert, welche Unterverzeichnisse als Gradle-Projekte behandelt werden. Die physische Verzeichnisstruktur muss nicht der logischen Projekthierarchie entsprechen. Gradle erlaubt flexible Mappings zwischen Verzeichnissen und Projektnamen, was besonders bei der Migration bestehender Codebasen hilfreich ist.

// settings.gradle.kts
rootProject.name = "enterprise-system"

include("core")
include("api")
include("web-frontend")
include("services:user-service")
include("services:payment-service")
include("libraries:commons")
include("libraries:test-utils")

// Anpassung der physischen Pfade
project(":web-frontend").projectDir = file("frontend/web")

Die Hierarchie ermöglicht Vererbung und gemeinsame Konfiguration. Subprojekte erben Einstellungen vom Root-Projekt und können diese überschreiben oder erweitern. Diese Struktur reduziert Konfigurationsduplikation und stellt Konsistenz über alle Module sicher. Build-Logik, die für alle Projekte gilt, wird einmal im Root-Projekt definiert und automatisch auf alle Subprojekte angewendet.

19.2 Projekt-Konfiguration und Vererbung

Die Konfiguration von Multi-Projekt-Builds erfolgt über mehrere Mechanismen, die zusammen ein mächtiges und flexibles System bilden. Das Root-Build-Skript kann Konfigurationen auf alle oder ausgewählte Subprojekte anwenden. Der allprojects-Block konfiguriert Einstellungen, die für das Root-Projekt und alle Subprojekte gelten. Der subprojects-Block betrifft nur Subprojekte und eignet sich für Konfigurationen, die das Root-Projekt nicht benötigt.

// root build.gradle.kts
allprojects {
    repositories {
        mavenCentral()
        maven { url = uri("https://repo.company.com/maven") }
    }
    
    group = "com.company.system"
    version = "2.3.0"
}

subprojects {
    apply(plugin = "java-library")
    
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    
    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
    }
}

Die selektive Konfiguration ermöglicht präzise Kontrolle über Projekt-Gruppen. Projekte können basierend auf Namen, Pfad oder eigenen Properties konfiguriert werden. Convention Plugins, implementiert als precompiled script plugins oder binary plugins, kapseln wiederverwendbare Konfigurationslogik. Diese Plugins werden im buildSrc-Verzeichnis oder als separate Gradle-Projekte entwickelt und können komplexe Setup-Logik enthalten.

Cross-Project-Konfiguration sollte vermieden werden, wo möglich. Direkte Referenzen zwischen Projekt-Build-Skripten schaffen implizite Abhängigkeiten und erschweren die Build-Logik. Stattdessen sollten Projekte über deklarierte Dependencies interagieren. Wenn Cross-Project-Konfiguration unvermeidbar ist, sollte sie explizit und gut dokumentiert sein.

19.3 Abhängigkeiten zwischen Projekten

Inter-Projekt-Dependencies definieren die Beziehungen zwischen Modulen innerhalb des Multi-Projekt-Builds. Diese Dependencies werden mit der project()-Methode deklariert und nutzen den Projekt-Pfad als Identifier. Gradle löst diese Dependencies innerhalb des Builds auf, ohne externe Repositories zu konsultieren. Dies ermöglicht schnelle Entwicklungszyklen, da Änderungen in einem Modul sofort in abhängigen Modulen verfügbar sind.

// In services/user-service/build.gradle.kts
dependencies {
    implementation(project(":libraries:commons"))
    implementation(project(":core"))
    testImplementation(project(":libraries:test-utils"))
}

Die Composite Builds erweitern dieses Konzept über Build-Grenzen hinweg. Separate Gradle-Builds können zu einem Composite kombiniert werden, wobei Dependencies zwischen den Builds automatisch durch lokale Projekt-Substitution ersetzt werden. Diese Technik ermöglicht die Entwicklung über Repository-Grenzen hinweg, ohne auf Snapshot-Versionen oder lokale Maven-Publikation angewiesen zu sein.

Project Dependencies unterstützen Configuration-Variants. Ein Projekt kann verschiedene Configurations exponieren, die von abhängigen Projekten konsumiert werden. Die Java Library Plugin unterscheidet zwischen api und implementation Dependencies, wobei api transitive Dependencies exponiert und implementation sie versteckt. Diese Unterscheidung optimiert die Compile-Classpath und reduziert unnötige Rekompilierung.

19.4 Build-Lifecycle in Multi-Projekt-Umgebungen

Der Build-Lifecycle in Multi-Projekt-Builds folgt einer definierten Sequenz, die die korrekte Initialisierung und Konfiguration aller Projekte sicherstellt. Die Initialization-Phase evaluiert die settings.gradle und erstellt die Projekt-Instanzen. Die Configuration-Phase durchläuft alle Projekte und evaluiert deren Build-Skripte. Die Execution-Phase führt die angeforderten Tasks in der durch Dependencies bestimmten Reihenfolge aus.

Task-Dependencies über Projekt-Grenzen werden automatisch von Gradle verwaltet. Wenn ein Task in Projekt A einen Task in Projekt B benötigt, stellt Gradle die korrekte Ausführungsreihenfolge sicher. Diese impliziten Dependencies entstehen durch Project-Dependencies und die damit verbundenen Task-Beziehungen. Explizite Task-Dependencies zwischen Projekten können mit dependsOn definiert werden, sollten aber sparsam verwendet werden.

Parallel Execution nutzt die Unabhängigkeit zwischen Projekten für Performance-Optimierung. Gradle analysiert den Dependency-Graph und führt unabhängige Tasks parallel aus. Die --parallel-Flag aktiviert diese Funktionalität, die besonders bei großen Multi-Projekt-Builds signifikante Zeitersparnisse bringt. Die Anzahl paralleler Worker wird durch org.gradle.workers.max konfiguriert und sollte auf die verfügbare Hardware abgestimmt werden.

19.5 Gemeinsame Ressourcen und Konfiguration

Die Zentralisierung gemeinsamer Ressourcen reduziert Duplikation und vereinfacht Wartung. Das buildSrc-Verzeichnis im Root-Projekt wird automatisch als inkludiertes Build behandelt und vor allen anderen Projekten kompiliert. Hier definierte Klassen und Plugins sind in allen Projekt-Build-Skripten verfügbar. Version-Konstanten, gemeinsame Funktionen und Custom Tasks werden typischerweise hier implementiert.

// buildSrc/src/main/kotlin/Dependencies.kt
object Versions {
    const val kotlin = "1.9.0"
    const val spring = "3.1.0"
    const val junit = "5.9.0"
}

object Libraries {
    const val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
    const val springBoot = "org.springframework.boot:spring-boot-starter:${Versions.spring}"
    const val junitJupiter = "org.junit.jupiter:junit-jupiter:${Versions.junit}"
}

Gradle Properties ermöglichen die Externalisierung von Konfigurationswerten. Die gradle.properties im Root-Projekt definiert Properties, die in allen Subprojekten verfügbar sind. Projekt-spezifische Properties können in lokalen gradle.properties überschrieben werden. System-Properties und Umgebungsvariablen ergänzen diese Hierarchie und ermöglichen umgebungsspezifische Konfiguration ohne Änderungen am Code.

Shared Build-Cache-Konfiguration maximiert Cache-Wiederverwendung über alle Subprojekte. Der lokale Build-Cache speichert Task-Outputs auf dem Entwicklerrechner, während ein Remote-Cache Team-weites Sharing ermöglicht. Die Cache-Konfiguration im Root-Projekt gilt automatisch für alle Subprojekte. Build-Cache-Keys berücksichtigen alle relevanten Inputs, wodurch Cache-Invalidierung bei Änderungen automatisch erfolgt.

19.6 Performance-Optimierung für große Projekte

Die Performance von Multi-Projekt-Builds degradiert ohne Optimierung mit wachsender Projekt-Anzahl. Configuration-on-Demand verzögert die Konfiguration von Projekten, bis sie tatsächlich benötigt werden. Diese Optimierung reduziert die Konfigurationszeit erheblich, besonders wenn nur einzelne Subprojekte gebaut werden. Die Aktivierung erfolgt über org.gradle.configureondemand=true, erfordert aber sorgfältige Vermeidung von Cross-Project-Konfiguration.

Die Modularisierung großer Projekte in kleinere, fokussierte Module verbessert nicht nur die Build-Performance, sondern auch die Code-Qualität. Jedes Modul sollte eine klare Verantwortlichkeit haben und minimale Dependencies zu anderen Modulen. Diese Struktur ermöglicht inkrementelle Builds, bei denen nur geänderte Module und ihre Abhängigen neu gebaut werden.

Build-Scans liefern detaillierte Performance-Analysen für Multi-Projekt-Builds. Die Visualisierung zeigt, welche Projekte und Tasks die meiste Zeit konsumieren. Kritische Pfade im Build-Graph werden identifiziert und können gezielt optimiert werden. Die Timeline-Ansicht offenbart Parallelisierungspotential und Bottlenecks. Regular Performance-Monitoring mit Build-Scans verhindert schleichende Performance-Degradation über Zeit.