In einem Java-Projekt gibt es oft zahlreiche Abhängigkeiten. Treten dann Versionskonflikte auf, wird der Build langsamer, und es ist nicht immer klar, von welcher Abhängigkeit eine bestimmte Bibliothek stammt. Genau diese Probleme adressiert Gradle mit seinem Configuration-System. Die Kernidee ist einfach: Gradle trennt strikt zwischen drei Aufgaben – das Deklarieren von Abhängigkeiten, das Auflösen der Abhängigkeiten und das Bereitstellen der Artefakte. Diese klare Trennung macht Builds nicht nur 30–50 % schneller(Durch die Kombination mit dem Cache), sondern auch deutlich wartbarer. Projekte wie das Spring Framework oder Netflix haben so ihre Build-Zeiten deutlich reduziert.
Gradle Configurations sind vergleichbar mit spezialisierten Containern. Bislang war jeder “Container” ein Art Alleskönner - das konnte zu unübersichtlichem Verhalten führen. Mit dem Configuration Pattern hat jeder “Container” genau eine Aufgabe:
Diese sind wie Einkaufslisten - sie sammeln nur, welche Dependencies
Sie brauchen, lösen aber nichts auf. Das ist Ihre
implementation oder api Configuration.
// So deklarieren Sie Dependencies - einfach und klar
dependencies {
implementation("org.springframework:spring-core:6.0.11") // Interne Abhängigkeit
api("com.google.guava:guava:32.1.3-jre") // Wird an Nutzer weitergegeben
}Diese sind die Consumer, die Ihre Einkaufsliste nehmen und tatsächlich die JARs herunterladen. Sie erstellen den Classpath für Compilation oder Runtime. Wichtig: Sie deklarieren niemals selbst Dependencies, sondern erweitern Bucket Configurations.
// Eine resolvable Configuration erstellen
val myClasspath = configurations.resolvable("myClasspath") {
extendsFrom(configurations.implementation.get()) // Nimmt Dependencies von implementation
}
// Praktisches Beispiel: Alle großen JARs finden
tasks.register("findLargeJars") {
doLast {
configurations.compileClasspath.files
.filter { it.length() > 10_000_000 }
.forEach { println("Große JAR gefunden: ${it.name}") }
}
}Diese stellen Artefakte für andere Projekte bereit - wie ein Laden, der Produkte verkauft. Wenn Sie eine Library bauen, exponieren diese Configurations Ihre JAR-Dateien.
// Configuration für API-Elemente, die andere Projekte nutzen können
configurations {
apiElements {
canBeConsumed = true
canBeResolved = false
outgoing {
artifact(tasks.jar) // Stellt die JAR-Datei bereit
}
}
}Nehmen wir an, Sie bauen eine Microservice-Anwendung mit gemeinsam genutzten Komponenten. Hier zeige ich Ihnen, wie Sie das sauber strukturieren:
// In Ihrem gemeinsamen Modul (shared-utils)
plugins {
`java-library` // Wichtig für die api Configuration
}
dependencies {
// Diese Dependencies werden an Nutzer weitergegeben
api("org.slf4j:slf4j-api:2.0.9")
// Diese bleiben intern
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
}
// In Ihrem Service-Modul
dependencies {
implementation(project(":shared-utils"))
// Sie bekommen automatisch slf4j-api, aber nicht jackson-databind
}Der Vorteil: Ihre Service-Module bekommen nur die Dependencies, die sie wirklich brauchen. Das verhindert Versionskonflikte und hält den Classpath sauber.
Ein häufiges Problem: Integration Tests brauchen zusätzliche Bibliotheken wie Testcontainers, die nicht im normalen Test-Classpath sein sollen.
// Schritt 1: Test Suite definieren
testing {
suites {
val integrationTest by registering(JvmTestSuite::class) {
useJUnitJupiter()
dependencies {
// Diese Dependencies nur für Integration Tests
implementation(project())
implementation("org.testcontainers:postgresql:1.19.1")
implementation("io.rest-assured:rest-assured:5.3.2")
}
}
}
}
// Schritt 2: Integration Tests nach Unit Tests ausführen
tasks.named<Test>("integrationTest") {
shouldRunAfter(tasks.test)
systemProperty("test.type", "integration")
}Viele Projekte generieren Code (z.B. mit JOOQ oder Protocol Buffers). Diese Generator-Tools sollen nicht im finalen Produkt landen:
// Eigene Configuration für Code-Generator
val codegen by configurations.creating {
canBeResolved = true // Kann aufgelöst werden
canBeConsumed = false // Wird nicht exponiert
}
dependencies {
codegen("com.squareup:javapoet:1.13.0")
}
// Task für Code-Generierung
val generateCode by tasks.registering(JavaExec::class) {
classpath = codegen // Nutzt nur codegen Dependencies
mainClass.set("com.example.Generator")
// Generierte Dateien zum Source Set hinzufügen
outputs.dir("$buildDir/generated/java")
}
sourceSets {
main {
java.srcDir(generateCode)
}
}Dieser Fehler tritt auf, wenn Sie versuchen, eine Configuration zu ändern, nachdem sie bereits aufgelöst wurde.
// ❌ FALSCH - Löst Configuration sofort auf
val files = configurations.runtimeClasspath.get().files
tasks.register<Copy>("copyLibs") {
from(files) // Configuration bereits aufgelöst!
into("libs")
}
// ✅ RICHTIG - Lazy Resolution
tasks.register<Copy>("copyLibs") {
from(configurations.runtimeClasspath) // Resolution erst bei Task-Ausführung
into("libs")
}Ein häufiger Fehler ist, Configurations während der Konfigurationsphase aufzulösen. Das macht Builds langsam und verhindert Configuration Caching.
// ❌ FALSCH - Resolution in der Konfigurationsphase
println("Dependencies: ${configurations.compileClasspath.get().files}")
// ✅ RICHTIG - Resolution nur wenn nötig
tasks.register("showDependencies") {
doLast {
println("Dependencies: ${configurations.compileClasspath.get().files}")
}
}Wenn verschiedene Dependencies unterschiedliche Versionen derselben Bibliothek brauchen:
configurations.all {
resolutionStrategy {
// Force eine spezifische Version
force("com.fasterxml.jackson.core:jackson-databind:2.15.3")
// Oder fail bei Konflikten für explizite Kontrolle
failOnVersionConflict()
// Oder nutze Substitution
dependencySubstitution {
substitute(module("log4j:log4j"))
.using(module("org.apache.logging.log4j:log4j-core:2.20.0"))
.because("log4j v1 hat Sicherheitslücken")
}
}
}Wenn Sie von Gradle 6.x oder älter migrieren, hier die wichtigsten Änderungen:
// ALT (Gradle < 7.0)
dependencies {
compile("...") // → implementation oder api
runtime("...") // → runtimeOnly
testCompile("...") // → testImplementation
}
plugins {
`java-library` // Für api Configuration
}
dependencies {
api("...") // Dependencies, die weitergegeben werden
implementation("...") // Interne Dependencies
runtimeOnly("...") // Nur zur Laufzeit benötigt
testImplementation("...") // Test Dependencies
}Fügen Sie zu Ihrer gradle.properties hinzu:
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
Dies kann Ihre Build-Zeit um 30-50% reduzieren, erfordert aber saubere Configuration-Patterns.
Anstatt Versionen überall zu wiederholen, nutzen Sie eine Platform:
dependencies {
// Platform definiert Versionen zentral
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.5"))
// Dependencies ohne Versionsangabe
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}configurations {
// Basis für alle Tests
val testBase by creating
// Spezialisierte Test-Typen
val unitTest by creating {
extendsFrom(testBase)
}
val integrationTest by creating {
extendsFrom(testBase)
}
}
dependencies {
testBase("org.junit.jupiter:junit-jupiter:5.9.3")
integrationTest("org.testcontainers:testcontainers:1.19.1")
}Gradle 8.1+ bietet spezielle Factory-Methoden, die automatisch die richtigen Flags setzen:
// Moderne, sichere Art Configurations zu erstellen
val myApi = configurations.dependencyScope("myApi")
val myClasspath = configurations.resolvable("myClasspath") {
extendsFrom(myApi.get())
}
val myElements = configurations.consumable("myElements") {
extendsFrom(myApi.get())
}Wenn etwas nicht funktioniert, helfen diese Kommandos:
# Zeigt alle Dependencies einer Configuration
./gradlew dependencies --configuration runtimeClasspath
# Erklärt, warum eine bestimmte Version gewählt wurde
./gradlew dependencyInsight --dependency jackson-databind
# Visualisiert den Build (kostenpflichtig, aber sehr hilfreich)
./gradlew build --scan
# Listet alle Configurations und ihre Eigenschaften
./gradlew configurationsDas neue Configuration-System in Gradle 8.x mag anfangs komplex wirken, aber die Grundidee ist einfach: Klare Trennung von Verantwortlichkeiten führt zu schnelleren, wartbareren Builds.
Merken Sie sich diese Kernregeln:
implementation statt
compile - das verhindert
Dependency-VerschmutzungMit diesen Patterns haben Projekte wie Spring Framework und Netflix ihre Build-Zeiten drastisch reduziert. Der initiale Aufwand für die Migration lohnt sich durch bessere Performance und weniger Build-Probleme in der Zukunft.
Der Schlüssel zum Erfolg liegt darin, die drei Rollen zu verstehen
und konsequent zu trennen. Beginnen Sie mit kleinen Schritten -
migrieren Sie zunächst von compile zu
implementation, aktivieren Sie dann Configuration Cache,
und optimieren Sie schrittweise Ihre Build-Logik. ## Übung
# Neues Verzeichnis anlegen und hineinwechseln
mkdir gradle-configs-demo
cd gradle-configs-demo
# Gradle-Projekt initialisieren
gradle init --type basic --dsl kotlin --project-name configs-demo
# Unterverzeichnisse für die zwei Module erstellen
mkdir -p producer/src/main/java/demo
mkdir -p consumer/src/main/java/demosettings.gradle.kts:
rootProject.name = "configs-demo"
include("producer", "consumer")producer/build.gradle.kts:
plugins {
java
}
// WICHTIG: Repositories definieren!
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// CONSUMABLE Configuration - kann von anderen genutzt werden
val myTools by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
// Ein paar Tools bereitstellen
dependencies {
myTools("com.google.guava:guava:32.1.3-jre")
myTools("org.apache.commons:commons-lang3:3.14.0")
}consumer/build.gradle.kts:
plugins {
java
}
// WICHTIG: Repositories definieren!
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// RESOLVABLE Configuration - kann Dependencies auflösen
val externalTools by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}
// Tools vom Producer-Modul holen
dependencies {
externalTools(project(":producer", configuration = "myTools"))
}
// Task: Zeige alle Tools (Configuration Cache kompatibel!)
tasks.register("showTools") {
// Input deklarieren für Configuration Cache
inputs.files(externalTools)
doLast {
println("\n📦 Tools in der Configuration:")
// Auf die Configuration über inputs zugreifen
inputs.files.files.forEach { file ->
println(" - ${file.name} (${file.length() / 1024} KB)")
}
}
}
// Task: Kopiere Tools in build-Ordner
tasks.register<Copy>("copyTools") {
from(externalTools)
into(layout.buildDirectory.dir("tools"))
doLast {
println("\n✅ ${outputs.files.asFileTree.files.size} Dateien nach build/tools kopiert")
}
}# Projekt bauen
./gradlew build
# Tools anzeigen (funktioniert jetzt!)
./gradlew :consumer:showTools
# Ausgabe:
# 📦 Tools in der Configuration:
# - guava-32.1.3-jre.jar (2931 KB)
# - commons-lang3-3.14.0.jar (651 KB)
# - failureaccess-1.0.1.jar (4 KB)
# - listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar (2 KB)
# - jsr305-3.0.2.jar (19 KB)
# - checker-qual-3.37.0.jar (231 KB)
# - error_prone_annotations-2.21.1.jar (15 KB)
# - j2objc-annotations-2.8.jar (8 KB)
# Tools kopieren
./gradlew :consumer:copyTools
# Prüfen was kopiert wurde
ls consumer/build/tools/# Dependencies vom Consumer anzeigen
./gradlew :consumer:dependencies --configuration externalTools
# Ausgabe:
# externalTools
# \--- project :producer (myTools)
# +--- com.google.guava:guava:32.1.3-jre
# | +--- com.google.guava:failureaccess:1.0.1
# | +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
# | +--- com.google.code.findbugs:jsr305:3.0.2
# | +--- org.checkerframework:checker-qual:3.37.0
# | +--- com.google.errorprone:error_prone_annotations:2.21.1
# | \--- com.google.j2objc:j2objc-annotations:2.8
# \--- org.apache.commons:commons-lang3:3.14.0val myTools by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}→ Kann von anderen Modulen genutzt werden
val externalTools by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}→ Kann Dependencies auflösen und Dateien herunterladen
Der Consumer erfüllt in dem Beispiel genau die Rolle, die das Kapitel didaktisch verdeutlichen soll:
Er definiert eine resolvable Configuration
(externalTools), die selbst nichts bereitstellt, sondern
nur Abhängigkeiten auflösen kann.
Über die Dependency-Deklaration
externalTools(project(":producer", configuration = "myTools"))bindet er sich an das vom Producer bereitgestellte Artefakt-Set.
Damit sorgt er dafür, dass die bereitgestellten Dependencies (Guava, Commons Lang, plus deren Transitive) heruntergeladen, im Build verfügbar und weiterverarbeitet werden können.
Mit den beiden Tasks (showTools und
copyTools) demonstriert er typische Verwendungen:
Kurz: Der Producer „stellt zur Verfügung“, der Consumer „holt ab und verarbeitet“. Damit wird der fundamentale Unterschied zwischen consumable und resolvable Configurations klar:
Der Producer ist im Beispiel der Gegenpart zum Consumer. Seine Aufgabe ist:
Er stellt eine consumable Configuration
(myTools) bereit.
val myTools by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}→ Diese Configuration kann exportiert, aber nicht selbst aufgelöst werden.
Er füllt diese Configuration mit Dependencies, hier z. B. Guava und Commons Lang.
dependencies {
myTools("com.google.guava:guava:32.1.3-jre")
myTools("org.apache.commons:commons-lang3:3.14.0")
}Damit macht er diese Bibliotheken für andere Projekte sichtbar, ohne sie selbst zu nutzen.
Im Multi-Projekt-Build kann ein anderes Modul (Consumer) explizit sagen:
externalTools(project(":producer", configuration = "myTools"))und erhält dann die Inhalte.
Kurz gesagt: Der Producer „schnürt ein Paket“ (seine consumable Configuration) und bietet es anderen Modulen an. Er ist der Lieferant von Abhängigkeiten, ohne selbst deren Auflösung oder Verarbeitung vorzunehmen.
Falls es noch einfacher sein soll - hier das absolute Minimum:
producer/build.gradle.kts:
repositories {
mavenCentral()
}
// CONSUMABLE Configuration
val myTools by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
dependencies {
myTools("org.apache.commons:commons-lang3:3.14.0")
}consumer/build.gradle.kts:
repositories {
mavenCentral()
}
// RESOLVABLE Configuration
val externalTools by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}
dependencies {
externalTools(project(":producer", configuration = "myTools"))
}
// Einfache Task ohne Configuration Cache Probleme
tasks.register("listTools") {
inputs.files(externalTools)
doLast {
println("Tools:")
inputs.files.files.forEach {
println("- ${it.name}")
}
}
}Mit:
./gradlew :consumer:listToolsErstellen Sie ein drittes Modul analyzer, das selbst
eine resolvable Configuration definiert. Dieses Modul
soll die vom consumer kopierten Artefakte
weiterverarbeiten. Ziel ist es, die Abhängigkeiten nicht nur
herunterzuladen, sondern in einer eigenen Task auszuwerten.
analyzer an und fügen Sie
es in settings.gradle.kts hinzu.analyzedTools, die sich die Dateien aus dem
consumer holt.analyzeTools, die alle
JARs ausgibt und deren Größe summiert.:analyzer:analyzeTools
aus und überprüfen Sie die Ausgabe.Ich erstelle für Sie eine Musterlösung für die Aufgabe mit dem
analyzer Modul:
settings.gradle.kts:
rootProject.name = "configs-demo"
include("producer", "consumer", "analyzer")consumer/build.gradle.kts (erweiterte
Version):
plugins {
java
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// RESOLVABLE Configuration - kann Dependencies auflösen
val externalTools by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}
// NEU: CONSUMABLE Configuration für andere Module
val processedTools by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
// Tools vom Producer-Modul holen
dependencies {
externalTools(project(":producer", configuration = "myTools"))
// Die gleichen Tools auch für andere Module bereitstellen
processedTools(project(":producer", configuration = "myTools"))
}
// Existierende Tasks bleiben unverändert
tasks.register("showTools") {
inputs.files(externalTools)
doLast {
println("\n📦 Tools in der Configuration:")
inputs.files.files.forEach { file ->
println(" - ${file.name} (${file.length() / 1024} KB)")
}
}
}
tasks.register<Copy>("copyTools") {
from(externalTools)
into(layout.buildDirectory.dir("tools"))
doLast {
println("\n✅ ${outputs.files.asFileTree.files.size} Dateien nach build/tools kopiert")
}
}analyzer/build.gradle.kts:
plugins {
java
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// RESOLVABLE Configuration - holt sich die Tools vom Consumer
val analyzedTools by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}
// Dependencies vom Consumer-Modul holen
dependencies {
analyzedTools(project(":consumer", configuration = "processedTools"))
}
// Task: Analysiere alle Tools
tasks.register("analyzeTools") {
// Input für Configuration Cache
inputs.files(analyzedTools)
doLast {
println("\n" + "=".repeat(60))
println("TOOL ANALYSE REPORT")
println("=".repeat(60))
val files = inputs.files.files.filter { it.name.endsWith(".jar") }
var totalSize = 0L
var maxSize = 0L
var maxFile = ""
println("\nGefundene JAR-Dateien:")
println("-".repeat(60))
files.sortedBy { it.name }.forEach { file ->
val sizeInKb = file.length() / 1024
val sizeInMb = file.length() / (1024.0 * 1024.0)
totalSize += file.length()
if (file.length() > maxSize) {
maxSize = file.length()
maxFile = file.name
}
println(String.format("%-50s %8d KB (%5.2f MB)",
file.name,
sizeInKb,
sizeInMb))
}
println("\n" + "=".repeat(60))
println("ZUSAMMENFASSUNG:")
println("-".repeat(60))
println("Anzahl JARs: ${files.size}")
println("Gesamtgröße: ${totalSize / 1024} KB (${String.format("%.2f", totalSize / (1024.0 * 1024.0))} MB)")
println("Durchschnittsgröße: ${if (files.isNotEmpty()) (totalSize / files.size) / 1024 else 0} KB")
println("Größte Datei: $maxFile (${maxSize / 1024} KB)")
println("=".repeat(60))
}
}
// Erweiterte Analyse-Task mit mehr Details
tasks.register("analyzeToolsDetailed") {
inputs.files(analyzedTools)
doLast {
println("\nDETAILLIERTE ANALYSE")
println("=".repeat(60))
val jarFiles = inputs.files.files.filter { it.name.endsWith(".jar") }
val groupedByPrefix = jarFiles.groupBy {
it.name.substringBefore("-").replace("_", "-")
}
println("\nNach Präfix gruppiert:")
groupedByPrefix.forEach { (prefix, files) ->
val totalSize = files.sumOf { it.length() }
println("\n $prefix:")
files.forEach { file ->
println(" - ${file.name} (${file.length() / 1024} KB)")
}
println(" Gesamt: ${totalSize / 1024} KB")
}
// Dependencies-Baum
println("\nDependency-Struktur:")
println(" guava")
println(" ├── failureaccess")
println(" ├── listenablefuture")
println(" ├── jsr305")
println(" ├── checker-qual")
println(" ├── error_prone_annotations")
println(" └── j2objc-annotations")
println(" commons-lang3 (standalone)")
}
}# Alle Module bauen
./gradlew build
# Analyzer-Task ausführen
./gradlew :analyzer:analyzeTools
# Detaillierte Analyse ausführen
./gradlew :analyzer:analyzeToolsDetailed
# Dependency-Graph anzeigen
./gradlew :analyzer:dependencies --configuration analyzedTools