Maven, tour d'horizon du plugin Assembly

Parce que la création d'un JAR peut ne pas suffire lors de la livraison d'une application, le plugin maven-assembly permet de réaliser des paquets plus complexes.

Prenons l'exemple de la création d'une archive ZIP structurée ainsi :

- racine/
   - conf/
      - *.properties
   - lib/
      - *.jar
   - scripts/
      - application.jar
      - run.sh

Tout commence par l'ajout du plugin essentiel au POM du projet :

<plugin>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.3</version>
 <configuration>
   <descriptors>
  <descriptor>assemble/assembly.xml</descriptor>
   </descriptors>
 </configuration>
  </plugin>

Comme on le voit, cet extrait fait appel à assembly.xml qui est la description des opérations à effectuer. Voici un exemple de sa structure :

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
  <id>mon-zip-final</id>
  <formats>
    <format>zip</format>
  </formats>
  <fileSets>
      <!-- Récupération du JAR livrable -->
  <fileSet>
   <directory>target</directory>
   <outputDirectory>scripts</outputDirectory>
   <includes>
    <include>*.jar</include>
   </includes>
  </fileSet>
  <!-- Récupération des scripts shell de lancement -->
  <fileSet>
   <directory>deployment</directory>
   <outputDirectory>scripts</outputDirectory>
   <includes>
    <include>*.sh</include>
   </includes>
  </fileSet>
  <fileSet>
   <directory>src/main/resources/env/${env}</directory>
   <outputDirectory>conf</outputDirectory>
   <includes>
    <include>*.properties</include>
   </includes>
  </fileSet>
  </fileSets>
</assembly>

La première balise formats permet de définir la liste des formats attendus pour la distribution finale.

Chaque balise fileSet permet de créer des règles d'inclusion et/ou d'exclusion de fichiers à partir d'un répertoire :

  • On récupère le jar créé à partir du POM dans le répertoire "target" de maven
  • On regroupe les scripts shell de lancement issus du répertoire "deployment" du projet
  • On cherche les fichiers de propriétés à déplacer dans le répertoire "conf" en fonction de l'environnement cible, défini par la ligne de commande du build, par exemple : -Denv=production

Ces environnements sont configurés dans le POM par l'intermédiaire des balises "profile" :

<profiles>
 <!-- Profil de l'environnement de developpement -->
 <profile>
  <id>env.developpement</id>
  <activation>
   <property>
    <name>env</name>
    <value>developpement</value>
   </property>
   <activeByDefault>true</activeByDefault>
  </activation>
  <properties>
   <env>developpement</env>
  </properties>
  <build>
   ...

Reste maintenant à s'occuper du classpath de l'application et du jar exécutable.

Pour créer le répertoire "lib" contenant toutes les dépendances du projet, il faut ajouter à notre assembly les balises :

<!-- Pour mettre les dépendances du projet dans le répertoire lib -->
<dependencySets>
 <dependencySet>
  <outputDirectory>/lib</outputDirectory>
 </dependencySet>
</dependencySets>

La constitution du MANIFEST est dédiée au plugin JAR de maven, configuré ainsi :

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-jar-plugin</artifactId>
 <version>2.2</version>
 <configuration>
  <archive>
   <manifestEntries>
    <Class-Path>../conf/</Class-Path>
   </manifestEntries>
   <manifest>
    <addClasspath>true</addClasspath>
    <mainClass>com.developpef.CamelStartup</mainClass>
    <classpathPrefix>../lib/</classpathPrefix>
   </manifest>
  </archive>
 </configuration>
</plugin>

Où :

  • manifestEntries permet d'indiquer des éléments personnalisés à ajouter au classpath, ici notre répertoire contenant les fichiers de propriétés
  • mainClass est la classe principale du jar à exécuter
  • classpathPrefix spécifie le préfixe à ajouter à chaque entrée du classpath, le but étant dans notre exemple de pointer vers le répertoire parent "lib"

A ce niveau, maven est d'ores-et-déjà capable de produire une distribution proche de ce que nous voulions, structurée ainsi :

distribution-mon-zip-final.zip :
   - <artifact-id>-<version>/
      - conf/
         - *.properties
      - lib/
         - *.jar
      - scripts/
         - application-<version>.jar
         - run.sh

Il reste cependant quelques détails à régler.

Par défaut, le nom de l'archive finale est suffixé par l'identifiant de l'assembly. Pour éviter cela, il est possible d'ajouter la balise suivante dans le POM au niveau de la configuration du plugin "assembly" :

<appendAssemblyId>false</appendAssemblyId>

De la même façon, le répertoire racine de la distribution est suffixé par la version de l'artifact. Pour l'enlever, il faut ajouter cette balise dans l'assembly :

<baseDirectory>${artifactId}</baseDirectory>

Enfin, puisque le jar de l'application est exécuté par le script shell, pour ne pas avoir à réécrire ce dernier à chaque changement du version du jar, il est possible d'indiquer à maven le nom du jar à produire. Pour cela, ajoutons cette balise à la configuration du plugin "JAR" dans le POM :

<finalName>${artifactId}</finalName>

Nous y voilà! Il ne reste plus qu'à décompresser l'archive produite et lancer le script shell pour démarrer notre application :

distribution.zip :
   - <artifact-id>/
      - conf/
         - *.properties
      - lib/
         - *.jar
      - scripts/
         - application.jar
         - run.sh

