Dépendances Maven en Java
Déclarez et gérez les dépendances Java dans Maven avec dependency, scope et résolution transitive.
Les vrais projets Java ne sont presque jamais autonomes. Ils font appel à des frameworks de journalisation, des clients HTTP, des parseurs JSON et des bibliothèques de test écrits par d'autres. Le rôle de Maven est de récupérer ces bibliothèques, de récupérer leurs bibliothèques à leur tour, et d'assembler un classpath cohérent sans que vous ayez à gérer un seul JAR manuellement. Comprendre comment il procède fait la différence entre un build qui fonctionne et un après-midi perdu à cause d'un NoSuchMethodError.
Ce chapitre explique comment une dépendance est nommée (coordonnées), comment les portées contrôlent la visibilité de chaque bibliothèque, comment Maven parcourt le graphe des dépendances transitives, et comment il résout les conflits de version. Il suppose que vous disposez déjà d'un pom.xml ; sinon, commencez par le chapitre Maven POM.
Coordonnées : Comment une dépendance est nommée
Chaque artifact dans l'univers Maven est identifié par un ensemble de coordonnées. Les trois que vous devez toujours fournir sont le groupId (l'éditeur, généralement un domaine inversé), l'artifactId (le nom du projet) et la version. Ensemble, ils pointent vers un seul JAR dans un dépôt.
Vous déclarez une dépendance dans le bloc <dependencies> de votre pom.xml :
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>Le raccourci groupId:artifactId:version est appelé une chaîne GAV, et vous la rencontrerez partout : dans les messages d'erreur, dans l'arbre de dépendances, et sur les pages web du dépôt central. Une quatrième coordonnée, le type (jar par défaut), et une cinquième, le classifier (pour les variantes comme sources ou javadoc), complètent l'adresse complète.
Portées : Quand une dépendance est visible
Toutes les dépendances n'ont pas leur place sur chaque classpath. Un framework de test ne doit pas être embarqué dans votre JAR de production, et une API de servlet fournie par le serveur d'application ne doit pas être incluse deux fois. Maven contrôle cela avec l'élément <scope>.
| Portée | Compilation | Test | Exécution | Packagé | Utilisation typique |
|---|---|---|---|---|---|
compile (défaut) | Oui | Oui | Oui | Oui | Bibliothèques principales que vous appelez directement |
provided | Oui | Oui | Non | Non | APIs fournies par le conteneur (servlet, pilote JDBC) |
runtime | Non | Oui | Oui | Oui | Implémentations nécessaires uniquement à l'exécution |
test | Non | Oui | Non | Non | JUnit, Mockito, bibliothèques d'assertion |
system | Oui | Oui | Non | Non | JARs locaux par chemin absolu (à éviter) |
Une dépendance de portée test est la plus courante en dehors du défaut. JUnit ne se retrouve jamais dans votre artifact livré :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Dépendances transitives
Lorsque vous dépendez d'une bibliothèque, vous dépendez également de tout ce dont elle dépend. Maven lit le pom.xml publié de chaque artifact, suit ces déclarations de manière récursive, et ajoute automatiquement l'ensemble du graphe à votre classpath. Ces entrées indirectes sont des dépendances transitives.
C'est pourquoi une seule ligne <dependency> pour un framework web peut entraîner des dizaines de JARs que vous n'avez jamais nommés. La portée s'applique toujours lors de ce parcours : une dépendance de portée test ne tire pas ses dépendances transitives dans votre classpath de compilation, et les dépendances provided ne sont pas propagées transitivement.
Vous pouvez voir le graphe complet avec le plugin dependency :
$ 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:compileMédiation des conflits de version
Un graphe aussi profond veut presque toujours le même artifact à deux versions différentes. Maven ne peut pas les placer toutes les deux sur un même classpath, il en choisit donc une en utilisant la médiation nearest-wins (le plus proche gagne) : la version déclarée à la profondeur la plus faible par rapport à la racine de votre projet l'emporte, et les autres sont omises pour conflit.
Dans l'arbre ci-dessus, votre projet demande org.json:json:1.9 directement (profondeur 1), tandis que org.web:server demande 1.7 transitivement (profondeur 2). La déclaration à la profondeur 1 gagne. Si deux candidats se trouvent à la même profondeur, celui déclaré en premier dans le pom.xml gagne.
Lorsque le choix automatique est incorrect, vous prenez le contrôle explicitement. Une dépendance directe gagne toujours, ou vous pouvez fixer les versions pour l'ensemble du projet avec <dependencyManagement> :
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Pour supprimer entièrement une branche transitive indésirable, utilisez <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>Un exemple complet
Maven lui-même n'est pas disponible sur ce runner de code, donc le programme ci-dessous modélise son résolveur en Java pur. Il publie quelques artifacts dans un petit dépôt en mémoire, puis effectue le même parcours en largeur d'abord et la même médiation nearest-wins que Maven utilise pour aplatir un graphe de dépendances en un seul classpath. Observez comment le org.json:json:1.7 plus profond perd face au 1.9 moins profond.
Ce qu'il faut retenir de l'exécution :
- Le classpath résolu liste chaque artifact exactement une fois, reflétant la façon dont Maven aplatit un graphe en un ensemble de JARs sans doublons de groupId:artifactId.
org.json:jsonapparaît à la version1.9, et non1.7, car la médiation nearest-wins conserve le candidat trouvé à la profondeur la plus faible (profondeur 1 l'emporte sur profondeur 2).- La colonne
depthrend le terme "plus proche" concret :app:appest à la profondeur 0, ses dépendances directes sont à la profondeur 1, etorg.log:logtiré transitivement est à la profondeur 2. - "Tree edges visited: 4" compte les relations de dépendance déclarées, tandis que "Distinct artifacts: 4" montre le graphe réduit à quatre coordonnées uniques après médiation.
- Une coordonnée est ignorée dès qu'elle est rencontrée à nouveau (
depthOf.containsKey(ga)), ce qui explique exactement pourquoi le1.7plus profond est "omis pour conflit" plutôt qu'ajouté une seconde fois.
Une fois vos dépendances résolues proprement, la question suivante est de savoir quand Maven les télécharge, compile, teste et package. Cet ordre est régi par les phases de build couvertes dans le chapitre sur le cycle de vie Maven.