6 Dependency Management und Repositories

6.1 Grundkonzepte des Dependency Managements

Dependency Management ist eine der Kernfunktionen von Gradle. Moderne Softwareprojekte nutzen dutzende bis hunderte externe Bibliotheken. Diese manuell herunterzuladen, zu versionieren und zu aktualisieren wäre nicht praktikabel. Gradle automatisiert diesen Prozess vollständig. Abhängigkeiten werden deklarativ definiert, und Gradle kümmert sich um Download, Caching und Classpath-Management.

Eine Dependency besteht aus drei Komponenten: Group ID, Artifact ID und Version. Die Group ID identifiziert die Organisation oder das Projekt, typischerweise in umgekehrter Domain-Notation wie org.springframework. Die Artifact ID benennt die spezifische Bibliothek, etwa spring-core. Die Version spezifiziert die gewünschte Release-Version. Diese Koordinaten bilden zusammen eine eindeutige Identifikation: org.springframework:spring-core:6.0.0.

Gradle unterscheidet zwischen direkten und transitiven Abhängigkeiten. Direkte Abhängigkeiten werden explizit im Build-Skript deklariert. Transitive Abhängigkeiten sind Abhängigkeiten der direkten Abhängigkeiten. Wenn ein Projekt Spring Boot einbindet, zieht dies automatisch Spring Core, Spring Context und weitere Bibliotheken nach sich. Gradle löst diese transitiven Abhängigkeiten automatisch auf und verwaltet den kompletten Dependency Graph.

Das Dependency Management löst auch Versionskonflikte. Wenn verschiedene Bibliotheken unterschiedliche Versionen derselben transitiven Abhängigkeit benötigen, wählt Gradle standardmäßig die höchste Version. Dieses Verhalten kann durch Dependency Constraints und Resolution Rules angepasst werden. Die Conflict Resolution ist deterministisch und reproduzierbar, was für stabile Builds essentiell ist.

6.2 Repository-Typen und ihre Konfiguration

Repositories sind die Quellen, aus denen Gradle Abhängigkeiten bezieht. Maven Central ist das größte öffentliche Repository für Java-Bibliotheken und wird in den meisten Projekten verwendet. Die Konfiguration erfolgt mit einer einzigen Zeile: repositories { mavenCentral() }. Google’s Maven Repository enthält Android-Bibliotheken und wird mit google() eingebunden.

6.2.1 Repository-Typen im Überblick

Repository-Typ Verwendung Konfiguration Typische Inhalte
Maven Central Standard Java-Bibliotheken mavenCentral() Open-Source Libraries
Google Maven Android-Entwicklung google() Android SDK, AndroidX
JCenter Deprecated (nicht mehr verwenden) jcenter() Legacy-Projekte
Maven Local Lokale Tests mavenLocal() Lokal gebaute Artifacts
Custom Maven Unternehmens-Repositories maven { url = uri("...") } Interne Bibliotheken
Flat Directory File-basierte Dependencies flatDir { dirs("libs") } JAR-Dateien im Projekt

Unternehmen betreiben häufig private Repository-Manager wie Nexus oder Artifactory. Diese fungieren als Proxy für öffentliche Repositories und hosten gleichzeitig interne Artefakte. Die Konfiguration eines privaten Maven-Repositories erfolgt über die URL:

repositories {
    maven {
        url = uri("https://repo.company.com/maven2")
        credentials {
            username = property("repoUser") as String
            password = property("repoPassword") as String
        }
    }
}

Die Reihenfolge der Repository-Deklarationen ist relevant. Gradle prüft Repositories sequenziell, bis eine Abhängigkeit gefunden wird. Interne Repositories sollten vor öffentlichen Repositories stehen, um den Netzwerk-Traffic zu minimieren und die Auflösung zu beschleunigen. Ein typisches Setup priorisiert den unternehmensinternen Artifactory, gefolgt von Maven Central als Fallback.

Lokale Repositories dienen Entwicklungs- und Testzwecken. Das Maven Local Repository unter ~/.m2/repository kann mit mavenLocal() eingebunden werden. Dies ermöglicht das Testen von lokal gebauten Bibliotheken vor der Veröffentlichung. File-basierte Repositories referenzieren Verzeichnisse im Dateisystem:

repositories {
    flatDir {
        dirs("libs", "../shared-libs")
    }
}

Repository-Content-Filtering optimiert die Dependency Resolution. Wenn bekannt ist, dass bestimmte Gruppen nur in spezifischen Repositories existieren, kann dies explizit konfiguriert werden:

repositories {
    maven {
        url = uri("https://repo.spring.io/release")
        content {
            includeGroup("org.springframework")
        }
    }
}

