Der nächste große Schritt

Gradle 6: Dependency-Management reloaded

Jendrik Johannes

© rdonar/Shutterstock.com

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. 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 (Kasten: „Gradle Build Scans“).

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 testen oder direkt einen Build Scan betrachten, der während der Arbeit an diesem Artikel entstand. Im Kontext des Artikels ist besonders die Sicht auf die Dependencies des Projekts interessant.

 

Abb. 1: Beispielprojekt

Separieren von API- und Implementierungsabhängigkeiten

Arbeitet man an einem existierenden Projekt, gibt es irgendwann viele Abhängigkeiten auf den Klassenpfad des Projekts verfügbar. 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, eine Unterscheidung zwischen dem Laufzeit-Classpath (alle Abhängigkeiten) und dem Compile-Zeit-Classpath (zum Kompilieren nötige Abhängigkeiten) zu machen. Mit dem Separieren von API- und Implementierungsabhängigkeiten bietet Gradle die Möglichkeit, das genau zu kontrollieren.

// 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 a 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 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:commons-lang3") – zu deklarieren.

Wenn man eine implementation Dependency deklariert, bedeutet das, dass diese nötig ist, um den Code im eigenen Projekt zu kompilieren. Wenn man eine api Dependency nutzt, heißt das, dass diese auch von anderen Projekten benötigt wird, die gegen das aktuelle Projekt kompiliert werden.

In dem Beispiel ist, wie gesagt, Apache Commons Lang ein Implementierungsdetail (daher implementation). Allerdings nutzen wir Datenklassen aus unserem :data-Projekt als Teil des API der Services. Zum Beispiel: public print(Message message) { … } – wobei Message.java als Klasse im :data-Projekt definiert ist. Darum wird :data auch immer dann benötigt, wenn gegen die public Interfaces von :services programmiert wird. Daher definieren wir die Abhängigkeit zu :data als api und diese wird damit auch im :app-Projekt zur Compile-Zeit sichtbar.

Java Whitepaper

Gratis-Dossier: Java 2020 – State of the Art

GraalVM, Spring Boot 2.2, Kubernetes, Domain-driven Design & Machine Learning sind einige der Themen, die Sie in unserem brandneuen Dossier 2020 wiederfinden werden!

Um den Compile-Zeit-Classpath möglichst schmal zu halten, kann man auch die Build-Performance bei inkrementeller Kompilierung und bei gecachten Builds steigern. Gradle analysiert den Compile-Zeit-Classpath, und wenn er sich nicht verändert hat, werden vorhandene Ergebnisse wiederverwendet. Sind unnötige Module auf dem Compile-Zeit-Classpath, erhöht sich die Wahrscheinlichkeit, dass Gradle mehr Arbeit wiederholen muss.

Wie oben beschrieben, geht durch die Nutzung von implementation keine Information verloren. Alle Abhängigkeiten sind für Gradle trotzdem sichtbar, können aber, je nach Kontext, versteckt werden. Hier kommen die angesprochenen Varianten ins Spiel. Für ein Java-Modul kennt Gradle standardmäßig zwei Varianten, aus denen es je nach Kontext eine auswählt: Soll Code kompiliert werden? Dann werden die API-Varianten aller Module genutzt, die nur die API-Abhängigkeiten zeigen. Soll die Software ausgeführt werden? Dann werden die Laufzeitvarianten aller Module selektiert, die alle Abhängigkeiten sichtbar machen. Bei komplexen Anwendungsfällen können die Varianten auch weiter individuell angepasst werden. Zum Beispiel könnte eine Laufzeitvariante weitere Artefakte – wie native Bibliotheken – enthalten, die zur Kompilierungszeit nicht benötigt werden. Ein Java-Projekt bzw. -Modul kann beliebig viele weitere Varianten definieren. Auf Anwendungsfälle dafür kommen wir später noch einmal zu sprechen.

Weitere Informationen finden sich im Kapitel zum java-library-Plug-in des Gradle-Nutzerhandbuches.

Dependency Constraints und Plattformen

