Avec une application toute simple on tombe déjà sur de nombreux problèmes, prenons l'exemple d'une application qui fait un "hello world" en prenant soin d'utiliser au passage une librairie tierce (Google Guava dans notre exemple).
On a la classe suivante qui sert de point d'entrée de l'application :
package org.codingmojo; import static com.google.common.base.Preconditions.checkNotNull; /** * Hello world! */ public class App { public static void main( String[] args ) { if(args.length !=0) { checkNotNull(args[0]); } System.out.println( "Hello World!" ); } }
Notre projet, qui respecte les standards maven d'organisation du code, contient le pom.xml suivant :
4.0.0 <groupId>org. codingmojo</groupId> <artifactId>batch</artifactId> <version>1.0</version> <packaging>jar</packaging> <name>batch</name> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>r07</version> </dependency> </dependencies> </project>
Pour créer notre jar exécutable via maven, la documentation du plugin jar fourni une partie de la solution à notre problème. En effet, il est possible de générer un jar qui pointe vers les dépendances déclarées dans le pom.xml du projet.
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.codingmojo.App</mainClass> </manifest> </archive> </configuration> </plugin>
Après avoir ajouté cette description du plugin jar au projet, si on lance un "mvn install" on obtient un jar dont le fichier manifest référence des librairies qui ne se trouvent pas dans $CLASSPATH/lib/*
Si on lance l'application à cette étape, on obtient donc à cette étape un joli :
> java -jar target/batch-1.0.jar test > Exception in thread "main" java.lang.NoClassDefFoundError: com/google/common/base/Preconditions at org.codingmojo.App.main(App.java:14) Caused by: java.lang.ClassNotFoundException: com.google.common.base.Preconditions at java.net.URLClassLoader$1.run(URLClassLoader.java:200) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:188) at java.lang.ClassLoader.loadClass(ClassLoader.java:315) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:330) at java.lang.ClassLoader.loadClass(ClassLoader.java:250) at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:398) ... 1 more
On a donc pour l'instant un jar qui sait qu'il a besoin de dépendances pour se lancer mais qui ne peut les trouver de manière simple (i.e. sans installation manuelle des dépendances au bon endroit).
Pour automatiser cette partie de la construction du livrable, une solution qui s'offre à nous passe par l'utilisation du plugin assembly de maven permettant à la fois de créer une archive zip (qui servira de livrable-with-dependencies) et d'injecter dans cette archive notre jar ainsi que ses dépendances bien placées.
Pour cela, plusieurs configurations sont nécessaires sur le projet, on ajoute une configuration au plugin assembly puis on décrit la manière dont il va construire le livrable (agréger les libs + le jar dans une nouvelle archive)
La configuration à ajouter au pom est assez succinte :
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptors> <descriptor>src/main/assembly/assembly.xml</descriptor> </descriptors> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
Il faut ensuite créer le descripteur assembly.xml référencé par la configuration du plugin dans src/main/assembly :
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> <id>executable</id> <formats> <format>zip</format> </formats> <dependencySets> <dependencySet> <outputDirectory>/lib</outputDirectory> <excludes><exclude>${project.groupId}:${project.artifactId}:*</exclude></excludes> <unpack>false</unpack> </dependencySet> </dependencySets> <fileSets> <fileSet> <directory>${project.build.directory}</directory> <outputDirectory>/</outputDirectory> <includes> <include>*.jar</include> </includes> </fileSet> </fileSets> </assembly>
Ce fichier explique au plugin qu'il faudra déplacer toutes les dépendances du projet dans un dossier lib (à l'exclusion du jar du module lui-même) puis déplacer le jar du module à la racine de l'archive. Le dossier de travail (par défaut "artifactId-version")
Une fois cette configuration ajoutée, il suffit de faire un mvn install puis de dezipper l'archive que l'on trouve dans le repertoire de build comme ceci, on peut ensuite lancer le jar sans opération manuelle :
> mvn clean install [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building batch [INFO] task-segment: [clean, install] [INFO] ------------------------------------------------------------------------ [INFO] [clean:clean {execution: default-clean}] [INFO] ... [INFO] ... [INFO] ... [INFO] Installing /Users/XXX/Maven/batch/target/batch-1.0.jar to /Users/XXX/.m2/repository/org/codingmojo/batch/1.0/batch-1.0.jar [INFO] Installing /Users/XXX/Maven/batch/target/batch-1.0-executable.zip to /Users/XXX/.m2/repository/org/codingmojo/batch/1.0/batch-1.0-executable.zip [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5 seconds [INFO] Finished at: Sat Dec 18 11:29:41 CET 2010 [INFO] Final Memory: 29M/79M [INFO] ------------------------------------------------------------------------
> unzip target/batch-1.0-executable.zip Archive: target/batch-1.0-executable.zip creating: batch-1.0/ inflating: batch-1.0/batch-1.0.jar creating: batch-1.0/lib/ inflating: batch-1.0/lib/guava-r07.jar
> java -jar batch-1.0/batch-1.0.jar test Hello World!
...ou alors on peut utiliser One-Jar qui "bundle" les dépendances à l'intérieur même du JAR (en utilisant un classloader spécifique au runtime):
RépondreSupprimerhttp://code.google.com/p/onejar-maven-plugin/
ou bien le maven-shade-plugin qui bundle les *classes* (et non les JARs):
http://maven.apache.org/plugins/maven-shade-plugin/index.html
l'approche de onejar avec classloader spécifique au runtime me paraît bien sympathique :-) Je me le garde sous le coude pour les jours de refacto.
RépondreSupprimerPar contre l'approche "je mets toutes mes classes dans un seul et unique jar" du shade plugin me paraît trop restreinte et assez contraignante en terme de couplage code/libs dans le livrable
Je prendrais comme exemple le Hamcrest embarqué dans l'une des dernières livraisons JUnit qui oblige ceux qui ne veulent pas de cette version à utiliser un junit-no-dep n'embarquant pas les classes Hamcrest - source "les castcodeurs" d'avril 2011.
J'utilisais l'approche qu'on retrouve généralement (tout dépacker dans un rep et tout repacker dans un jar) mais j'trouve ton approche bcp plus propre :)
RépondreSupprimerJe me garde aussi le onjar de coté qui à l'aire bien sympathique même si pas super documenté sur la config.
Super intéressant ton site Ludo, j'me le rajoute dans mon netvibes ;)
Bonjour,
RépondreSupprimerPour un projet "simple", le plugin assembly offre aussi une solution tout-en-un grâce à un "descriptorRef" inclus en natif par le plugin : http://maven.apache.org/plugins/maven-assembly-plugin/descriptor-refs.html#jar-with-dependencies.