6.3 Configurations und Scopes

Configurations definieren Gruppen von Abhängigkeiten für verschiedene Zwecke. Das Java-Plugin stellt mehrere Standard-Configurations bereit. Die implementation Configuration enthält Abhängigkeiten für Kompilierung und Laufzeit. Diese Abhängigkeiten sind nicht Teil der API und werden nicht an Konsumenten weitergegeben. Dies verbessert die Kapselung und reduziert die Rekompilierung bei Änderungen.

6.3.1 Standard-Configurations des Java-Plugins

Configuration Scope Verwendung Beispiel
implementation Compile + Runtime Standard-Dependencies, nicht Teil der API Spring Boot, Apache Commons
api Compile + Runtime + Export Öffentliche API-Dependencies Interfaces, öffentliche Typen
compileOnly Nur Compile Zur Laufzeit bereitgestellt Lombok, Servlet API
runtimeOnly Nur Runtime Nicht beim Kompilieren benötigt JDBC-Treiber, Logback
testImplementation Test Compile + Runtime Test-Frameworks JUnit, Mockito, AssertJ
testCompileOnly Nur Test Compile Test-Annotations JUnit Platform
testRuntimeOnly Nur Test Runtime Test-Ausführung H2 Database, Test Containers
annotationProcessor Compile Code-Generierung MapStruct, Dagger

Die api Configuration macht Abhängigkeiten Teil der öffentlichen API. Wenn eine Bibliothek Typen aus einer Abhängigkeit in ihrer öffentlichen Schnittstelle exponiert, muss diese Abhängigkeit als api deklariert werden. Dies ist relevant für Bibliotheksprojekte, die von anderen Projekten konsumiert werden. Die Verwendung von api sollte minimiert werden, da sie die Kopplung erhöht.

compileOnly definiert Abhängigkeiten, die nur zur Compile-Zeit benötigt werden. Typische Beispiele sind Annotation Processors wie Lombok oder die Servlet API in Webanwendungen. Diese Abhängigkeiten werden vom Application Server bereitgestellt und dürfen nicht im Deployment-Artefakt enthalten sein. Das Gegenstück runtimeOnly enthält Abhängigkeiten, die nur zur Laufzeit benötigt werden, wie JDBC-Treiber oder Logging-Implementierungen.

Test-Configurations folgen dem gleichen Muster mit dem Präfix test. Die Configuration testImplementation enthält Test-Frameworks wie JUnit oder Mockito. Diese sind nur beim Kompilieren und Ausführen von Tests verfügbar. testRuntimeOnly könnte einen speziellen JDBC-Treiber für eine In-Memory-Testdatenbank enthalten.

Custom Configurations erweitern das Standard-Set für spezielle Anforderungen. Eine Configuration für Code-Generatoren könnte folgendermaßen definiert werden:

val codegen by configurations.creating

dependencies {
    codegen("org.jooq:jooq-codegen:3.18.0")
}

tasks.register<JavaExec>("generateCode") {
    classpath = configurations["codegen"]
    mainClass.set("org.jooq.codegen.GenerationTool")
}

6.4 Versionsmanagement-Strategien

Versionsnummern folgen üblicherweise Semantic Versioning mit Major.Minor.Patch-Schema. Gradle unterstützt verschiedene Versionsnotationen. Exakte Versionen wie 2.5.0 garantieren Reproduzierbarkeit, verhindern aber automatische Updates. Dynamische Versionen mit 2.5.+ oder latest.release ermöglichen automatische Patch-Updates, bergen aber Stabilitätsrisiken.

6.4.1 Versions-Notationen in Gradle

Notation Beispiel Bedeutung Verwendung
Exakte Version 2.5.0 Genau diese Version Produktions-Builds
Dynamische Patch 2.5.+ Neueste 2.5.x Version Patch-Updates erlauben
Dynamische Minor 2.+ Neueste 2.x Version Feature-Updates erlauben
Latest latest.release Neueste stabile Version Entwicklung/Experimente
Version Range [2.0, 3.0) 2.0 bis 2.9.x Kontrollierter Spielraum
Strict {strictly 2.5.0} Exakt 2.5.0, keine Upgrades Kritische Dependencies
Prefer 2.5.0!! Bevorzuge 2.5.0 bei Konflikten Konfliktauflösung
Reject !2.4.0 Alles außer 2.4.0 Bekannte fehlerhafte Version

