© rdonar/Shutterstock.com
Gradle 6: Dependency-Management reloaded

Der nächste große Schritt


Das kürzlich erschienene Release 6 des modernen Build-Tools bringt eine Menge neuer Features, die es erlauben, Java-, Groovy-, Kotlin- und Scala-Projekte besser zu strukturieren und zu modularisieren. Ein Highlight ist dabei die Einführung des Gradle-Module-Metadata-Formats, um Module mit reichhaltigen Metadaten zu publizieren und wiederzuverwenden.

Gradle wurde über die letzten Jahre um immer mehr Features erweitert, die es ermöglichen, Projekte besser zu strukturieren und einzelne Teile besser zu isolieren. Der nächste große Schritt wurde nun mit Gradle 6 getan, das neue Funktionalitäten zur Verwaltung von Abhängigkeiten bereitstellt, die über Projekt- und Modulgrenzen hinweg genutzt werden können. Ein zentrales Konzept dabei ist, dass jedes Softwaremodul mehrere Varianten bereitstellt, aus denen je nach Kontext ausgewählt wird.

In diesem Artikel stellen wir die interessantesten Features anhand eines Beispielprojekts vor, das zum Selbsterkunden auf GitHub bereitsteht [1]. Es handelt sich dabei um ein Gradle-Multiprojekt, in dem mehrere Projekte gemeinsam in einem Build definiert werden. Jedes dieser Projekte kann Abhängigkeiten zu anderen Projekten und zu publizierten Modulen deklarieren (in diesem Artikel verwenden wir den Begriff „Projekt“ für lokale Projekte, die Teil des Builds sind, und den Begriff „Modul“ für publizierte Bibliotheken).

Um die Strukturierung in mehrere Projekte zu demonstrieren, definieren wir drei Java-Projekte: :app, :services und :data (Abb. 1). Als Beispiel für ein Modul verwenden wir Apache Commons Lang. Das Multiprojekt kann auch in einem Gradle Build Scan betrachtet werden [2] (Kasten: „Gradle Build Scans“).

johannes_gradle_1.tif_fmt1.jpgAbb. 1: Beispielprojekt

Gradle Build Scans

Startet man einen Gradle Build mit dem Parameter --scan, werden diverse Daten über den Build gesammelt und in einem Build Scan aufbereitet zur Verfügung gestellt. Der Build Scan ist über einen geheimen Link auf scans.gradle.com einzusehen und erlaubt es, den Build zu analysieren, beispielsweise auf Performanceprobleme.

Sie können das mit dem Beispielprojekt [1] testen oder direkt einen Build Scan betrachten, der während der Arbeit an diesem Artikel entstand [2]. Im Kontext des Artikels ist besonders die Sicht auf die Dependencies des Projekts interessant.

Separieren von API- und Implementierungsabhängigkeiten

Arbeitet man an einem existierenden Projekt, gibt es irgendwann viele Abhängigkeiten auf den Klassenpfad des Projekts. Das ist der Tatsache geschuldet, dass alle transitiven Abhängigkeiten benötigt werden, um eine lauffähige Software zu erstellen. Im Falle unseres Beispiels (Abb. 1) definiert :services die Abhängigkeit zu Apache Commons Lang. Diese ist dann transitiv auch im :app-Projekt verfügbar, obwohl das :app-Projekt sie gar nicht direkt benötigt (keine Klassen aus Apache Commons Lang werden dort referenziert). Das führt in großen Projekten zu einem unnötig unübersichtlichen Dschungel von Abhängigkeiten, was mit der Zeit viele Probleme mit sich bringen kann. Beispielsweise werden oft transitive Abhängigkeiten aus Versehen benutzt und nicht direkt deklariert. Wollen wir z. B. Apache Commons Lang in der Implementierung von :app verwenden, dann sollten wir dort direkt die entsprechende Abhängigkeit definieren. Andernfalls würde ein späteres Entfernen der Abhängigkeit aus dem :service-Projekt dazu führen, dass das :app-Projekt bricht. Obwohl es sich bei Apache Commons Lang nur um ein Implementierungsdetail von :services handelt, das intern (nicht in public Interfaces) genutzt wird und andere Projekte nicht beeinflussen sollte.

Daher ist es sinnvoll zwischen dem Laufzeit-Classpath (alle Abhängigkeiten) und dem Compile-Zeit-Classpath (zum Kompilieren nötige Abhängigkeiten) zu unterscheiden. Mit dem Separieren von API- und Implementierungsabhängigkeiten bietet Gradle die Möglichkeit, das genau zu kontrollieren.

Listing 1

// Build-Datei: services/build.gradle.kts plugins { 'java-library' } dependencies { api(project(":data")) implementation("org.apache.commons:commons-lang3") api(platform(project(":platform"))) }

In Listing 1 sehen wir die Build-Datei des :services-Projekts (services/build.gradle.kts). Der Build ist in der Kotlin DSL von Gradle geschrieben (Kasten: „Kotlin DSL oder Groovy DSL“). Die erste wichtige Neuerung ist die Nutzung des java-library-Plug-ins. Es stellt die Funktionalität für API-Abhängigkeiten zur Verfügung und kann mit anderen Plug-ins (groovy, scala, kotlin) kombiniert werden.

Kotlin DSL oder Groovy DSL

In Gradle 5 wurde die Kotlin DSL als Alternative zur bekannten Groovy DSL als Syntax für Gradle-Build-Dateien eingeführt. Wie der Name schon sagt, basiert die Syntax auf Kotlin und ist daher, im Vergleich zur Groovy DSL, statisch typisiert. Der wesentliche Vorteil ist, dass die IDE (IntelliJ oder Eclipse) mehr Kontextinformationen hat und beim Erstellen der Build-Dateien den Autor stärker unterstützt (z. B. durch zeitnahes Fehlerreporting und Code Completion). Wenn man keine Sprachpräferenz hat, ist es daher empfehlenswert, die Kotlin DSL zu nutzen.

Ansonsten bieten beide DSLs die gleichen Features, und das Modell des Projekts, das durch die Build-Dateien aufgebaut wird, ist unabhängig von der genutzten DSL. Daher kann man am Ende frei wählen und auch auf Groovy zurückgreifen, wenn man z. B. mit Groovy vertrauter ist oder auch andere Teile des Projekts in Groovy entwickelt werden. Auch die Kombination beider Sprachen (eine Build-Datei in Groovy DSL, eine andere in Kotlin DSL) im gleichen Projekt ist problemlos möglich.

Welche DSL genutzt wird, ist am Namen der Build-Datei zu erkennen: build.gradle (Groovy DSL) oder build.gradle.kts (Kotlin DSL).

Das Beispiel in diesem Artikel nutzt die Kotlin DSL. Die Groovy-Syntax ist jedoch oft ähnlich oder sogar identisch. Im Zweifelsfall kann das entsprechende Thema im Gradle-Nutzerhandbuch [3] nachgeschlagen werden, das alle Beispiele in beiden Sprachen enthält.

Wenn wir den dependencies-Block in Listing 1 betrachten, können wir sehen, dass unterschiedliche Keywords – api und implementation – genutzt werden, um Abhängigkeiten zu anderen Projekten – api(project(":data")) – oder Modulen – implementation("org.apache.commons:co...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang