Optimierung der Gradle-Aufgabenverwaltung mit Kotlin DSL und buildSrc
Publié le 11 July 2025
Die Automatisierung von Build-Aufgaben ist für jedes Softwareprojekt entscheidend, und Gradle bietet mit seinem Kotlin-DSL eine außergewöhnliche Flexibilität. Im Verlauf unseres Gesprächs haben wir erforscht, wie man Build-Logik zentralisieren und wiederverwenden kann, insbesondere für Testberichte, indem manallprojects, buildSrc, und personalisierte Aufgaben in Kotlin.
Zentralisiere die Konfigurationen mit allprojects und `subprojects
Die Blöcke`allprojects` et `subprojects`in Ihrem`build.gradle.kts`Die Wurzeln sind grundlegend für die Anwendung gemeinsamer Konfigurationen über dein Multi-Modul-Projekt.
*allprojects { … }`Wende die Konfiguration auf denRoot-Projekt und zu allen seinen Unterprojekten. Ideal zum Definierenden eines`group, eine`version`, oder einige`repositories`gemeinsame. *subprojects { … }`Wende die Konfiguration annur zu den Unterprojekten, ohne das Stammprojekt. Perfekt zum Anwenden von modulspezifischen Plugins (wie`java ou kotlin-jvm) oder gemeinsame Abhängigkeiten Ihrer Bibliotheken.
Hier ist ein illustratives Beispiel:
// build.gradle.kts (projet racine)
plugins {
base // Appliqué au projet racine
}
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
tasks.withType<org.gradle.api.tasks.testing.Test> {
useJUnitPlatform() // Configuration commune des tests pour tous les projets
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "org.jetbrains.kotlin.jvm")
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
}
\==`buildSrc`Das Schweizer Taschenmesser der Build-Logik
Wenn die Logik Ihrer Aufgaben komplex wird oder wiederverwendet werden muss,buildSrcist die bevorzugte Lösung. Es ist ein spezielles Gradle-Modul, das vor den Haupt-Build-Skripten kompiliert wird, wodurch seine Klassen im Klassenpfad des gesamten Builds verfügbar werden.
Warum verwenden`buildSrc`für die Aufgaben ?
-
Wiederverwendbarkeit: Eine definierte Aufgabe in`buildSrc`kann auf jedes Build-Projekt angewendet werden.
-
OrganisationZentralisiert den Build-Code, wodurch er sauberer und wartbarer wird.
-
Typsicherheit und Autovervollständigung: Der Kotlin-Code in`buildSrc`wird kompiliert, bietet die Fehlerprüfung und Autovervollständigung Ihrer IDE, verbessert das Entwicklungserlebnis.
\=== Flussdiagramm`buildSrc`(Code PlantUML)
Kopieren Sie den unten stehenden Code und fügen Sie ihn in ein Tool ein, das PlantUML unterstützt.
[source,plantuml]
@startuml skinparam handwritten true skinparam monochrome wahr
Rechteck "Gradle Build Process" { Komponente "buildSrc" als BS { file "MyCustomTask.kt" as T Datei "MyConventionPlugin.kt" als P } Komponente "Root Project" als RP Komponente "Subproject A" als SA Komponente "Subproject B" als SB }
T --\> P : "ist definiert in P -→ RP : "wird angewendet auf P --\> SA : "is applied to P --\> SB : "wird angewendet auf
RP --|\> SA : "enthält RP --|\> SB : "contains
RP -up→ BS : "abhängig von (für die Build-Logik) SA -up-\> BS : "abhängig von (für die Build-Logik) SB -up-\> BS : "abhängig von (für die Build-Logik)
Notiz rechts von T Benutzerdefinierte Aufgabenklassen (z. B.: OpenTestReportTask) Endnote
Notiz rechts von P Konventions-Plugins die die Aufgaben aufzeichnen Endnote
@enduml
\== Erstellung von abstrakten Berichtsaufgaben
Um die Testberichte zu verwalten, haben wir einen modularen Ansatz entwickelt, indem wir eine abstrakte Aufgabenclasse in`buildSrc`. Je nach Ihren Bedürfnissen kann diese Klasse von erben`DefaultTask` ou de Exec.
\=== Abstrakte Aufgabe`OpenTestReportTask`(erbend von`DefaultTask`)
Dieser Ansatz wird empfohlen, wenn Sie eine benutzerdefinierte Kotlin-Logik benötigen, die mit dem Dateisystem oder anderen Gradle-APIs interagiert und anschließend einen externen Befehl ausführt.
[source,kotlin]
package com.yourpackage
import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction import java.io.File
abstract class OpenTestReportTask : DefaultTask() {
----
----
init {
group = "verification"
description = "Opens a test report in Firefox."
dependsOn("check") // Assure que les rapports sont générés
}
@get:Input
abstract var reportPath: String
@TaskAction
fun openReport() {
val separator = File.separator
val reportFile = project.layout.projectDirectory.asFile.toPath()
.resolve(reportPath.replace("/", separator))
.toAbsolutePath()
.toFile()
if (!reportFile.exists()) {
logger.warn("Report file does not exist: $reportFile. Ensure 'check' ran.")
return
}
project.exec {
commandLine("firefox", "--new-tab", reportFile.absolutePath)
}
logger.lifecycle("Opened test report: ${reportFile.absolutePath}")
}
----
## }
\=== konkrete Implementierungen
Diese Klassen erben von der abstrakten Aufgabe und definieren den spezifischen Pfad des Berichts.
## [source,kotlin]
package com.yourpackage
abstract class ReportUnitTestsTask : OpenTestReportTask() init { description = "Öffnet den Einzeltestbericht in Firefox. reportPath = "build/reports/tests/test/index.html } }
package com.yourpackage
## abstract class ReportFunctionalTestsTask : OpenTestReportTask() { init { description = "Öffnet den Funktionaltestbericht in Firefox." reportPath = "build/reports/tests/functionalTest/index.html" } }
\=== Aufzeichnung der Aufgaben
Im`build.gradle.kts`von Ihrem Stammprojekt :
## [source,kotlin]
tasks.register\<com.yourpackage.ReportUnitTestsTask\>("reportTests") {} tasks.register\<com.yourpackage.ReportFunctionalTestsTask\>("reportFunctionalTests") {}
\=== UML-Diagramm der Berichtsaufgaben (Code PlantUML)
Kopieren Sie den unten stehenden Code und fügen Sie ihn in ein Tool ein, das PlantUML unterstützt.
## [source,plantuml]
@startuml skinparam handgeschrieben true skinparam monochrome true
abstrakte Klasse DefaultTask { }
abstrakte Klasse Exec { \+ commandLine(args: String...) \+ exec() }
abstrakte Klasse OpenTestReportTask extends DefaultTask { \+ group: String = "verification \+ description: String \+ dependsOn("check") + abstrakt reportPath: String \+ openReport() : void }
abstrakte Klasse AbstractJbakeExecTask extends Exec { \+ group: String = "Überprüfung \+ description: String \+ dependsOn("check") \+ abstract reportRelativePath: String \+ exec() : void }
class ReportUnitTestsTask extends OpenTestReportTask { \+ reportPath: String = "build/reports/tests/test/index.html }
class ReportFunctionalTestsTask extends OpenTestReportTask { \+ reportPath: String = "build/reports/tests/functionalTest/index.html }
class ReportJbakeTestsTask extends AbstractJbakeExecTask { \+ reportRelativePath: String = "build/reports/tests/test/index.html }
class ReportJbakeFunctionalTestsTask extends AbstractJbakeExecTask { \+ reportRelativePath: String = "build/reports/tests/functionalTest/index.html }
OpenTestReportTask \<-- ReportUnitTestsTask OpenTestReportTask \<-- ReportFunctionalTestsTask
AbstractJbakeExecTask <-- ReportJbakeTestsTask AbstractJbakeExecTask \<-- ReportJbakeFunctionalTestsTask
DefaultTask \<|-- OpenTestReportTask Exec \<|-- AbstractJbakeExecTask Standardausgabe \<|-- Ausführen
## @enduml
\== Abstrakte Aufgabe`AbstractJbakeExecTask`(erben von`Exec`)
Wenn Ihre Aufgabe hauptsächlich darin besteht, einen externen Befehl mit variablen Argumenten auszuführen, erben Sie direkt von**Ausführen**ist direkter.
[source,kotlin]
package com.ihrpaket
import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Input import java.io.File
abstrakte Klasse AbstractJbakeExecTask : Exec() {
----
init { group = "verification" description = "Opens a Jbake project report in Firefox." dependsOn("check") }
@get:Input abstract var reportRelativePath: String
override fun exec() { val separator = File.separator val reportFile = project.layout.projectDirectory.asFile.toPath() .resolve(reportRelativePath.replace("/", separator)) .toAbsolutePath() .toFile()
if (!reportFile.exists()) { logger.warn("Report file does not exist: $reportFile. Ensure 'check' ran.") return }
commandLine("firefox", "--new-tab", reportFile.absolutePath) logger.lifecycle("Attempting to open report: ${reportFile.absolutePath}") super.exec() // Appelle la méthode exec() de la super-classe Exec }
## }
\=== Konkrete Implementierungen für`Exec`
## [source,kotlin]
Paket com.yourpackage
abstrakte Klasse ReportJbakeTestsTask : AbstractJbakeExecTask() { init { description = "Öffnet den Jbake-Unit-test Bericht in Firefox. reportRelativePath = "build/reports/tests/test/index.html } }
package com.yourpackage
## abstract class ReportJbakeFunctionalTestsTask : AbstractJbakeExecTask() { init { description = "Öffnet den Funktionaltestbericht in Firefox." reportRelativePath = "build/reports/tests/functionalTest/index.html" } }
\== Sichtbarkeit der Aufgaben`buildSrc`
Die Aufgaben und Klassen, die Sie in**buildSrc**sind :
***Sichtbar und nutzbar**von allen Projekten deines Hauptbuilds (Wurzel- und Unterprojekte). Aus diesem Grund kannst du verwenden`com.yourpackage.ReportUnitTestsTask`in`allprojects { ... }`. ***Nicht ausführbar**direkt als buildSrc-Aufgaben (zum Beispiel,`gradle :buildSrc:reportTests`würde nicht funktionieren wenn die Aufgabe nicht speziell gespeichert wird in`buildSrc/build.gradle.kts`).`buildSrc`ist ein Kompiliermodul, kein ausführbares Anwendungsmodul für diese Aufgaben des Haupt-Builds.
\=== Einen Bericht für die Tests von`buildSrc`ihm selbst
Si `buildSrc`er hat seine eigenen Tests und generiert Berichte, Sie können eine Berichtsaufgabe direkt in`buildSrc/build.gradle.kts`:
## [source,kotlin]
plugins { `kotlin-jvm` }
Repositorys { mavenCentral() }
tasks.withType\<Test\> { useJUnitPlatform() reports.html.outputLocation.set(layout.buildDirectory.dir("reports/tests")) }
import com.yourpackage.ReportJbakeTestsTask // Importiere deine Taskklasse
) { // Der Pfad ist bereits in der Klasse definiert, er zeigt auf die BuildSrc-Berichte }
Sie können anschließend ausführen :`./gradlew :buildSrc:test`gefolgt von`./gradlew :buildSrc:reportBuildSrcTests`.
Wenn Sie diese Praktiken übernehmen, werden Sie Gradle‑Build‑Systeme in Kotlin DSL erstellen, die nicht nur leistungsstark sind, sondern auch außergewöhnlich modular, wartbar und leicht verständlich.