Ein weiterer wichtiger Aspekt, um die Übersicht über Abhängigkeiten zu wahren, ist die Verwaltung von Versionsnummern. In unserem Beispiel nutzt :services Version 3.7 von Apache Commons Lang. Wenn wir nun die gleiche Bibliothek in :app nutzen wollen, wäre es sinnvoll sicherzustellen, dass wir die gleiche Version verwenden. Anderenfalls bekommen wir zur Laufzeit, wenn alle Abhängigkeiten auf dem Klassenpfad sind, Probleme, da immer nur eine Version parallel genutzt werden kann und Gradle eine Version durch Konfliktauflösung auswählt.

Um diese Problematik anzugehen, bietet Gradle zwei zusammenhängende Features an: Dependency Constraints und Plattformen.

// Build-Datei: platform/build.gradle.kts
plugins {
  `java-platform`
}
dependencies {
  constraints {
    api("org.apache.commons:commons-lang3:3.7")
  }
}

Manchem Leser wird in Listing 1 bereits aufgefallen sein, dass die Abhängigkeit zu Apache Commons Lang keine Versionsnummer enthält. Nutzer älterer Gradle-Versionen werden Notationen mit Versionsnummer, z. B. org.apache.commons:commons-lang3:3.7, vertrauter sein. Der Grund, warum dieser Build trotzdem funktioniert, ist, dass die Version in einen Dependency Constraint ausgelagert wurde, der in dem :platform-Projekt definiert ist. Durch die Abhängigkeit auf dieses Projekt, api(platform(project(":platform"))), wird die dort definierte Version sichtbar.

In Listing 2 ist die Build-Datei des :platform-Projekts (platform/build.gradle.kts) abgebildet. Dort sehen wir, dass wir hier das java-platform-Plug-in nutzen. Das bedeutet, dass dieses Projekt keinerlei Implementierungscode enthält, sondern lediglich Dependency Constraints definiert.

Im unteren Teil der Build-Datei wird der constraints-Block innerhalb des dependencies-Blocks genutzt, um den org.apache.commons:commons-lang3:3.7 Constraint zu definieren. Ein Vorteil davon, Versionen durch Dependency Constraints zu definieren, ist, dass die Versionen an einer zentralen Stelle verwaltet werden können. In unserem Beispiel sammeln wir alle Constraints im :platform-Projekt und fügen all unseren Projekten eine Abhängigkeit zu :platform hinzu (Abb. 1).

Darüber hinaus können Constraints auch genutzt werden, um Versionen von transitiven Dependencies zu beeinflussen, von denen kein eigenes Projekt direkt abhängt. Dafür können Constraints auch in java-library-Projekten direkt definiert werden, wenn man kein platform-Projekt nutzen möchte.

Gradle bietet eine Vielzahl weiterer Möglichkeiten, die erlaubten Versionen einzuschränken, um die Ergebnisse bei Versionskonflikten zu kontrollieren (Kasten: „Rich Versions und Strict Versions“).

Außerdem können die von Maven bekannten BOMs (Bill of Material) als Plattform genutzt werden: implementation(platform("org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE"))

Weitere Informationen finden sich im Kapitel zum Thema Plattformen des Gradle-Nutzerhandbuches.

Rich Versions und Strict Versions

Deklariert man eine Version in der traditionellen Dependency-Notation oder als Dependency Constraint mit der gleichen Notation (z. B. org.apache.commons:commons-lang3:3.7), wird die Version (im Beispiel 3.7) von Gradle als sogenannte Required Version interpretiert. Das ist dann von Bedeutung, wenn es zu einem Versionskonflikt kommt, weil z. B. auch Version 3.9 von Apache Commons Lang durch eine transitive Abhängigkeit hinzugefügt wird. Required bedeutet, dass Gradle die Version anheben (aber in der Regel nicht herabsetzen) darf. Im Beispiel würde der Konflikt zwischen den beiden Required Versions 3.7 und 3.9 dann zu 3.9 aufgelöst.

Ob diese Auflösung am Ende zu einer funktionierenden Software führt, hängt von vielen Faktoren ab – nicht zuletzt der Versionierung der genutzten Bibliothek. Darum bietet Gradle nun viele Möglichkeiten, Versionen weiter einzuschränken. So kann man z. B. das Heraufsetzen auf eine bestimmte Version verbieten (reject), wenn bekannt ist, dass diese eine Sicherheitslücke enthält.
Die Notation dafür sind die sogenannten Rich Versions. In der Build-Datei öffnet man dazu einen Konfigurationsblock für Versionen, der dann neben require und reject noch andere Konstrukte bereitstellt:

