Versionsmanagement in Gradle-Projekten entwickelte sich über Jahre von einfachen String-Literalen zu ausgefeilten Systemen. Frühe Ansätze verwendeten Properties oder Extra-Properties für Versionsnummern. Diese Methoden führten zu Problemen: keine IDE-Unterstützung, fehlerhafte String-Referenzen und schwierige Wartung bei hunderten Dependencies. Multi-Modul-Projekte litten besonders unter inkonsistenten Versionen zwischen Modulen.
Version Catalogs wurden in Gradle 7.0 als experimentelles Feature eingeführt und sind seit Gradle 7.4 stabil. Sie bieten eine typsichere, zentrale Definition von Dependencies und Versionen. Die IDE erkennt Catalog-Einträge, bietet Autovervollständigung und validiert Referenzen zur Compile-Zeit. Dies eliminiert die häufigsten Fehlerquellen im Dependency Management.
Der fundamentale Vorteil liegt in der Trennung von Dependency-Koordinaten und ihrer Verwendung. Ein Catalog definiert verfügbare Libraries mit ihren Versionen zentral. Build-Skripte referenzieren diese Libraries über typsichere Accessors. Updates erfolgen an einer Stelle und wirken sich automatisch auf alle Konsumenten aus. Diese Indirektion vereinfacht Versions-Upgrades erheblich, besonders in großen Projekten.
Version Catalogs werden in TOML-Format definiert, typischerweise in
der Datei gradle/libs.versions.toml. Die TOML-Syntax ist
menschenlesbar und strukturiert Informationen hierarchisch. Ein Catalog
besteht aus vier Hauptsektionen: versions, libraries, bundles und
plugins.
Die Grundstruktur eines Version Catalogs:
[versions]
spring-boot = "3.1.5"
junit = "5.10.0"
testcontainers = "1.19.1"
[libraries]
spring-boot-starter = { group = "org.springframework.boot", name = "spring-boot-starter", version.ref = "spring-boot" }
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "spring-boot" }
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }
testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
[bundles]
spring-web = ["spring-boot-starter", "spring-boot-starter-web"]
testcontainers = ["testcontainers-core", "testcontainers-postgresql"]
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }Die versions Sektion definiert Versionsnummern als
benannte Konstanten. Diese können in anderen Sektionen über
version.ref referenziert werden. Die Zentralisierung
ermöglicht koordinierte Updates zusammengehöriger Libraries.
Die libraries Sektion deklariert konkrete Dependencies.
Jede Library erhält einen Alias, der in Build-Skripten verwendet wird.
Die Notation unterstützt sowohl die ausführliche Form mit separaten
group und name Attributen als auch die
kompakte module Notation für Group:Name Kombinationen.
Bundles gruppieren häufig gemeinsam verwendete Libraries. Statt mehrere Dependencies einzeln zu deklarieren, fügt ein Bundle alle enthaltenen Libraries mit einer Zeile hinzu. Dies reduziert Boilerplate und stellt sicher, dass zusammengehörige Dependencies konsistent eingebunden werden.
Nach Definition des Catalogs generiert Gradle typsichere Accessors.
Diese werden im Build-Skript über das libs Extension-Objekt
referenziert:
plugins {
java
alias(libs.plugins.spring.boot)
}
dependencies {
implementation(libs.spring.boot.starter)
implementation(libs.spring.boot.starter.web)
// oder als Bundle
implementation(libs.bundles.spring.web)
testImplementation(libs.junit.jupiter)
testImplementation(libs.bundles.testcontainers)
}Die Accessor-Namen folgen einer Konvention: Bindestriche werden zu
Punkten, Unterstriche bleiben erhalten. Der Catalog-Eintrag
spring-boot-starter wird zu
libs.spring.boot.starter. Diese Transformation ist
deterministisch und vorhersagbar.
Versionskonflikte zwischen Catalog und direkten Deklarationen werden zugunsten expliziter Versionen aufgelöst. Eine direkte Version überschreibt immer die Catalog-Version:
dependencies {
implementation(libs.spring.boot.starter) {
version {
strictly("3.0.0") // Überschreibt Catalog-Version
}
}
}Rich Version Constraints funktionieren mit Catalog-Einträgen. Required, preferred und rejected Versions können in der TOML-Datei definiert werden:
[libraries]
guava = { module = "com.google.guava:guava", version = { require = "[31.0,32.0)", prefer = "31.1-jre" } }Die Migration zu Version Catalogs erfolgt systematisch in mehreren Phasen. Die erste Phase ist die Inventarisierung aller Dependencies über alle Module. Ein Gradle-Task kann diese Information sammeln:
tasks.register("analyzeDependencies") {
doLast {
configurations
.filter { it.isCanBeDeclared }
.forEach { config ->
config.dependencies.forEach { dep ->
println("${dep.group}:${dep.name}:${dep.version}")
}
}
}
}Die zweite Phase erstellt den initialen Catalog. Duplicate Versionen werden identifiziert und konsolidiert. Dependencies derselben Library-Familie teilen sich eine Version-Referenz. Die Namensgebung der Aliases folgt einer konsistenten Konvention, typischerweise der Artifact-ID mit Präfix für Namespaces.
Die dritte Phase ist die schrittweise Umstellung der Build-Skripte. Module werden einzeln migriert, um Fehler zu isolieren. Ein typischer Migrationsprozess für ein Modul:
// Vorher
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.1.5")
implementation("org.slf4j:slf4j-api:2.0.9")
}
// Nachher
dependencies {
implementation(libs.spring.boot.starter)
implementation(libs.slf4j.api)
}Die Validierung erfolgt durch Vergleich der Dependency-Trees vor und
nach Migration. Der Befehl
gradle dependencies > before.txt vor der Migration und
gradle dependencies > after.txt danach ermöglicht
Diff-Analyse. Abweichungen deuten auf Fehler in der Migration hin.
Continuous Integration muss angepasst werden. Der Catalog wird Teil des Repository und muss in Version Control eingecheckt werden. Build-Caches müssen möglicherweise invalidiert werden, da sich die Configuration-Cache-Keys ändern. Dependency-Update-Bots wie Renovate oder Dependabot benötigen Konfiguration für TOML-Support.
Hier eine überarbeitete Fassung deines Abschnitts, stärker erklärend und praxisnah formuliert, aber mit allen wesentlichen Details:
Version Catalogs sind nicht auf eine einzelne
libs.versions.toml beschränkt. In größeren Projekten oder
Organisationen nutzt man verschiedene Muster, um Dependencies modular
und wiederverwendbar zu verwalten.
Platform-Integration Ein Catalog kann auch Plattformen wie BOMs (Bill of Materials) aufnehmen. Damit definiert man zentrale Version Constraints für ganze Dependency-Familien. Ein typisches Beispiel ist das Spring Boot BOM:
[libraries]
spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }Im Build-Skript bindet man es dann ein und alle abhängigen Libraries orientieren sich an den darin gesetzten Versionen:
dependencies {
implementation(platform(libs.spring.boot.bom))
}Mehrere Catalogs im selben Projekt Man kann Catalogs
nach Aufgabenbereichen oder Teams trennen. Beispielsweise einen
allgemeinen Katalog für Produktionsabhängigkeiten
(libs.versions.toml) und einen separaten für Tests
(test-libs.versions.toml):
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
create("testLibs") {
from(files("gradle/test-libs.versions.toml"))
}
}
}Die Build-Skripte greifen dann gezielt auf libs oder
testLibs zu.
Geteilte und vererbte Catalogs In großen Organisationen lohnt es sich, einen Basiskatalog als eigenständiges Artefakt zu veröffentlichen. Projekte können diesen importieren und erhalten damit automatisch alle freigegebenen Versionen. Lokale Anpassungen oder Ergänzungen sind weiterhin möglich:
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from("com.company:base-catalog:1.0.0")
// Lokale Ergänzung oder Überschreibung
library("custom-lib", "com.company", "custom").version("2.0.0")
}
}
}Auf diese Weise werden Versionen und Abhängigkeiten wie normale Bibliotheken behandelt: modular, versioniert und für viele Projekte wiederverwendbar.
Ein kompaktes Setup mit zwei Subprojekten (app und
lib) sowie getrenntem Test-Catalog:
settings.gradle.kts
rootProject.name = "catalog-multi"
include("app", "lib")
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
create("testLibs") {
from(files("gradle/test-libs.versions.toml"))
}
}
}gradle/libs.versions.toml
[versions]
slf4j = "2.0.9"
[libraries]
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }gradle/test-libs.versions.toml
[versions]
junit = "5.10.1"
[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }app/build.gradle.kts
plugins { java }
dependencies {
implementation(libs.slf4j.api)
testImplementation(testLibs.junit.jupiter)
}lib/build.gradle.kts
plugins { java }
dependencies {
implementation(libs.slf4j.api)
testImplementation(testLibs.junit.jupiter)
}Smoke-Test in der Konsole
./gradlew :app:dependencies --configuration testCompileClasspath
./gradlew :lib:dependencies --configuration testCompileClasspathBeide Module zeigen im Dependency-Tree dieselben Versionen aus den
Catalogs an – einmal aus libs, einmal aus
testLibs.
Die Organisation großer Catalogs erfordert Struktur. Alphabetische Sortierung innerhalb der Sektionen verbessert die Lesbarkeit. Kommentare dokumentieren spezielle Versionsanforderungen oder Known Issues. Verwandte Libraries gruppieren sich durch gemeinsame Präfixe:
# Database
database-postgresql = { module = "org.postgresql:postgresql", version = "42.6.0" }
database-hikari = { module = "com.zaxxer:HikariCP", version = "5.0.1" }
database-flyway = { module = "org.flywaydb:flyway-core", version = "9.22.0" }
# Testing
test-junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
test-mockito = { module = "org.mockito:mockito-core", version = "5.5.0" }
test-assertj = { module = "org.assertj:assertj-core", version = "3.24.2" }Version-Update-Strategien nutzen Catalog-Features optimal. Punkt-Releases derselben Minor-Version können durch Range-Notation automatisch verwendet werden. Major-Updates erfordern explizite Änderung und Testing. Die Version-Sektion dokumentiert Update-Policies:
[versions]
# Automatic patch updates within 3.1.x
spring-boot = "3.1.+"
# Locked version due to breaking changes in 2.0
commons-lang = "1.9.4"
# Beta version for testing
experimental-lib = "2.0.0-beta.3"Catalog-Validation verhindert fehlerhafte Konfigurationen. Gradle validiert TOML-Syntax und referentielle Integrität beim Parsing. Custom-Validation-Tasks prüfen zusätzliche Regeln:
tasks.register("validateCatalog") {
doLast {
val catalogFile = file("gradle/libs.versions.toml")
val lines = catalogFile.readLines()
// Prüfe auf direkte Versionen statt version.ref
lines.forEach { line ->
if (line.contains("version =") && line.contains("\"")) {
if (!line.contains("version.ref")) {
throw GradleException("Direct version found: $line")
}
}
}
}
}Die Integration in den Entwicklungsprozess standardisiert Catalog-Änderungen. Pull Requests für Version-Updates folgen einem Template. Breaking Changes werden in Commit-Messages markiert. Automated Tests validieren, dass alle Module nach Catalog-Updates kompilieren. Review-Prozesse stellen sicher, dass Version-Updates koordiniert erfolgen.
Version Catalogs bieten eine zentrale, typsichere Verwaltung von Dependencies und Versionen. Dieses Beispiel demonstriert die wichtigsten Features anhand eines einfachen Java-Projekts.
# Projekt-Verzeichnis erstellen
mkdir version-catalog-demo
cd version-catalog-demo
# Gradle-Projekt initialisieren
gradle init --type basic --dsl kotlin --project-name catalog-demo
# Verzeichnisstruktur anlegen
mkdir -p gradle
mkdir -p src/main/java/demo
mkdir -p src/test/java/demoDer Version Catalog wird in einer TOML-Datei definiert. Diese liegt
standardmäßig unter gradle/libs.versions.toml.
gradle/libs.versions.toml:
[versions]
# Versionsnummern zentral definiert
junit = "5.10.1"
slf4j = "2.0.9"
commons-lang = "3.14.0"
jackson = "2.16.0"
[libraries]
# Commons Libraries
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" }
commons-io = { module = "commons-io:commons-io", version = "2.15.1" }
# Logging
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
# JSON Processing
jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
# Testing
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "1.10.1" }
assertj = { module = "org.assertj:assertj-core", version = "3.24.2" }
[bundles]
# Gruppierung zusammengehöriger Libraries
logging = ["slf4j-api", "slf4j-simple"]
jackson = ["jackson-core", "jackson-databind"]
testing = ["junit-jupiter", "assertj"]
[plugins]
# Plugin-Definitionen
# versions plugin entfernt - nicht kompatibel mit Configuration Cachebuild.gradle.kts:
plugins {
java
application
}
// Java-Version festlegen
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// Main-Klasse für application plugin
application {
mainClass.set("demo.Main")
}
repositories {
mavenCentral()
}
dependencies {
// Einzelne Libraries aus dem Catalog
implementation(libs.commons.lang3)
implementation(libs.commons.io)
// Bundle verwenden (mehrere Libraries auf einmal)
implementation(libs.bundles.logging)
implementation(libs.bundles.jackson)
// Test-Bundle
testImplementation(libs.bundles.testing)
testRuntimeOnly(libs.junit.platform.launcher)
}
// JUnit 5 aktivieren
tasks.withType<Test> {
useJUnitPlatform()
}
// Task zum Anzeigen der Dependencies
tasks.register("showCatalogDeps") {
inputs.files(configurations.runtimeClasspath)
doLast {
println("\n📚 Dependencies aus dem Version Catalog:")
inputs.files.files.forEach { file ->
when {
file.name.contains("commons-lang") ->
println(" Commons Lang: ${file.name}")
file.name.contains("slf4j") ->
println(" SLF4J: ${file.name}")
file.name.contains("jackson") ->
println(" Jackson: ${file.name}")
}
}
}
}src/main/java/demo/Main.java:
package demo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
private static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws Exception {
// Commons Lang verwenden
String text = "version catalog demo";
String capitalized = StringUtils.capitalize(text);
// Logging
logger.info("Original: {}", text);
logger.info("Capitalized: {}", capitalized);
// Jackson für JSON
Map<String, String> data = Map.of(
"feature", "Version Catalogs",
"benefit", "Zentrale Versionsverwaltung"
);
String json = mapper.writeValueAsString(data);
logger.info("JSON: {}", json);
System.out.println("\n✅ Version Catalog Demo erfolgreich!");
System.out.println("Verwendete Features:");
System.out.println("- Commons Lang für String-Operationen");
System.out.println("- SLF4J für Logging");
System.out.println("- Jackson für JSON-Verarbeitung");
}
}src/test/java/demo/MainTest.java:
package demo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.assertj.core.api.Assertions.*;
class MainTest {
@Test
@DisplayName("Version Catalog Dependencies sind verfügbar")
void testDependenciesAvailable() {
// Test ob Commons Lang verfügbar ist
String result = org.apache.commons.lang3.StringUtils.capitalize("test");
assertThat(result).isEqualTo("Test");
// Test ob Jackson verfügbar ist
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
assertThat(mapper).isNotNull();
}
@Test
@DisplayName("Logging funktioniert")
void testLogging() {
org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(MainTest.class);
assertThat(logger).isNotNull();
logger.info("Test-Logging über Version Catalog");
}
}# Dependencies anzeigen
./gradlew showCatalogDeps
# Tests ausführen
./gradlew test
# Anwendung starten
./gradlew run# Kompletter Dependency-Baum
./gradlew dependencies
# Nur Runtime-Dependencies
./gradlew dependencies --configuration runtimeClasspath
# Nach bestimmten Dependencies suchen
./gradlew dependencies | grep jacksonUm alle Jackson-Libraries zu aktualisieren, wird nur eine Zeile geändert:
gradle/libs.versions.toml:
[versions]
jackson = "2.16.1" # Vorher: 2.16.0Diese Änderung wirkt sich automatisch auf alle Libraries aus, die
version.ref = "jackson" verwenden.
libs.versions.toml definieren:[libraries]
guava = { module = "com.google.guava:guava", version = "32.1.3-jre" }dependencies {
implementation(libs.guava)
}Ein bestehendes Bundle kann einfach erweitert werden:
[bundles]
# Vorher
testing = ["junit-jupiter", "assertj"]
# Nachher - mit Mockito
testing = ["junit-jupiter", "assertj", "mockito"][libraries]
# Gruppierung nach Funktion mit Präfixen
database-postgresql = { ... }
database-hikari = { ... }
web-spring-core = { ... }
web-spring-mvc = { ... }
test-junit = { ... }
test-mockito = { ... }[versions]
spring = "6.1.2"
[libraries]
# Alle Spring-Libraries nutzen dieselbe Version
spring-core = { group = "org.springframework", name = "spring-core", version.ref = "spring" }
spring-context = { group = "org.springframework", name = "spring-context", version.ref = "spring" }
spring-web = { group = "org.springframework", name = "spring-web", version.ref = "spring" }[versions]
# Locked wegen Breaking Changes in 2.0
legacy-lib = "1.9.4"
# Beta-Version zum Testen
experimental = "2.0.0-RC1"Bestandsaufnahme: Alle Dependencies auflisten
./gradlew dependencies > current-deps.txtCatalog erstellen: Häufig verwendete Dependencies zuerst
[versions]
# Start mit den wichtigsten Versionen
[libraries]
# Kernabhängigkeiten definierenSchrittweise umstellen: Modul für Modul
// Alt
implementation("org.slf4j:slf4j-api:2.0.9")
// Neu
implementation(libs.slf4j.api)Validierung: Dependencies vergleichen
./gradlew dependencies > new-deps.txt
diff current-deps.txt new-deps.txtVersion Catalogs bieten entscheidende Vorteile:
Das Beispiel zeigt alle wichtigen Features in einem praktischen Kontext und kann als Ausgangspunkt für eigene Projekte dienen.
Für den Abschnitt zu Version Catalogs passt eine anschließende Aufgabe, die über die reine Einführung hinausgeht. Sie sollte die Teilnehmer dazu bringen, mit Bundles, mehreren Catalogs und Versionsänderungen praktisch zu arbeiten.
Vorschlag für die Aufgabe:
Legen Sie neben libs.versions.toml eine zweite Datei
gradle/test-libs.versions.toml an.
testing, das beide Libraries
enthält.Ergänzen Sie in settings.gradle.kts die Einbindung
des neuen Catalogs, z. B. mit create("testLibs").
Verwenden Sie in Ihrem Build-Skript
(build.gradle.kts) die neuen Einträge, sodass Tests
ausschließlich aus dem testLibs-Catalog versorgt
werden.
Fügen Sie anschließend eine neue Library (z. B. Mockito) in den
Test-Catalog ein und erweitern Sie das testing-Bundle
entsprechend.
Überprüfen Sie mit
./gradlew dependencies --configuration testCompileClasspath,
ob alle drei Libraries im Dependency-Baum erscheinen.