Pour perfectionner notre système, il est possible de configurer l'appel à l'assemblage de manière implicite lors d'une phase du build maven. Par exemple, si nous voulons que notre distribution soit produite chaque fois que le jar est déployé, il suffit d'intégrer l'assemblage au moment de la phase "package" du build, comme ceci :

<plugin>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.2</version>
 <executions>
  <execution>
   <phase>package</phase>
   <goals>
    <goal>single</goal>
   </goals>
   <configuration>
    <descriptors>
     <descriptor>assemble/assembly.xml</descriptor>
    </descriptors>
    <appendAssemblyId>false</appendAssemblyId>
   </configuration>
  </execution>
 </executions>
</plugin>

Note :

Certaines versions de maven et/ou du plugin "assembly" peuvent provoquer l'erreur suivante lors du build :

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ------------------------------------------------------------------------
[INFO] Failed to create assembly: Error adding file '(...)' to archive: /(...)/target/classes isn't a file.

Ceci est du à la présence de la balise dependencySets dans l'assembly ainsi qu'à un conflit lors de l'exécution des différentes phases du build (dans un projet multimodules par exemple).

La solution de contournement la plus rapide consiste à copier manuellement les dépendances du projet dans un répertoire temporaire et les ajouter ensuite dans la distribution.

Il faut donc configurer dans le POM le plugin "maven-dependency-plugin" pour appeler la copie :

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-dependency-plugin</artifactId>
 <executions>
  <execution>
   <id>copy-dependencies</id>
   <phase>package</phase>
   <goals>
    <goal>copy-dependencies</goal>
   </goals>
   <configuration>
    <outputDirectory>dependencies</outputDirectory>
    <overWriteReleases>false</overWriteReleases>
    <overWriteSnapshots>false</overWriteSnapshots>
    <overWriteIfNewer>true</overWriteIfNewer>
   </configuration>
  </execution>
 </executions>
</plugin>

Puis récupérer par l'assembly tous les jar produits dans notre zip final (ce qui remplace l'appel à dependencySets) :

<fileSet>
 <directory>dependencies</directory>
 <outputDirectory>lib</outputDirectory>
 <includes>
  <include>*.jar</include>
 </includes>
</fileSet>

Enfin, puisqu'il s'agit d'un répertoire temporaire, ne pas oublier de l'inclure dans la phase de "clean" du projet. Dans le POM :

<plugin>
 <artifactId>maven-clean-plugin</artifactId>
 <configuration>
  <filesets>
   <fileset>
    <directory>dependencies</directory>
   </fileset>
  </filesets>
 </configuration>
</plugin>

Bon déploiement!


Fichier(s) joint(s) :

2 commentaires:

Julien Carsique a dit…

Bonjour,

Plutôt que "target", mieux vaut utiliser la propriété ${project.build.directory} (voir http://docs.codehaus.org/display/MAVENUSER/MavenPropertiesGuide).

Plutôt que d'utiliser maven-clean-plugin pour supprimer le répertoire temporaire "dependencies", mieux vaut mettre ce répertoire dans le répertoire "target" qui est dédié au contenu temporaire et sera automatiquement supprimé par maven-clean-plugin sans configuration spécifique.

Concernant la création d'un assemblage, mon expérience m'amène à déconseiller l'utilisation du maven-assembly-plugin pour les raisons suivantes :
- syntaxe non intuitive
- fonctionnalités limitées
- utilisation fastidieuse
- maintenance difficile dès lors qu'on veut faire des assemblages complexes.

Chez Nuxeo, après avoir utilisé ce plugin, nous nous sommes rapidement orientés vers d'autres solutions, par ordre chronologique :
- maven-nuxeo-plugin: plugin maison étendant reprenant les principes du maven-assembly-plugin,
- maven-antrun-plugin
- maven-antrun-extended-plugin: une extension du maven-antrun-plugin faite par Kohsuke
- nuxeo-distribution-tools: une extension de maven-antrun-extended-plugin faite par Nuxeo.

Le choix de ces plugins Ant s'est révélé judicieux.
La syntaxe Ant est connue, maîtrisée et concise.
Ces plugins ajoutent aux tâches Ant classiques des tâches permettant de manipuler les objets Maven (arbre de dépendances, artifact, propriétés, profils, ...). Il est très facile d'ajouter de nouvelles tâches (ant-contrib ou spécifiques).
Le résultat est beaucoup plus facile à maintenir parce que plus lisible et plus puissant fonctionnellement.
Voici deux exemples d'assemblages relativement complexes et quasi-impossibles à réaliser avec maven-assembly-plugin:
- nuxeo-distribution-cap assembly
- nuxeo-distribution-tomcat assembly

A noter que nous n'utilisons plus du tout maven-assembly-plugin, y compris pour des assemblages très simples : ça sera toujours plus simple et concis avec Ant.

Paul-Emmanuel Faidherbe a dit…

Bonjour,

Merci beaucoup pour toutes ces remarques constructives et ce retour d'expérience intéressant. C'est effectivement le genre de contribution que je cherchai à "provoquer".

Il est vrai que l'Assembly paraît assez vite limitatif comparé à ce que propose Ant (certainement bien plus dédié à ce genre de manipulation de ressources).

Je tiendrai donc compte de ces informations pour, dans un premier temps, améliorer ce qui est en place, puis éventuellement proposer la migration vers Ant si le système vient à se complexifier.

A suivre donc!