Lazy Configuration revolutioniert die Art, wie Gradle Build-Skripte
ausgewertet und Tasks konfiguriert werden. Das traditionelle
Eager-Configuration-Modell evaluierte alle Tasks während der
Konfigurationsphase, unabhängig davon, ob sie tatsächlich ausgeführt
wurden. Bei einem Projekt mit hunderten Tasks bedeutete dies, dass
selbst für einen simplen gradle help Aufruf alle Tasks
konfiguriert wurden. Diese unnötige Arbeit verlängerte Build-Zeiten
erheblich.
Das Lazy-Configuration-Paradigma verschiebt die Konfiguration auf den tatsächlichen Bedarfszeitpunkt. Tasks werden erst konfiguriert, wenn sie definitiv ausgeführt werden oder ihre Outputs von anderen Tasks benötigt werden. Diese Verzögerung reduziert die Konfigurationszeit drastisch, besonders in großen Multi-Modul-Projekten. Ein Build, der nur Tests ausführt, muss keine Deployment- oder Publishing-Tasks konfigurieren.
Die technische Basis bilden die Provider und Property APIs, eingeführt in Gradle 4.0 und kontinuierlich erweitert. Diese APIs ermöglichen die Deklaration von Werten, ohne sie sofort zu berechnen. Ein Provider repräsentiert einen zukünftigen Wert, der erst bei Bedarf materialisiert wird. Properties sind veränderbare Container, die Provider als Inputs akzeptieren und selbst als Provider fungieren.
Provider ist das zentrale Interface für lazy Werte in Gradle. Ein Provider kapselt die Berechnung eines Wertes und führt diese erst bei Zugriff aus. Gradle erstellt Provider für verschiedene Kontexte: Task-Outputs, Projekt-Properties, File-Collections und berechnete Werte.
Die Erstellung und Verwendung von Providern:
val timestampProvider = providers.provider {
println("Computing timestamp")
System.currentTimeMillis()
}
tasks.register("printTimestamp") {
doLast {
println("Timestamp: ${timestampProvider.get()}")
}
}Die Berechnung erfolgt nur, wenn der Task tatsächlich ausgeführt
wird. Bei gradle tasks wird der Provider nie evaluiert, die
println-Ausgabe erscheint nicht.
Provider-Transformationen ermöglichen Verkettung ohne Evaluation:
val projectVersion = providers.provider { version.toString() }
val jarName = projectVersion.map { version -> "${project.name}-$version.jar" }
val jarPath = jarName.map { name -> layout.buildDirectory.file("libs/$name").get() }
tasks.register<Copy>("copyJar") {
from(jarPath)
into(layout.projectDirectory.dir("dist"))
}Die gesamte Transformation-Chain bleibt lazy. Erst wenn der Copy-Task seine Inputs benötigt, werden alle Provider in der Kette evaluiert. Diese Verkettung ermöglicht komplexe Abhängigkeiten zwischen Werten ohne Performance-Penalty während der Konfiguration.
Provider bieten null-safe Operationen. Die orElse
Methode definiert Fallback-Werte, orNull liefert null statt
NoSuchElementException:
val customVersion = providers.systemProperty("build.version")
val effectiveVersion = customVersion.orElse(providers.provider { project.version.toString() })Properties erweitern Provider um Mutabilität. Eine Property kann gesetzt, an Provider gebunden oder mit Conventions versehen werden. Task-Properties nutzen diese API für flexible Konfiguration:
abstract class CustomTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun execute() {
outputFile.get().asFile.writeText(message.get())
}
}
tasks.register<CustomTask>("writeMessage") {
message.set("Hello Gradle")
outputFile.set(layout.buildDirectory.file("message.txt"))
}Properties unterstützen Convention-Werte, die als Defaults fungieren aber überschrieben werden können:
message.convention("Default message")
// Später überschreibbar
message.set("Custom message")Configuration Avoidance nutzt lazy APIs, um unnötige
Task-Konfiguration zu vermeiden. Die register Methode
erstellt Tasks lazy, während create sofort
konfiguriert:
// Lazy - Task wird nur bei Bedarf konfiguriert
tasks.register<JavaCompile>("compileExtra") {
source = fileTree("src/extra")
}
// Eager - Task wird sofort konfiguriert (vermeiden!)
tasks.create<JavaCompile>("compileEager") {
source = fileTree("src/extra")
}Die named Methode liefert einen TaskProvider für
existierende Tasks:
val testTask = tasks.named<Test>("test")
testTask.configure {
maxHeapSize = "2g"
}Diese Configuration erfolgt nur, wenn der test-Task Teil des Build-Graphen wird.
Die Migration bestehender Build-Skripte zu Lazy Configuration erfolgt durch systematisches Ersetzen eager Patterns. Direct Task Access wird durch Provider-basierte Referenzen ersetzt:
// Eager - Alte Schreibweise
task myTask {
dependsOn tasks.getByName("compile")
}
// Lazy - Moderne Schreibweise
tasks.register("myTask") {
dependsOn(tasks.named("compile"))
}Project-Properties migrieren von direktem Zugriff zu Provider-basiertem Access:
// Eager
val buildNumber = project.findProperty("buildNumber") ?: "0"
tasks.register("printBuild") {
doLast {
println(buildNumber)
}
}
// Lazy
val buildNumber = providers.systemProperty("buildNumber").orElse("0")
tasks.register("printBuild") {
doLast {
println(buildNumber.get())
}
}File-Operations nutzen lazy FileCollection und ConfigurableFileTree:
// Eager - Files werden sofort gesucht
val sourceFiles = fileTree("src").matching {
include("**/*.java")
}
// Lazy - Files werden erst bei Bedarf gesucht
val sourceFiles = providers.provider {
fileTree("src").matching {
include("**/*.java")
}
}Task-Inputs und -Outputs verwenden Property-Types statt direkte Werte:
// Eager
task.inputs.file("config.xml")
task.outputs.file("${buildDir}/output.txt")
// Lazy
task.inputs.file(layout.projectDirectory.file("config.xml"))
task.outputs.file(layout.buildDirectory.file("output.txt"))Lazy Configuration reduziert Konfigurationszeit signifikant. Messungen in realen Projekten zeigen Verbesserungen von 30-70% für die Configuration-Phase. Ein Gradle-Enterprise-Projekt mit 500 Submodulen reduzierte die Konfigurationszeit von 45 auf 12 Sekunden durch konsequente Lazy-Configuration-Nutzung.
Die Performance-Gewinne skalieren mit Projektgröße. Kleine Projekte
mit wenigen Tasks profitieren minimal. Multi-Modul-Projekte mit
hunderten Tasks und komplexen Inter-Task-Dependencies zeigen dramatische
Verbesserungen. Der Effekt verstärkt sich bei häufig ausgeführten,
fokussierten Builds wie gradle test oder
gradle assemble.
Build Scans visualisieren Configuration Avoidance Erfolge. Die Timeline zeigt vermiedene Task-Konfigurationen als graue Einträge. Die Configuration Performance Sektion quantifiziert eingesparte Zeit. Diese Metriken helfen bei der Identifikation weiterer Optimierungspotentiale.
Profiling mit --profile generiert detaillierte
Reports:
Configuration on demand is an incubating feature.
:buildSrc:compileKotlin UP-TO-DATE
:buildSrc:jar UP-TO-DATE
Configuration time: 2.451 secs (avoided 85%)
Task execution time: 15.234 secs
Total time: 17.685 secs
Der “avoided” Prozentsatz zeigt den Erfolg der Lazy Configuration. Werte über 80% sind in gut strukturierten Projekten erreichbar.
Die konsequente Verwendung der Provider API maximiert
Performance-Gewinne. Alle Task-Konfigurationen sollten in
configure Blocks erfolgen, nicht während der Registration.
Provider-Chains sollten so lange wie möglich lazy bleiben. Die
get() Methode sollte nur in Task Actions aufgerufen werden,
nie während der Konfiguration.
Task-Dependencies nutzen Provider-basierte Methoden:
tasks.register("aggregate") {
val testTasks = tasks.withType<Test>()
dependsOn(testTasks)
inputs.files(testTasks.map { it.outputs.files })
}Dieser Pattern vermeidet die Iteration über alle Test-Tasks während der Konfiguration.
Cross-Project-Dependencies bleiben lazy durch Project-Provider:
val apiProject = project.provider { project(":api") }
dependencies {
implementation(apiProject)
}Convention Plugins nutzen lazy APIs für flexible Defaults:
interface MyExtension {
val serverUrl: Property<String>
}
val extension = extensions.create<MyExtension>("myPlugin")
extension.serverUrl.convention(providers.environmentVariable("SERVER_URL")
.orElse("https://default.server.com"))Error Handling in lazy Kontexten erfordert Sorgfalt.
Provider-Evaluation kann Exceptions werfen, die erst zur Execution-Zeit
auftreten. Defensive Programmierung mit mapNotNull und
orElse verhindert Runtime-Fehler:
val safePath = providers.systemProperty("custom.path")
.mapNotNull { path ->
try {
file(path).takeIf { it.exists() }?.absolutePath
} catch (e: Exception) {
null
}
}
.orElse(layout.projectDirectory.dir("default").asFile.absolutePath)Die Dokumentation lazy Patterns in Team-Guidelines standardisiert die Verwendung. Code Reviews prüfen auf eager Anti-Patterns. Automated Tests validieren Configuration Avoidance durch Assertions auf Task-Registrations. Diese Praktiken etablieren Lazy Configuration als Standard und verhindern Performance-Regressionen.
Gradle unterscheidet zwischen eager und lazy Configuration.
Wichtige Mechanismen:
mkdir lazy-demo && cd lazy-demo
gradle init --type basic --dsl kotlin
mkdir -p src/main/javaimport java.time.LocalDateTime
plugins {
java
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
dependencies {
testImplementation("junit:junit:4.13.2")
}
// Provider für verzögerte Berechnung
val zeitstempel = providers.provider {
println(">>> BERECHNE Zeitstempel")
LocalDateTime.now().toString()
}
// Custom Task mit Properties
abstract class MeinTask : DefaultTask() {
@get:Input
abstract val eingabe: Property<String>
@get:OutputFile
abstract val ausgabe: RegularFileProperty
@TaskAction
fun aktion() {
ausgabe.get().asFile.writeText(eingabe.get())
println("Datei geschrieben: ${eingabe.get()}")
}
}
// Lazy Task Registration
tasks.register<MeinTask>("meinTask") {
eingabe.set(zeitstempel)
ausgabe.set(layout.buildDirectory.file("output.txt"))
}
// Zusätzlicher Demo-Task
tasks.register("demo") {
doLast {
println("Demo-Task läuft")
}
}public class Demo {
public static void main(String[] args) {
System.out.println("Lazy Demo");
}
}./gradlew demo Task meinTask wird nicht
konfiguriert, der Provider nicht ausgewertet.
./gradlew meinTask Task wird konfiguriert, Provider
liefert den Zeitstempel, Datei wird geschrieben.
tasks.create("task") → sofortige Konfiguration
(eager)tasks.register("task") → Konfiguration bei Bedarf
(lazy)providers.provider { ... } → Berechnung nur bei
ZugriffProperty<T> → verzögerte Bindung von Eingaben und
AusgabenErgänze dein Projekt um zwei Tasks:
tasks.create("eagerTask"), der beim
Konfigurieren sofort eine Ausgabe
(println("Konfiguriere eagerTask")) erzeugt.tasks.register("lazyTask"), der dasselbe
tut – aber erst im doLast-Block.Führe ./gradlew tasks aus.
Führe ./gradlew lazyTask aus.
Erstelle zusätzlich einen Provider, der einen Zufallswert erzeugt:
val randomProvider = providers.provider {
println("Berechne Zufallswert")
(1..1000).random()
}Verwende diesen Provider in beiden Tasks. Vergleiche, wann die Berechnung ausgelöst wird.