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) :