Java Maven-Abhängigkeiten
Java-Abhängigkeiten in Maven deklarieren und verwalten mit dependency, scope und transitiver Auflösung.
Echte Java-Projekte stehen selten für sich allein. Sie ziehen Logging-Frameworks, HTTP-Clients, JSON-Parser und Testbibliotheken von anderen Entwicklern heran. Maven übernimmt die Aufgabe, diese Bibliotheken abzurufen, anschließend deren Bibliotheken zu holen und einen einzigen konsistenten Classpath zusammenzustellen – ohne dass du auch nur eine einzige JAR-Datei von Hand verwalten musst. Zu verstehen, wie das funktioniert, entscheidet darüber, ob ein Build einfach funktioniert oder du einen Nachmittag mit einem NoSuchMethodError verlierst.
Dieses Kapitel erklärt, wie eine Abhängigkeit benannt wird (Koordinaten), wie Scopes steuern, wo jede Bibliothek sichtbar ist, wie Maven den transitiven Abhängigkeitsgraphen durchläuft und wie Versionskonflikte aufgelöst werden. Es wird vorausgesetzt, dass du bereits eine pom.xml hast. Falls nicht, beginne mit dem Kapitel Maven POM.
Koordinaten: Wie eine Abhängigkeit benannt wird
Jedes Artefakt in der Maven-Welt wird durch eine Menge von Koordinaten identifiziert. Die drei, die du immer angibst, sind die groupId (wer es veröffentlicht, üblicherweise eine umgekehrte Domain), die artifactId (der Name des Projekts) und die version. Zusammen zeigen sie auf genau eine JAR-Datei in einem Repository.
Du deklarierst eine Abhängigkeit innerhalb des <dependencies>-Blocks deiner pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>Die Kurzform groupId:artifactId:version wird als GAV-String bezeichnet und begegnet dir überall: in Fehlermeldungen, im Abhängigkeitsbaum und auf den Webseiten des zentralen Repositorys. Eine vierte Koordinate, der type (standardmäßig jar), und eine fünfte, der classifier (für Varianten wie sources oder javadoc), vervollständigen die vollständige Adresse.
Scopes: Wann eine Abhängigkeit sichtbar ist
Nicht jede Abhängigkeit gehört auf jeden Classpath. Ein Test-Framework sollte nicht in deiner Produktions-JAR landen, und eine von einem Anwendungsserver bereitgestellte Servlet-API sollte nicht doppelt gebündelt werden. Maven steuert das mit dem <scope>-Element.
| Scope | Kompilierung | Test | Laufzeit | Paketiert | Typische Verwendung |
|---|---|---|---|---|---|
compile (Standard) | Ja | Ja | Ja | Ja | Core-Bibliotheken, die du direkt aufrufst |
provided | Ja | Ja | Nein | Nein | APIs, die der Container bereitstellt (Servlet, JDBC-Treiber) |
runtime | Nein | Ja | Ja | Ja | Implementierungen, die nur zur Laufzeit benötigt werden |
test | Nein | Ja | Nein | Nein | JUnit, Mockito, Assertion-Bibliotheken |
system | Ja | Ja | Nein | Nein | Lokale JARs über absoluten Pfad (vermeiden) |
Eine test-scoped Abhängigkeit ist die häufigste Abweichung vom Standard. JUnit taucht nie in deinem ausgelieferten Artefakt auf:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Transitive Abhängigkeiten
Wenn du von einer Bibliothek abhängst, bist du auch von allem abhängig, was sie benötigt. Maven liest die veröffentlichte pom.xml jedes Artefakts, verfolgt diese Deklarationen rekursiv und fügt den gesamten Graphen automatisch zu deinem Classpath hinzu. Diese indirekten Einträge sind transitive Abhängigkeiten.
Deshalb kann eine einzige <dependency>-Zeile für ein Web-Framework Dutzende von JARs heranziehen, die du nie explizit angegeben hast. Scopes gelten dabei auch während dieses Durchlaufs: Eine test-scoped Abhängigkeit zieht ihre transitiven Abhängigkeiten nicht in deinen Kompilierungs-Classpath, und provided-Abhängigkeiten werden transitiv überhaupt nicht weitergegeben.
Du kannst den vollständigen Graphen mit dem Dependency-Plugin anzeigen:
$ mvn dependency:tree
[INFO] com.example:app:jar:1.0
[INFO] +- org.web:server:jar:2.4:compile
[INFO] | +- org.log:log:jar:1.2:compile
[INFO] | \- org.json:json:jar:1.7:compile - omitted for conflict with 1.9
[INFO] \- org.json:json:jar:1.9:compileVersionskonflikte und ihre Auflösung
Ein so tiefer Graph will fast immer dasselbe Artefakt in zwei verschiedenen Versionen haben. Maven kann nicht beide auf einem einzigen Classpath ablegen und wählt daher eine mit der Nearest-Wins-Mediation: Die Version, die in der geringsten Tiefe von deiner Projektwurzel entfernt deklariert ist, gewinnt – die anderen werden als omitted for conflict ausgelassen.
Im obigen Baum fordert dein Projekt org.json:json:1.9 direkt (Tiefe 1), während org.web:server transitiv 1.7 anfordert (Tiefe 2). Die Deklaration auf Tiefe 1 gewinnt. Befinden sich zwei Kandidaten auf derselben Tiefe, gewinnt der in der pom.xml zuerst deklarierte.
Wenn die automatische Wahl falsch ist, greifst du explizit ein. Eine direkte Abhängigkeit gewinnt immer, oder du fixierst Versionen im gesamten Projekt mit <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Um einen unerwünschten transitiven Zweig vollständig zu kappen, verwendest du <exclusions>:
<dependency>
<groupId>org.web</groupId>
<artifactId>server</artifactId>
<version>2.4</version>
<exclusions>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
</exclusions>
</dependency>Ein vollständiges Beispiel
Maven selbst ist in diesem Code-Runner nicht verfügbar, daher modelliert das folgende Programm seinen Resolver in reinem Java. Es veröffentlicht einige Artefakte in einem kleinen In-Memory-Repository und führt dann denselben Breitensuche-Durchlauf und die Nearest-Wins-Mediation durch, die Maven verwendet, um einen Abhängigkeitsgraphen in einen einzigen Classpath zu überführen. Beobachte, wie das tiefere org.json:json:1.7 gegen das flachere 1.9 verliert.
Was du aus dem Programmlauf mitnehmen solltest:
- Der aufgelöste Classpath listet jedes Artefakt genau einmal auf und spiegelt damit wider, wie Maven einen Graphen in eine Menge von JARs ohne doppelte group:artifact-Einträge überführt.
org.json:jsonerscheint in Version1.9, nicht1.7, weil die Nearest-Wins-Mediation den Kandidaten auf der flacheren Tiefe beibehält (Tiefe 1 schlägt Tiefe 2).- Die Spalte
depthmacht „nearest" greifbar:app:apphat Tiefe 0, seine direkten Abhängigkeiten haben Tiefe 1, und das transitiv gezogeneorg.log:loghat Tiefe 2. - „Tree edges visited: 4" zählt die deklarierten Abhängigkeitsbeziehungen, während „Distinct artifacts: 4" zeigt, dass der Graph nach der Mediation auf vier eindeutige Koordinaten reduziert wurde.
- Eine Koordinate wird übersprungen, sobald sie erneut auftaucht (
depthOf.containsKey(ga)), was genau der Grund ist, warum das tiefere1.7als „omitted for conflict" gilt und nicht ein zweites Mal hinzugefügt wird.
Sobald deine Abhängigkeiten sauber aufgelöst sind, stellt sich die nächste Frage: Wann lädt, kompiliert, testet und paketiert Maven sie? Diese Reihenfolge wird durch die Build-Phasen geregelt, die im Kapitel zum Maven-Lifecycle behandelt werden.