Range-Notationen bieten kontrollierten Spielraum. Die Notation [2.0, 3.0) akzeptiert alle Versionen von 2.0 inklusive bis 3.0 exklusive. Prefer-Notationen mit 2.5.0!! forcieren eine Version auch bei Konflikten. Required-Notationen mit {strictly 2.5.0} verhindern jegliche Version-Upgrades durch transitive Abhängigkeiten.

Platform BOMs (Bill of Materials) zentralisieren Versionsmanagement. Spring Boot’s BOM definiert kompatible Versionen für das gesamte Spring-Ökosystem. Die Einbindung erfolgt über die platform Notation:

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.0"))
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}

Die konkreten Versionen der Starter-Abhängigkeiten werden vom BOM vorgegeben und müssen nicht explizit angegeben werden. Dies garantiert Kompatibilität zwischen allen Spring-Komponenten.

6.5 Caching und Offline-Builds

Gradle implementiert ein ausgeklügeltes Caching-System für Abhängigkeiten. Heruntergeladene Artefakte werden im Gradle-Cache unter ~/.gradle/caches/modules-2 gespeichert. Dieser Cache wird projektübergreifend geteilt. Eine einmal heruntergeladene Bibliothek muss nicht erneut geladen werden, selbst wenn sie in anderen Projekten verwendet wird.

Der Cache verwendet Checksummen zur Integritätsprüfung. Gradle verifiziert MD5 und SHA1-Hashes der heruntergeladenen Dateien gegen die Repository-Metadaten. Beschädigte oder manipulierte Dateien werden erkannt und abgelehnt. Bei Verifikationsfehlern lädt Gradle die Datei erneut herunter.

Cache-Timeouts steuern die Aktualität. Standardmäßig cached Gradle dynamische Versionen für 24 Stunden und SNAPSHOT-Versionen für 0 Sekunden. Diese Timeouts können angepasst werden:

configurations.all {
    resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
    resolutionStrategy.cacheDynamicVersionsFor(10, TimeUnit.MINUTES)
}

Offline-Builds ermöglichen Arbeit ohne Netzwerkzugang. Der Parameter --offline nutzt ausschließlich gecachte Abhängigkeiten. Fehlende Abhängigkeiten führen zum Build-Fehler. Dies ist nützlich in restriktiven Umgebungen oder bei instabiler Netzwerkverbindung.

Der Dependency Cache kann mit --refresh-dependencies invalidiert werden. Gradle prüft dann alle Abhängigkeiten auf Updates, respektiert aber weiterhin Version-Constraints. Für komplette Neuauflösung kann der Cache-Ordner gelöscht werden, was aber nur in Ausnahmefällen notwendig ist.

6.6 Fehlerdiagnose und Dependency Insights

Die Analyse von Dependency-Problemen erfordert geeignete Werkzeuge. Der Befehl gradle dependencies visualisiert den kompletten Dependency-Tree. Die Ausgabe zeigt alle Configurations mit ihren direkten und transitiven Abhängigkeiten. Versionskonflikte werden mit Pfeilen markiert, die zeigen, welche Version gewählt wurde.

6.6.1 Wichtige Diagnose-Befehle

Befehl Zweck Beispiel
dependencies Zeigt vollständigen Dependency-Tree gradle dependencies --configuration runtimeClasspath
dependencyInsight Details zu spezifischer Dependency gradle dependencyInsight --dependency slf4j-api
buildEnvironment Build-Script Dependencies gradle buildEnvironment
dependencyUpdates Prüft auf neuere Versionen gradle dependencyUpdates (Plugin erforderlich)
--refresh-dependencies Cache-Refresh erzwingen gradle build --refresh-dependencies
--scan Interaktive Web-Analyse gradle build --scan

Dependency Insight liefert detaillierte Informationen zu spezifischen Abhängigkeiten. Der Befehl gradle dependencyInsight --dependency slf4j-api zeigt alle Pfade, über die eine Bibliothek eingebunden wird. Dies hilft bei der Identifikation unerwünschter transitiver Abhängigkeiten oder Versionskonflikte.

Build Scans bieten eine interaktive Dependency-Analyse. Die Web-Oberfläche ermöglicht Suche, Filterung und Navigation durch den Dependency-Graph. Konflikte, veraltete Versionen und Sicherheitslücken werden hervorgehoben. Die Timeline zeigt, wann Abhängigkeiten heruntergeladen wurden und wie lange dies dauerte.

Strict Version Checking verhindert unbeabsichtigte Downgrades. Die Konfiguration failOnVersionConflict() lässt den Build fehlschlagen, wenn Versionskonflikte auftreten. Dies erzwingt explizite Konfliktauflösung durch force-Direktiven oder Dependency Constraints. Für kritische Produktionsumgebungen ist diese strikte Kontrolle empfehlenswert.