Las aplicaciones web Java se empaquetan en ficheros war o ear y son desplegadas por un contenedor de servlets o servidor de aplicaciones JEE siguiendo siempre los estándares. En cambio, a la hora de empaquetar una aplicación de escritorio o consola tendremos que encontrar una estrategia que permita una fácil distribución y ejecución de estas aplicaciones. En este artículo veremos de forma muy práctica varias alternativas utilizando plugins de Maven.
Entorno de pruebas:
Requisitos: Conocimientos muy básicos de Maven
Proyecto para pruebas
Vamos a partir de una aplicación de escritorio desarrollada con Swing y gestionada con Maven que cuente además con dependencias. Para simplificar, usaremos el Hello World de Oracle ligeramente modificado y una única dependencia con log4j.
package com.danielme.blog.maven; import java.awt.Dimension; import javax.swing.*; import org.apache.log4j.Logger; public class HelloWorldSwing { private static final Logger LOG = Logger.getLogger(HelloWorldSwing.class); /** * Create the GUI and show it. For thread safety, * this method should be invoked from the * event-dispatching thread. */ private static void createAndShowGUI() { //Create and set up the window. JFrame frame = new JFrame("HelloWorldSwing"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Add the ubiquitous "Hello World" label. JLabel label = new JLabel("Hello World"); frame.getContentPane().add(label); frame.setPreferredSize(new Dimension(300, 250)); frame.pack(); frame.setVisible(true); } public static void main(String[] args) { //Schedule a job for the event-dispatching thread: //creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGUI(); } }); } }
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.danielme.blog</groupId> <artifactId>maven-exec</artifactId> <version>1.0</version> <name>maven-exec</name> <inceptionYear>2015</inceptionYear> <packaging>jar</packaging> <description>Maven - ficheros ejecutables</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.7</java.version> <compiler.plugin>3.3</compiler.plugin> <log4j.version>1.2.17</log4j.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> </dependencies> </project>
Construir un jar ejecutable
Para construir un fichero jar ejecutable mediante el comando java -jar .jar (o con javaw si se trata de una aplicación gráfica para Windows) podemos utilizar los dos siguientes plugins:
- maven-jar-plugin: crea el .jar ejecutable propiamente dicho y permite personalizar el fichero META_INF/MANIFEST.MF que se creará en la raiz del jar con los detalles sobre el mismo. Las entradas del Manifest que tenemos que configurar como mínimo son dos:
- El nombre completo de la clase Main que se lanzará al ejecutar el jar. De esta forma se evita tener que pasar este parámetro como argumento a java.
- El classpath que utilizará la aplicación, es decir, los directorios y ficheros en los que la máquina virtual buscará los recursos que utiliza la aplicación y que no se encuentran dentro del propio jar ejecutable. Habitualmente se utiliza un directorio llamado lib ubicado en el mismo nivel que el jar ejecutable.
- maven-dependency-plugin (opcional). Usaremos una de las numerosas funcionalidades proporcionadas por este plugin para que al generarse el jar ejecutable se genere el directorio lib mencionado en el punto anterior con todas las dependencias del proyecto. Esta tarea puede realizarse manualmente pero es más práctico automatizarla.
El elemento build del pom quedará como sigue:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>${jar.plugin}</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.danielme.blog.maven.HelloWorldSwing</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>
Ejecutando mvn clean package se creará la siguiente estructura.
Podemos echar un vistazo al Manifest dentro del jar.
Para poder utilizar la aplicación habrá que distribuirla de forma que tengamos siempre junto al jar ejecutable el directorio lib con las dependencias. En función de la configuración del sistema operativo es posible que baste con hacer doble click en el jar ejecutable para lanzarlo pero resulta conveniente proporcionar un script .bat, .sh, etc, para su ejecución. En el caso de Windows veremos más adelante cómo crear un .exe que incluso compruebe la presencia de Java en el equipo.
Empaquetar toda la aplicación un único jar ejecutable
Existe la posibilidad de crear un único jar ejecutable con todas las dependencias empaquetadas en el mismo. Esto se puede conseguir con el plugin genérico Assembly o bien con el plugin shade específico para estos menesteres. Veamos el segundo que es el que utilizo habitualmente.
La configuración básica de plugin shade es muy sencilla y ya está presente en ejemplos de otros artículos del blog. Simplemente indicamos el nombre completo de la clase Main y el plugin se encargará de crear el correspondiente jar ejecutable en el que se incluirán los ficheros .class de todas las dependencias que tengamos en el pom (existe la posibilidad de renombrar el nombre de los paquetes de las dependencias para evitar conflictos).
A veces podemos encontrarnos con errores si los jar de alguna dependencia están firmados por lo que al crear nuestro jar debemos excluir del mismo los ficheros relacionados con estas firmas.
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>${shade.plugin}</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.danielme.blog.maven.HelloWorldSwing</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> <exclude>META-INF/*.EC</exclude> </excludes> </filter> </filters> </configuration> </plugin> </plugins> </build>
La ejecución de mvn package creará dos ficheros: el jar de nuestro proyecto y el jar que además incluye todas las dependencias.
Ficheros .exe para Windows
Existe un fantástico proyecto Open Source llamado launch4j que permite la creación de .exe «lanzadores» de aplicaciones Java y que ofrece una gran variedad de opciones de configuración. Launch4j cuenta con una herramienta gráfica pero también se puede utilizar directamente en Maven gracias al plugin lauch4j-maven-plugin.
Vamos a combinar la configuración ya realizada del plugin dependency con el plugin de launch4j para crear un .exe que además contendrá el jar ejecutable de nuestro proyecto, utilizando como classpath el directorio lib.
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>${dependency.plugin}</version> <executions> <execution> <id>copy-dependencies</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.akathist.maven.plugins.launch4j</groupId> <artifactId>launch4j-maven-plugin</artifactId> <version>${launch4j.plugin}</version> <executions> <execution> <id>l4j-clui</id> <phase>package</phase> <goals> <goal>launch4j</goal> </goals> <configuration> <!--indicar gui (javaw) o console (java) --> <headerType>gui</headerType> <outfile>${project.build.directory}/app.exe</outfile> <classPath> <mainClass>com.danielme.blog.maven.HelloWorldSwing</mainClass> <!-- la ruta absoluta en la que tendremos las dependencias --> <!-- %EXEDIR% es la variable que indica el directorio en el que se está ejectando el .exe --> <jarLocation>%EXEDIR%/lib/</jarLocation> </classPath> <!-- icono del ejecutable --> <icon>src/main/resources/icono.ico</icon> <!-- impide ejecutar más de una instancia a la vez --> <singleInstance> <mutexName>myApp</mutexName> </singleInstance> <jre><!-- versión mínima requerida de java --> <minVersion>${java.version}.0</minVersion> </jre> <!-- metadatos Windows del .exe--> <versionInfo> <fileVersion>1.0.0.0</fileVersion> <txtFileVersion>versión 1.0</txtFileVersion> <fileDescription>demo de launch4j</fileDescription> <copyright>danielme.com</copyright> <productVersion>1.0.0.0</productVersion> <txtProductVersion>demo de launch4j</txtProductVersion> <productName>demo de launch4j</productName> <internalName>app</internalName> <originalFilename>app.exe</originalFilename> </versionInfo> </configuration> </execution> </executions> </plugin> </plugins> </build>
La orden mvn package creará además del jar del proyecto el correspondiente .exe y el directorio lib con las dependencias.
Si intentamos ejecutar la aplicación en un equipo que no disponga de una versión de Java compatible según lo configurado en lauch4j (en el ejemplo se ha establecido una versión mínima pero también se pueden definir exactamente las versiones compatibles) se mostrará el siguiente mensaje y al aceptar se abrirá el navegador para que el usuario se descargue Java (tanto el mensaje como la url de descarga son configurables).
Launch4j también permite especificar una jre distribuída dentro de nuestra aplicación para que sea la utilizada.
Si queremos que el .exe empaquete dentro del mismo todas las dependencias bastará con utilizar de nuevo el plugin shade y hacer que este sea el jar que launch4j utilice. En este caso no se definirá el elemento classpath.
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>${shade.plugin}</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.danielme.blog.maven.HelloWorldSwing</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> <exclude>META-INF/*.EC</exclude> </excludes> </filter> </filters> </configuration> </plugin> <plugin> <groupId>com.akathist.maven.plugins.launch4j</groupId> <artifactId>launch4j-maven-plugin</artifactId> <version>${launch4j.plugin}</version> <executions> <execution> <id>l4j-clui</id> <phase>package</phase> <goals> <goal>launch4j</goal> </goals> <configuration> <!--indicar gui (javaw) o console (java) --> <headerType>gui</headerType> <outfile>${project.build.directory}/app.exe</outfile> <!--<classPath> <mainClass>com.danielme.blog.maven.HelloWorldSwing</mainClass>--> <!-- la ruta absoluta en la que tendremos las dependencias --> <!-- %EXEDIR% es la variable que indica el directorio en el que se está ejectando el .exe --> <!--<jarLocation>%EXEDIR%/lib/</jarLocation> </classPath>--> <!-- icono del ejecutable --> <icon>src/main/resources/icono.ico</icon> <!-- impide ejecutar más de una instancia a la vez --> <singleInstance> <mutexName>myApp</mutexName> </singleInstance> <jre><!-- versión mínima requerida de java --> <minVersion>${java.version}.0</minVersion> </jre> <versionInfo> <fileVersion>1.0.0.0</fileVersion> <txtFileVersion>versión 1.0</txtFileVersion> <fileDescription>demo de launch4j</fileDescription> <copyright>danielme.com</copyright> <productVersion>1.0.0.0</productVersion> <txtProductVersion>demo de launch4j</txtProductVersion> <productName>demo de launch4j</productName> <internalName>app</internalName> <originalFilename>app.exe</originalFilename> </versionInfo> </configuration> </execution> </executions> </plugin> </plugins> </build>
Código de ejemplo
El código de ejemplo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Hola:
Lo configuré todo para generar el .exe a partir de mi aplicación. Todo funcionó correctamente, sin error alguno, pero cuando ejecuto el .exe que generé no hace lo que está en mi código fuente, pero tampoco genera error alguno. Lo mismo me sucedió con las opciones anteriores descritas en esta página.
Lo ejecuté desde el CMD y termina su ejecución sin error alguno, pero no realiza lo que está en mi código fuente. Mi código se conecta a varias fuentes de datos de internet y actualiza una base de datos local, sin embargo, cuando ejecuto el .exe nada de esto sucede.
Agradecería una respuesta.
Saludos.