mercredi 18 mai 2011

Maven: quand test et compile s'emmêlent

Voici un problème un peu vicieux auquel j’ai été confronté récemment. En cause, un comportement inattendu (du moins à mes yeux) de Maven.

Le contexte, un petit projet Web avec, entre autres, de l’Hibernate. Dans mon pom.xml, j’ai donc les lignes suivantes:

<dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-annotations</artifactId>
         <version>3.4.0.GA</version>
         <scope>compile</scope>
</dependency>

Cette dépendance est suffisante pour obtenir toutes les librairies nécessaires à Hibernate. Elle importe notamment Hibernate-core.

Pour les tests de mes dao, j’utilise HSQL comme DB mémoire et DBCP pour me fournir un pool de connexions.

Il y a aussi du spring dans mon projet, ce qui a finalement peu d'importance. Je configure les fichiers web.xml, applicationContext.xml... et j’essaye de déployer "à blanc" l’application sur un Tomcat. C’est une opération que j’effectue toujours avec un nouveau projet, car elle me permet de valider que la configuration de base est correcte.

Et là, l’application refuse de se déployer. Dans les logs, je vois un java.lang.NoClassDefFoundError: org/apache/commons/collections/map/LRUMap. C'est Hibernate qui cherche cette classe mais ne la trouve pas.

C’est une classe des commons-collections. Eclipse, avec le plugin Maven m'indique pourtant qu'elle se trouve dans mes dépendances. Une autre vérification m'indique que le jar est effectivement déployé dans le répertoire WEB-INF/lib. Cependant, la version déployée ne contient pas la classe LRUMap.

Il s'agit de la version 2.1 et en y regardant de plus près, je vois que Hibernate demande (dépendance transitive) la version 3.1.

Comment cela se fait-il?

Une version peut en masquer une autre

Le problème vient de la version de DBCP utilisée pour les tests, la 1.2.1, une "vieille".

<dependency>
         <groupId>commons-dbcp</groupId>
         <artifactId>commons-dbcp</artifactId>
         <version>1.2.1</version>
         <type>jar</type>
         <scope>test</scope>
</dependency>

Celle-ci a comme dépendance transitive la version 2.1 des commons-collections.

Du point de vue Maven, il y a donc conflit entre les deux versions. Pour le résoudre, Maven va choisir la dépendance la plus proche de la racine.

La version 3.1 de commons-collection est une dépendance transitive d’une dépendance transitive (hibernate-core) d’hibernate-annotations. Donc, un niveau 2. Par contre, la version 2.1 est une dépendance transitive directe de dbcp, donc de niveau 1.

C’est la version 2.1 qui est choisie.

Oui mais !

Le mécanisme de résolution est clair, mais dbcp est en test ! Et quand je fais un package, les dépendances de test ne sont pas incluses dans mon War final (de même que les classes et les ressources de test). C’est normal.

Et pourtant...

Si on fait un mvn  dependency:resolve, on obtient le résultat suivant (je ne garde que les lignes intéressantes):

[INFO] The following files have been resolved:
...
[INFO]    commons-collections:commons-collections:jar:2.1:compile
[INFO]    commons-dbcp:commons-dbcp:jar:1.2.1:test
...
[INFO]    org.hibernate:hibernate-annotations:jar:3.4.0.GA:compile
[INFO]    org.hibernate:hibernate-commons-annotations:jar:3.1.0.GA:compile
[INFO]    org.hibernate:hibernate-core:jar:3.3.0.SP1:compile
…

Le scope de dbcp est bien test. Le scope d’hibernate est bien compile. De même pour commons-collection... sauf qu’il est dans la version 2.1, celle qui vient de test.

Et si je fais mvn dependency:resolve -DincludeScope=compile, je n’ai pas les jars de scope test (heureusement !). Commons-collections y est bien, puisqu’il est une dépendance transitive d’un jar en scope compile (hibernate)  MAIS sa version est celle qui vient de dbcp, lequel est en test.

Est-ce un bug? Toujours que le résultat est le même avec Maven version 2.2.1 et 3.0.3.

Le compile et le packaging ne devraient-ils se baser uniquement sur les dépendances compile et provided (hors test) pour résoudre les dépendances et leurs versions?

Quoi qu’il en soit, dans mon cas, la solution au problème était assez simple, puisqu’il suffisait de changer la version de dbcp:

<dependency>
       <groupId>commons-dbcp</groupId>
       <artifactId>commons-dbcp</artifactId>
       <version>1.3</version>
       <type>jar</type>
       <scope>test</scope>
</dependency>

Cela met les pendules à l’heure puisque la dépendance transitive vers les commons-collection est aussi 3.1.

N’empêche, le risque est  là. Dans le cas présent, le problème empêchait le déploiement, mais ne peut-on pas imaginer que le problème se produirait plus tard, à l'exécution d'une obscure méthode (forcément jamais testée...), avec alors beaucoup de difficultés pour déterminer la cause profonde.

Voilà qui fait froid dans le dos, non?

Aucun commentaire:

Enregistrer un commentaire