W3docs

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.

ScopeKompilierungTestLaufzeitPaketiertTypische Verwendung
compile (Standard)JaJaJaJaCore-Bibliotheken, die du direkt aufrufst
providedJaJaNeinNeinAPIs, die der Container bereitstellt (Servlet, JDBC-Treiber)
runtimeNeinJaJaJaImplementierungen, die nur zur Laufzeit benötigt werden
testNeinJaNeinNeinJUnit, Mockito, Assertion-Bibliotheken
systemJaJaNeinNeinLokale 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:compile

Versionskonflikte 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.

java— editable, runs on the server

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:json erscheint in Version 1.9, nicht 1.7, weil die Nearest-Wins-Mediation den Kandidaten auf der flacheren Tiefe beibehält (Tiefe 1 schlägt Tiefe 2).
  • Die Spalte depth macht „nearest" greifbar: app:app hat Tiefe 0, seine direkten Abhängigkeiten haben Tiefe 1, und das transitiv gezogene org.log:log hat 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 tiefere 1.7 als „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.

Übungen

Übung
In Maven erscheinen zwei Versionen desselben Artefakts im Abhängigkeitsgraphen: Version 1.9, direkt in deiner pom deklariert (Tiefe 1), und Version 1.7, transitiv hereingezogen (Tiefe 2). Welche Version landet auf dem Classpath?
In Maven erscheinen zwei Versionen desselben Artefakts im Abhängigkeitsgraphen: Version 1.9, direkt in deiner pom deklariert (Tiefe 1), und Version 1.7, transitiv hereingezogen (Tiefe 2). Welche Version landet auf dem Classpath?
Was this page helpful?