Maven : les concepts clés
Avant de commencer
Avant Maven, construire un projet Java signifiait écrire soi-même un script de compilation, maintenir à la main la liste des bibliothèques du classpath et rejouer l’opération sur chaque machine. Le résultat est fragile : il dépend de la mémoire du scripteur et de l’état de son poste de travail.
Maven change de contrat. Au lieu de décrire comment construire, on décrit ce qu’est le projet : ses coordonnées, ses dépendances, sa structure. Maven en déduit une procédure de construction standard. Cette leçon pose le vocabulaire et la logique sur lesquels le TP s’appuie ; elle n’énumère pas de commandes.
Objectifs
À l’issue de cette leçon, vous devriez être capable de :
- expliquer ce qu’un POM déclaratif remplace par rapport à un script manuel ;
- identifier les coordonnées d’un artefact et en déduire son emplacement dans le dépôt local ;
- situer une invocation dans un cycle de vie, et distinguer phase, goal et plugin ;
- choisir un scope de dépendance selon le moment où la bibliothèque est nécessaire ;
- prévoir l’effet d’une dépendance transitive et la rendre lisible ;
- distinguer héritage (
<parent>) et agrégation (<modules>) ; - lire un
BUILD FAILUREet isoler la cause utile.
1. Le POM décrit le projet
Le fichier pom.xml, appelé POM (Project Object Model), est la seule configuration obligatoire d’un projet Maven. Il répond à trois questions : qui êtes-vous, de quoi avez-vous besoin, comment devez-vous être construit.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.example</groupId>
<artifactId>billing</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
</project>Le triplet groupId / artifactId / version forme les coordonnées du projet : il l’identifie de façon unique aux yeux de Maven, comme une adresse à trois niveaux. Une quatrième brique, packaging (jar, war, pom), précise la forme de l’artefact produit. Pour un projet Java standard, <packaging>jar</packaging> est la forme attendue ; jar étant la valeur par défaut, l’élément peut être omis sans changer le résultat — on le rend explicite quand il faut signaler une autre nature d’artefact.
2. Les coordonnées dictent l’organisation du dépôt local
Les dépendances ne vivent pas dans le projet. Elles sont téléchargées puis rangées dans le dépôt local, par défaut ~/.m2/repository, dont l’arborescence reproduit exactement les coordonnées :
~/.m2/repository/
└── fr/example/billing/1.0.0/
├── billing-1.0.0.jar
└── billing-1.0.0.pom
Trois conséquences pratiques :
- une machine neuve doit tout télécharger : la première construction est lente, la seconde quasi instantanée ;
- le dépôt local est un cache, jamais une source : on peut le vider sans rien perdre, et il n’est jamais versionné ;
- une dépendance « introuvable » est d’abord une question de coordonnées.
3. Cycle de vie : les phases sont ordonnées
Maven définit plusieurs cycles de vie indépendants (clean, default, site). Une phase est une étape nommée et ordonnée : invoquer une phase exécute implicitement toutes celles qui la précèdent.
mvn package # exécute validate, compile, test, puis package| Phase | Rôle | Ce qui est produit |
|---|---|---|
validate |
vérifier la cohérence du POM | rien |
compile |
compiler src/main/java |
target/classes |
test |
exécuter les tests de src/test/java |
rapports de test |
package |
assembler l’artefact | target/*.jar |
verify |
contrôles complémentaires sur l’artefact | rien de publié |
install |
publier l’artefact dans le dépôt local | entrée dans ~/.m2 |
deploy |
publier l’artefact dans un dépôt distant | artefact partagé |
clean n’appartient pas au cycle default. Écrire mvn clean package ne place pas une phase avant les autres : cela invoque deux cycles distincts dans l’ordre donné sur la ligne de commande.
4. Phases, goals et plugins
Une phase n’accomplit rien par elle-même : elle est reliée à des goals (objectifs) apportés par des plugins. Formule à retenir : le cycle ordonne, le plugin fournit, le goal exécute.
mvn compiler:compile # goal 'compile' du plugin maven-compiler-plugin
mvn dependency:tree # goal 'tree' : affiche l'arbre des dépendancesCertains goals, comme dependency:tree, sont des outils de diagnostic invocables hors cycle. La configuration d’un plugin, elle, se déclare dans le POM et non sur la ligne de commande, sans quoi elle n’existerait que pour la frappe du jour :
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release> <!-- version de Java cible du projet -->
</configuration>
</plugin>
</plugins>
</build>5. Le scope répond à « quand cette dépendance est-elle nécessaire ? »
Le scope d’une dépendance borne les étapes où elle apparaît dans le classpath, et décide de sa propagation transitive. Pour un jar Maven standard, l’artefact final n’embarque pas les dépendances ; l’emballage dans une archive exécutable dépend d’un autre mécanisme (war, shade, assembly).
| Scope | Compilation | Tests | Exécution | Transitif |
|---|---|---|---|---|
compile (défaut) |
oui | oui | oui | oui |
provided |
oui | oui | non | non |
runtime |
non | oui | oui | oui |
test |
non | oui | non | non |
provided: l’environnement d’exécution fournit la bibliothèque ; elle sert à compiler, elle ne doit pas être fournie à l’exécution ni empaquetée dans un artefact d’exécution sous peine de doublon.runtime: inutile pour compiler, indispensable pour exécuter — un pilote JDBC que le code ne nomme jamais en est l’exemple type.test: strictement confinée aux tests.
Deux scopes supplémentaires existent (system, déprécié ; import, réservé au <dependencyManagement>) et ne sont pas mobilisés ici.
6. Une dépendance en apporte d’autres
En déclarant une dépendance, vous héritez aussi de ses propres dépendances : ce sont les dépendances transitives. Le scope se propage : une dépendance compile expose ses dépendances compile et runtime en compile ; une dépendance test d’un tiers reste confinée à ce tiers.
billing
└── fr.example:common:1.2.0 (compile)
└── org.slf4j:slf4j-api:2.0.9 (compile)
mvn dependency:tree -Dverbose # révèle l'arbre et les arbitrages de versionBénéfice : ne déclarer que ce dont le code se sert directement. Contrepartie : des versions que vous n’avez pas choisies peuvent être sélectionnées à votre place. Cette lecture est le premier réflexe lorsqu’un NoSuchMethodError apparaît à l’exécution alors que la compilation passait.
7. Héritage et agrégation ne sont pas la même relation
| Critère | Héritage | Agrégation |
|---|---|---|
| Déclaration | <parent> dans l’enfant |
<modules> dans le père |
| Effet | reprend propriétés, versions et configurations | construit plusieurs projets d’un seul appel (le reactor) |
| Implique une arborescence ? | non | oui : chaque module est un chemin déclaré |
<!-- côté enfant : je reprends une politique de construction -->
<parent>
<groupId>fr.example</groupId>
<artifactId>platform-parent</artifactId>
<version>3.1.0</version>
</parent><!-- côté père : je suis responsable de ces modules -->
<modules>
<module>common</module>
<module>billing</module>
</modules>Les deux relations se logent souvent dans le même fichier, d’où la confusion courante. Règle de conduite : le parent porte la politique (versions, encodage, options de compilation), chaque module garde la description de ce qu’il est. Un POM purement agrégateur se déclare en général <packaging>pom</packaging>.
8. Le wrapper fige la version de Maven dans le dépôt
mvn dépend d’une installation locale : deux machines, deux versions, deux comportements possibles. Le wrapper — mvnw, mvnw.cmd et le répertoire .mvn/wrapper/ — est un lanceur versionné qui télécharge et exécute la version de Maven inscrite dans le dépôt, et non celle laissée sur le poste.
./mvnw package # même version de Maven pour tout le mondeDans un projet versionné qui fournit un wrapper, les consignes doivent mentionner ./mvnw plutôt que mvn. Dans un TP où le projet est créé à la main et où le wrapper n’existe pas encore, les commandes peuvent rester écrites avec mvn ; générer le wrapper ensuite est une amélioration facultative. Le wrapper fige l’outil ; il ne dispense en rien de comprendre ce que la commande fait.
9. Lire un BUILD FAILURE
Un échec Maven est bavard, mais structuré :
[INFO] BUILD FAILURE
[ERROR] Failed to execute goal o.a.m.plugins:maven-compiler-plugin:3.11.0:compile
(default-compile) on project billing: Compilation failure
[ERROR] /home/you/dev/billing/src/main/java/App.java:[12,9] cannot find symbol
[ERROR] symbol: class Price
Quatre questions, dans cet ordre :
- Une goal est-elle nommée ? Si oui, elle apparaît en tête de la première ligne (
Failed to execute goal …) et localise l’étape en cause. Si aucune goal n’est citée, l’échec est antérieur à l’exécution d’une tâche : il relève en général de la résolution — dépendances ou plugins introuvables — et c’est cette piste qu’il faut suivre d’emblée. - Quel module est en cause ? Réponse dans
on project …, lorsqu’un projet est identifié. - La première ligne
[ERROR]porte la cause ; les suivantes sont souvent des conséquences. On ne lit qu’elle. - S’agit-il d’un problème de résolution ou de compilation / test ? Dans le premier cas, l’enquête porte sur les coordonnées et les dépôts ; dans le second, sur le code.
Could not resolve dependencies ne signifie pas « cette bibliothèque n’existe pas », mais « Maven ne l’a pas trouvée aux coordonnées indiquées, dans les dépôts qu’il connaît ». Relire la déclaration dans le POM est plus efficace que de vider le dépôt local, qui ne fait que masquer le problème et le faire rejouer. Pour trancher sans lancer une construction complète, dependency:resolve vérifie que chaque dépendance déclarée est bien atteignable ; dependency:tree en affiche le sous-arbre.
À retenir
- Le POM décrit, Maven déduit.
- Coordonnées =
groupId:artifactId:version— et l’arborescence du dépôt local en est la conséquence mécanique. - Une phase entraîne toutes les phases précédentes ;
cleanest un autre cycle. - Un scope se choisit en demandant quand la bibliothèque est nécessaire.
- Toute dépendance apporte son propre sous-arbre.
<parent>est une relation d’héritage,<modules>une relation d’agrégation.- Dans un échec, le premier
[ERROR]est celui qui compte.
Auto-évaluation
Répondez en une ou deux phrases avant d’ouvrir le TP.
- Qu’est-ce qu’un POM déclaratif remplace, par rapport à une construction manuelle ?
- Où Maven range-t-il l’artefact
fr.example:billing:1.0.0dans le dépôt local ? mvn testcompile-t-il les sources principales ? Justifiez.- Vous avez besoin de JUnit uniquement depuis
src/test/java: quel scope ? - Un pilote JDBC est requis à l’exécution mais jamais nommé dans le code : quel scope ?
- Une dépendance déclarée en
testcontamine-t-elle l’artefact final ? - Un POM père déclare
<modules>et chaque enfant déclare<parent>: qui relève de l’agrégation, qui relève de l’héritage ? mvn clean packageéchoue surCould not resolve dependencies: citez deux causes distinctes à tester dans l’ordre.
Éléments de réponse
- Le script de construction et la gestion manuelle du classpath.
~/.m2/repository/fr/example/billing/1.0.0/billing-1.0.0.jar, avec son.pom.- Oui :
testest postérieure àcompile, qui est donc exécutée par la même invocation. test.runtime.- Non : une dépendance en scope
testn’est ni transitive, ni embarquée dans unjarstandard. <modules>agrège,<parent>hérite.- D’abord les coordonnées (artefact, groupe ou version mal écrits, version inexistante) ; ensuite le dépôt censé héberger l’artefact — un échec de téléchargement mis en cache se relance avec
-U.
Étape suivante
Le TP 01_java_P_maven_lab.qmd met ces notions à l’épreuve sur un projet que vous manipulez vous-même : ses consignes fixent le détail des commandes, la présente leçon en fournit la logique. Revenez y chercher une définition au premier message d’erreur obscur — la cause est le plus souvent une coordonnée, un scope ou une phase mal interprétée.