api("org.apache.commons:commons-lang3") { 
  version { 
    require("3.7")
    reject("3.8")
  }
  because("If upgraded, don't use 3.8; patch in 3.8.1 is needed")
}

Mit Gradle 6 sind außerdem Strict Versions – strictly(„3.7“) – hinzugekommen, die ein Heraufsetzen der Version verbieten und auch transitive Versionen zu einem Herabsetzen zwingen können. Alle Details zu Rich und Strict Versions finden sich im Gradle-Nutzerhandbuch.

 

Module publizieren mit Gradle Module Metadata

Ein Nachteil früherer Gradle-Versionen ist, dass einige der in diesem Artikel vorgestellten Funktionalitäten – wie Dependency Constraints – nur noch teilweise funktionieren, wenn Projekte nicht direkt innerhalb eines Multiprojekts definiert sind, sondern in einem Repository als Module publiziert wurden. Das liegt daran, dass sämtliche Information, die zur korrekten Nutzung des Moduls benötigt werden, als Metadaten serialisiert und mit dem Modul abgelegt werden müssen. Das verbreitete und von Gradle genutzte Metadatenformat POM unterstützt aber nicht alle benötigten Konzepte. Daher führt Gradle 6 das Gradle-Module-Metadata-Format ein, das zusätzliche Informationen abbilden kann. Das macht es nun einfach, Module zu veröffentlichen und wiederzuverwenden, ohne dabei Funktionalität gegenüber lokal gebauten Projekten einzubüßen.

// Build-Datei: data/build.gradle.kts
plugins {
  `java-library`
  `maven-publish`
}
group = "org.gradle.hello6"
version = "0.1"

publishing {
  publications.create<MavenPublication>("maven") {
    from(components["java"])
  }
  repositories {
    maven { setUrl(rootProject.file("repo")) }
  }
}

An unserem Beispiel wollen wir zeigen, wie das Publizieren mit Gradle 6 funktioniert. Listing 3 zeigt die Build-Datei unseres :data-Projekts (data/build.gradle.kts), das die nötige Konfiguration enthält, um das :data-Projekt als Modul org.gradle.hello6:data:0.1 zu publizieren. Zusätzlich zum java-library-Plug-in verwendet man das maven-publish-Plug-in. Der Name rührt daher, dass die Benennung der publizierten Module den Struktur- und Namenskonventionen von Maven Repositories entsprechen. Die Module sind dadurch auch von anderen Tools konsumierbar.

Wenn man das maven-publish-Plug-in nutzt, ist es wichtig, Gruppe und Version des Moduls zu definieren, da sie für die GAV-Koordinaten im Repository genutzt werden. Im Beispiel wählen wir org.gradle.hello6 als Gruppe und 0.1 als Version. Außerdem muss man Gradle mitteilen, welchen Teil des Projekts man publizieren möchte, indem man eine MavenPublication erstellt; in unserem Beispiel publications.create<MavenPublication>("maven"). Im Falle einer Java Library genügt es, zu definieren, dass die Publikation auf Basis der Java-Komponente gefüllt werden soll: from(components["java"]). Die Java-Komponente ist eine Datenstruktur, die vom java-library-Plug-in automatisch erstellt und gefüllt wird und im Normalfall nicht weiter angepasst werden muss.

Zu guter Letzt wird noch konfiguriert, wohin publiziert werden soll. Im Beispiel wählen wir einen lokalen Ordner als Repository aus – maven { setUrl(rootProject.file("repo")) }. In einem realen Projekt-Set-up würde man hier den URL zu einem Artefakt-Repository, wie z. B. Artifactory, angeben.

Es ist ebenfalls möglich, ein java-platform-Projekt zu publizieren. In diesem Fall lässt sich die Publikation auch als BOM in Maven verwenden.

Weitere Informationen zum Konfigurieren einer Publikation (z. B. zum Hinzufügen von Signaturen) finden sich im entsprechenden Kapitel des Gradle-Nutzerhandbuchs.

org/gradle6/data/0.1/
├── data-0.1.pom
├── data-0.1.module
├── data-0.1.jar
├── ...

