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.
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.
| 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")
}
}
}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.
| 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")
}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.
| 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.
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.
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.
| 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.