Jar ejecutable con Maven
Una pregunta común que hacen usuarios nuevos de Maven es cómo crear un jar ejecutable. Generalmente las soluciones propuestas van en la línea de generar un sólo jar gigante –con todas las dependencias dentro– utilizando algo como uberjar, fatjar o el descriptor jar-with-dependencies de maven-assembly-plugin. Esto, en mi opinión, es inconveniente y poco estético, por varias razones:
- El jar resultante es una sopa de artefactos, ya que se mezclan todas las dependencias directas y transitivas con la aplicación misma.
- No escala; para proyectos con muchas dependencias, el proceso de formar el “super jar” es lento, por lo que no es una opción viable para utilizar en tiempo de desarrollo (sólo al hacer un release).
- Pueden haber problemas de licencia. Algunas dependencias pueden exigir que se mantenga la integridad de los binarios que distribuyen, prohibiendo “reempaques”.
Los requerimientos que deseo satisfacer son bastante simples:
- Generar un jar con el manifiesto necesario; esto es, que indique la clase con el punto de entrada main y las dependencias.
- Poder obtener todos los jars de las dependencias, directas y transitivas, en un directorio de salida aparte (lib, por ejemplo).
- Generar un pequeño script bash, que lance el jar con el classpath configurado.
- Obtener una distribución de la aplicación, o sea, copiar todo lo anterior en un directorio del sistema (o en target).
- Debe ser rápido!, ya que será ejecutado recurrentemente, en tiempo de desarrollo.
En algún momento del pasado lejano llegué a una configuración que satisface los objetivos listados, consistente en una combinación de maven-jar-plugin, maven-dependency-plugin y maven-resources-plugin.
La aplicación
La sofisticada aplicación de ejemplo es un “hola mundo” camuflado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package cl.totex.demo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Demo { private static final Logger logger = LoggerFactory.getLogger(Demo.class); public static void main(String... args) { if(args.length > 0) logger.info("holo {}", args[0]); else logger.info("duh!"); } } |
El POM
Aparte de toda la información usual de un POM, uso la propiedad install-dir, que indicará a Maven el directorio de instalación de la aplicación.
1 2 3 | <properties> <install-dir>${build.directory}</install-dir> </properties> |
Las únicas dependencias son las correspondientes al logger, slf4j. Por simplicidad, el binding elegido es SimpleLogger
; por lo tanto, las dependencias necesarias en el POM son:
1 2 3 4 5 6 7 8 9 10 | <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.6.0</version> </dependency> |
El requerimiento 1 se puede solucionar con plugin maven-jar-plugin, que permite generar un jar con un manifiesto donde queda establecida la clase con el punto de entrada main y el classpath.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <plugin> <artifactId>maven-jar-plugin</artifactId> <version>2.2</version> <configuration> <archive> <manifest> <mainClass>cl.totex.demo.Demo</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib</classpathPrefix> </manifest> </archive> </configuration> </plugin> |
Para el segundo requerimiento se puede usar el plugin maven-dependency-plugin, que permite copiar las dependencias directas y transitivas al directorio lib. El plugin sólo copiará los jars faltantes, por lo que la ejecución de la fase install no se ralentiza.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <plugin> <artifactId>maven-dependency-plugin</artifactId> <version>2.0</version> <executions> <execution> <id>copy-dependencies</id> <phase>install</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${install-dir}/${artifactId}/lib</outputDirectory> <overWriteReleases>false</overWriteReleases> <overWriteSnapshots>false</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> </configuration> </execution> </executions> </plugin> |
El plugin maven-resources-plugin permite satisfacer los últimos dos requerimientos: generar un script de lanzamiento y copiar el jar de la aplicación al directorio install-dir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.4.3</version> <executions> <execution> <id>copy-jar</id> <phase>install</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${install-dir}/${artifactId}</outputDirectory> <resources> <resource> <directory>${build.directory}</directory> <includes> <include>${build.finalName}.jar</include> </includes> </resource> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> |
La opción filtering, le indica a Maven que debe filtrar es el script run.sh, esto es, reemplazar todas las propiedades que encuentra. El código original de run.sh es:
1 2 3 4 | #!/bin/bash java -cp ${install-dir}/${artifactId}/lib\ -jar ${install-dir}/${artifactId}/${build.finalName}.jar $* |
Al filtrar el script anterior, se reemplazan los placeholders ${install-dir}, ${artifactId} y ${build.finalName} por sus valores reales.
Instalación
Por ejemplo, al ejecutar mvn -Dinstall-dir=/home/totex install se crea el directorio /home/totex/demo, con el siguiente contenido:
1 2 3 4 5 6 7 8 9 | $ tree -f . |-- ./demo-1.0-SNAPSHOT.jar |-- ./lib | |-- ./lib/slf4j-api-1.6.0.jar | `-- ./lib/slf4j-simple-1.6.0.jar `-- ./run.sh 1 directory, 4 files |
El script run.sh resultante es:
1 2 3 4 | #!/bin/bash java -cp /home/totex/demo/lib\ -jar /home/totex/demo/demo-1.0-SNAPSHOT.jar $* |
La mayor ventaja de este mecanismo, es que se puede ir trabajando en el proyecto y probando la instalación al mismo tiempo, ya que el proceso es rápido.
Puedes bajar el archivo demo.tar.gz, que contiene el proyecto de ejemplo.

Ultimos comentarios