Wird das maven-publish-Plug-in genutzt, kann das Publizieren mit dem publish-Task durchgeführt werden. Im Beispiel können wir ./gradlew :data:publish aufrufen. In Listing 4 sehen wir das Resultat im Ordner repo (nicht gezeigt sind die MD5, SHA1, SHA256 und SHA512 Checksums, die für jede Datei mitpubliziert werden). Dort ist zu sehen, dass neben der bekannten data-0.1.pom-Datei nun auch eine data-0.1.module-Datei erzeugt wurde, die zusätzliche Metadaten enthält.

Die Anwendungsfälle, die durch die zusätzlichen Metadaten adressiert werden können, sind vielseitig. In dem GitHub-Repository sind einige Beispiele zu finden, wie existierende Open-Source-Bibliotheken von der Nutzung zusätzlicher Metadaten profitieren können. Die Metadaten ermöglichen die Nutzung aller in diesem Artikel vorgestellten Features über Modulgrenzen hinweg, zum Beispiel in einer Microservices-Architektur, in der Teams unabhängig Komponenten entwickeln, versionieren und publizieren. Die Dokumentation des Metadatenformats ist online einzusehen.

Wiederverwendung von Testcode mit Test-Fixtures

Ein Feature, das die Möglichkeit nutzt, einem Java-Projekt weitere Varianten hinzuzufügen, sind Test-Fixtures. Unter einer Test-Fixture versteht man Code, der benutzt wird, um Tests zuverlässig ausführen zu können. Dabei kann es sich zum Beispiel um Mock-Implementierungen, Testumgebungs-Set-up-Code oder einfach um Testdaten bzw. Testdatengeneratoren handeln. Test-Fixtures können in der Regel mit Teilen des zu testenden Codes assoziiert und dann an verschiedenen Stellen wiederverwendet werden.

// Build-Datei: data/build.gradle.kts
plugins {
  `java-library`
  `java-test-fixtures`
  `maven-publish`
}
...

In unserem Beispiel können wir uns einen Testdatengenerator vorstellen, der Instanzen der Message.java-Klasse zu Testzwecken erzeugt. Wenn wir nun im :data-Projekt das java-test-fixtures-Plug-in nutzen (Listing 5), können wir so einen Testdatengenerator als Teil des :data-Projekts schreiben und anderen Projekten, die Datenklassen aus dem Projekt nutzen, zum Testen zur Verfügung stellen. Dafür können wir jetzt Code im neuen Source-Ordner src/testFixtures/java ablegen.

In unserem :services-Projekt können wir diese Test-Fixtures dann nutzen, indem wir folgende Abhängigkeit deklarieren: testImplementation(testFixtures(project(":data"))). Durch die Nutzung des Konstrukts testFixture(…) teilen wir Gradle mit, dass wir nicht an einer der Hauptvarianten des Projekts :data interessiert sind, sondern an einer der Test-Fixture-Varianten. Diese bietet das :data-Projekt durch die Nutzung des java-test-fixtures-Plug-ins jetzt automatisch an.

Auch wenn wir das publizierte Modul org.gradle.hello6:data:0.1 nutzen, stehen die Test-Fixtures, dank der erweiterten Metadaten, zur Verfügung –testImplementation(testFixtures("org.gradle.hello6:data:0.1")). Schauen wir uns den Inhalt des Repositories mit der Nutzung des java-test-fixtures Plug-ins an, stellen wir fest, dass die data-0.1.module-Datei nun auch Test-Fixture-Varianten deklariert und eine data-0.1-test-fixtures.jar im Repository zu finden ist.

Weitere Informationen zu den Themen Testen und Test-Fixtures finden sich im Gradle-Nutzerhandbuch.

Projekte in Features aufteilen

Eine weitere Funktionalität des java-library-Plugins, die die Möglichkeit zusätzlicher Varianten nutzt, ist die Deklaration von Featurevarianten. Eine Featurevariante erlaubt es, ein Projekt in mehrere Features aufzuteilen, die dann einzeln selektiert werden können. Das funktioniert ähnlich wie bei Test-Fixtures.

// Build-Datei: services/build.gradle.kts
...
val loud: SourceSet by sourceSets.creating
java {
  registerFeature("loud") {
    usingSourceSet(loud)
  }
}

In dem Projekt, das ein Feature implementiert, müssen wir zunächst das Feature registrieren. Das wird in Listing 6 für das :services-Projekt gezeigt. In diesem Fall fügen wir ein weiteres Feature hinzu, das wir loud nennen (weil es einen Service implementieren soll, der eine Nachricht besonders „laut“ macht).

Ähnlich wie bei Test-Fixtures stellt uns Gradle nach der Registrierung des Features einen neuen Ordner für Source Code zur Verfügung, in diesem Fall src/loud/java. Code, den wir in diesem Ordner ablegen, wird nun unabhängig vom Hauptfeature (Code in src/main/java) kompiliert. Das Feature kann eigene Abhängigkeiten definieren, für die wir nun die Keywords loudImplementation und loudApi nutzen können.

// Build-Datei: app/build.gradle.kts
...
dependencies {
  // Abhängigkeit zum Hauptfeature von :services
  implementation(project(“:services"))
  // Abhängigkeit zum Loud-Feature von :services
  implementation(project(":services")) {
    capabilities { 
      requireCapability(“org.gradle.hello6:services-loud")
    }
  }
}

In unserem :app-Projekt, das eine Abhängigkeit zm :services-Projekt definiert (Abb. 1), ist das zusätzliche Feature nun erst einmal nicht sichtbar, da es individuell selektiert werden muss. Erst wenn wir, wie in Listing 7 gezeigt, eine zweite Abhängigkeit zu :services einfügen, die die org.gradle.hello6:services-loud Capability anfordert, steht das zusätzliche Feature bereit. Das Konzept der Capability weist einer einzelnen Variante Koordinaten zu, die dann genutzt werden können, um sie direkt zu adressieren. Im Fall von Features wird diese Koordinate automatisch aus Projektkoordinaten und Featuremame abgeleitet (im Beispiel org.gradle.hello6:services-loud).

Weitere Informationen zu den Themen Featurevarianten und Capabilities finden sich im Gradle-Nutzerhandbuch.

Fehlende Information mit Metadata-Regeln ergänzen

Die in diesem Artikel vorgestellten Funktionalitäten bilden nur einen Bruchteil der Anwendungsfälle ab, die mit dem Variantenkonzept und Gradle Module Metadata adressiert werden können. Nicht unerwähnt lassen wollen wir aber die Möglichkeit, Metadata-Regeln (Component Metadata Rules) im eigenen Build oder Gradle-Plug-in zu definieren. Diese Regeln ermöglichen es, Informationen zu Metadaten hinzuzufügen, die bereits publiziert sind. Das ist aktuell besonders interessant für Open-Source-Bibliotheken, die (noch) ohne Gradle Module Metadata publiziert wurden. Ein prominentes Beispiel ist die bekannte Utility Library Guava, die jede Version in zwei Varianten (eine für Java 6 und eine für Java 8) publiziert.

Regeln für dieses und andere Beispiele finden sich im entsprechenden Kapitel im Gradle-Nutzerhandbuch.

Fazit

Gradle 6 bietet reichhaltige Dependency-Management-Features, die es erlauben, eine Vielzahl von Anwendungsszenarien zu modellieren. Durch die Nutzung von Gradle Module Metadata können die Features über Modulgrenzen hinweg genutzt werden. Dieser Artikel hat gezeigt, wie einfach die prominentesten Features in ihrer Standardkonfiguration genutzt werden können. Darüber hinaus bietet Gradle 6 viele Möglichkeiten, die Funktionalitäten individuell zu konfigurieren oder eigene Build-Tool-Erweiterungen als Plug-ins hinzuzufügen, indem man auf den variantenbasierten Dependency-Management-APIs aufsetzt. Mehr Informationen dazu sind im Gradle-Nutzerhandbuch und auf dem Gradle-Blog zu finden. Außerdem gibt es online eine Übersicht über alle weiteren Neuerungen in Gradle 6.

Geschrieben von
Jendrik Johannes
Jendrik Johannes
Jendrik Johannes arbeitet als Entwickler bei Gradle direkt an der Weiterentwicklung des Open-Source Tools mit. Momentan liegt sein Fokus dabei auf Dependency-Management und dem JVM-Sprachsupport. Twitter: @jeoj
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: