Crear un paquete Jar ejecutable con las dependencias incluidas usando Maven

por Mar 29, 2020Desarrollo de Software0 Comentarios

Recientemente he estado trabajando en un proyecto Java que usa bastantes dependencias (librerías externas) y algunas de ellas tenían a su vez dependencias transitivas. Lo que más quebraderos de cabeza me ha dado, ha sido crear un paquete jar las dependencias incluidas usando Maven. Eso es lo que pretendo explicar en esta ocasión

Introducción a Maven

Inicié el proyecto como un proyecto de Java normal y corriente, cuando empecé a necesitar las librerías las iba añadiendo. Pronto me dí cuenta del problema de las dependencias transitivas de las librerías que estaba usando, y era una locura ver cuales eran e ir añadiéndolas al classpath manualmente.

Aquello era insostenible, había que cambiar. Sabía de la existencia de Maven, pero nunca lo había usado y por falta de tiempo me resistía al cambio. Pero no había duda, el cambio era necesario.

Maven es una herramienta de construcción automática de proyectos tal como lo puede ser Ant, pero con una gran ventaja, el manejo automático de las dependencias. No es necesaria la descarga de las librerías, lo único que hay que hacer es especificarlas en el fichero pom.xml de configuración y maven se encargará de descargar las dependencias al repositorio local, y como no, también descargará sus dependencias.

Crear jar con dependencias con Maven

Se basa un poco en la filosofía de ANT, usar un fichero xml de configuración, en este caso se llamará pom.xml.

Como esto no pretende ser un tutorial completo sobre Maven, sino que pretendo tratar el problema concreto del empaquetado del proyecto, os dejo un buen tutorial sobre maven para iniciarse con ello.

Problema: Configurar Maven para crear el paquete con las dependencias

Cuando el proyecto ya estaba completado solo faltaba distribuirlo. Para ello lo más cómodo es crear un jar con las dependencias incluidas dentro del mismo, así no habrá que distribuir las dependencias por separado.

Conseguir esto me ha llevado un par de noches de madrugada y algo de desesperación. Los problemas que he ido encontrando para solucionar completamente el problema han sido varios, y la documentación que he encontrado no era clara o no era completa. Por ello he pensado en hacer esta guía para que a otros no les pase lo mismo.

El proyecto en concreto ha dado varios problemas. En primer lugar es un proyecto hecho con Swing y con el asistente de Netbeans, el cual usa Matisse. Esto ya fue un problema ya que, aún estando la librería necesaria (swing-layaout.jar) dentro del jar no funcionaba, al ejecutar el proyecto se obtenía esta excepción.

Exception in thread "main" java.lang.NoClassDefFoundError: org/jdesktop/layout/GroupLayout$Group
	at org.directgeoreference.App.main(App.java:39)
Caused by: java.lang.ClassNotFoundException: org.jdesktop.layout.GroupLayout$Group

Otro problema fue el usar el conjunto de librerías de GeoTools que definen varios módulos en META-INF/services, de manera que al juntar todos los META-INF de las librerías usadas en un único jar estas carpetas se perdían. Y por lo tanto no funcionaba.

El proyecto también usa la librería JAI (Java Advanced Imaging) y al guardar un archivo PNG en disco también tenía otra excepción porque le faltaban datos al MANIFEST.MF del jar sobre el proveedor de la máquina virtual de Java.

Exception in thread "AWT-EventQueue-0" java.util.ServiceConfigurationError: javax.imageio.spi.ImageWriterSpi: Provider com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi could not be instantiated: java.lang.IllegalArgumentException: vendorName == null!

El proyecto también tenía ficheros que no eran clases java como imágenes para la interfaz. Esto también provocó fallos, no se estaban incluyendo esos ficheros en el jar.

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at javax.swing.ImageIcon.<init>(ImageIcon.java:205)
	at org.directgeoreference.interfaz.Ventana.initComponents(Ventana.java:440)

Los problemas que me iba encontrando a resolver eran bastantes, cuando parecía que lo había conseguido aparecía uno más…

Los intentos han sido varios. El primero de ellos que encontré fue usar el plugin de maven freehep-jarjar, aquella solución no funcionó porque enseguida se hizo denotar el problema que ya he descrito al usar JAI.

El segundo intento fue usar el plugin maven-assembly, esta solución parecía bastante personalizable , aunque tediososo de configurar, teniendo que tener otro fichero de configuración aparte y además tenía bugs a la hora de incluir el classpath en el MANIFEST.MF, por no decir que tenía partes obsoletas.

Encontré un tutorial en inglés que me ayudó pero, me encontré el problema del Matisse de Netbeans y saltaba la excepción que ya os he enseñado.

Después de todos estos problemas di con la solución que a continuación os la explicaré.

La solución: Maven Shade Plugin y Maven Resources Plugin

Maven Shade Plugin

En una de mis numerosas búsquedas vi una web que venía de la documentación de Geotools. Especificaban el problema que ya os he comentado de la carpeta services y decían de usar el plugin Maven Shade.

Esta configuración del plugin era la solución, poniendo por supuesto la clase Main en el MANIFEST.MF para que al ejecutar el Jar se sepa que clase se debe iniciar.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>1.3.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <minimizeJar>true</minimizeJar>
                <transformers>
                    <!-- This bit sets the main class for the executable jar as you otherwise -->
                    <!-- would with the assembly plugin                                       -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Main-Class>org.directgeoreference.App</Main-Class>
                        </manifestEntries>
                    </transformer>
                    <!-- This bit merges the various GeoTools META-INF/services files         -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Con esto ya conseguí que se solucionasen los problemas de GeoTools y los problemas de la librería de Swing-layaout que ya os he comentado.

Maven Resources Plugin

Pero apareció otro el problema de los recursos, las imágenes y cualquier otro recurso archivo que no fuera un class no se había incluido en el jar. La interfaz no podía arrancar le faltaban las imágenes. Y además me di cuenta de que todos los ficheros de ayuda de JavaHelp tampoco se habían incluido ni un fichero de propiedades que también estaba usando.

La solución parecía ser que había que definir los recursos que usaba la aplicación, y para ello había que usar el plugin Maven Resources. Pero no es tan simple como parece, por defecto añade los ficheros a la raiz del fichero jar, y ese no es su sitio correcto. Además incluía también los propios ficheros fuente java sino marcabas su exclusión. La solución os la pongo a continuación con comentarios para solucionar los problemas que planteo. El código habrá que ponerlo dentro del build del pom.xml.

<build>
    <resources>
        <!-- Define un recurso -->
        <resource>
            <!-- Directorio del recurso dentro del proyecto Java -->
            <directory>src/main/java/org/directgeoreference/interfaz/resources</directory>
            <!-- Destino que indica donde se han de copiar los recursos dentro del jar -->
            <targetPath>org/directgeoreference/interfaz/resources</targetPath>
        </resource>
        <resource>
            <directory>src/main/java/org/directgeoreference/geo</directory>
            <!-- Excluyo todos los ficheros java para seleccionar solo un .properties -->
            <excludes>
                <exclude>**/*.java</exclude>
            </excludes>
            <targetPath>org/directgeoreference/geo</targetPath>
        </resource>
        <resource>
            <directory>src/main/java/org/directgeoreference/help</directory>
            <!-- Excluyo dos carpetas que hay dentro de help (bin y lib) -->
            <excludes>
                <exclude>bin/**</exclude>
                <exclude>lib/**</exclude>
            </excludes>
            <targetPath>org/directgeoreference/help</targetPath>
        </resource>
    </resources>
    <plugins>
        ...
    </plugins>
</build>

Con todo esto la aplicación ya arrancaba, pero al usar la función que guardaba un png apareció el problema que ya os he comentado de JAI, le faltaban datos en el MANIFEST.MF. La cuestión era saber cuales le faltaban. Para añadir los datos lo hacemos dentro de la sección del manifiesto del Maven Shade Plugin, debajo de donde pusimos la clase Main.

<manifestEntries>
    <Main-Class>org.directgeoreference.App</Main-Class>
    <Specification-Title>Java Advanced Imaging Image I/O Tools</Specification-Title>
    <Specification-Version>1.1</Specification-Version>
    <Specification-Vendor>Sun Microsystems, Inc.</Specification-Vendor>
    <Implementation-Title>com.sun.media.imageio</Implementation-Title>
    <Implementation-Version>1.1</Implementation-Version>
    <Implementation-Vendor>Sun Microsystems, Inc.</Implementation-Vendor>
    <Extension-Name>com.sun.media.imageio</Extension-Name>
</manifestEntries>

Después de añadir estas propiedades la aplicación arrancaba y funcionaba toda su funcionabilidad.

Fichero pom.xml completo

Una vez vistos todos los problemas que me surgieron y sus soluciones creo que sería interesante ver el pom.xml con todos los datos que hemos ido incluyendo en los diferentes pasos.

El pom.xml lo he dejado lo más comentado que he podido para que sirva de ayuda.

<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>org</groupId>
    <artifactId>DirectGeoreferenceMaven</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <name>DirectGeoreferenceMaven</name>
    <url>http://maven.apache.org</url>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <geotools.version>8.2</geotools.version>
        <src.dir>src/main/java/org/directgeoreference</src.dir>
    </properties>
 
    <build>
        <resources>
            <!-- Define un recurso -->
            <resource>
                <!-- Directorio del recurso dentro del proyecto Java -->
                <directory>src/main/java/org/directgeoreference/interfaz/resources</directory>
                <!-- Destino que indica donde se han de copiar los recursos dentro del jar -->
                <targetPath>org/directgeoreference/interfaz/resources</targetPath>
            </resource>
            <resource>
                <directory>src/main/java/org/directgeoreference/geo</directory>
                <!-- Excluyo todos los ficheros java para seleccionar solo un .properties -->
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
                <targetPath>org/directgeoreference/geo</targetPath>
            </resource>
            <resource>
                <directory>src/main/java/org/directgeoreference/help</directory>
                <!-- Excluyo dos carpetas que hay dentro de help (bin y lib) -->
                <excludes>
                    <exclude>bin/**</exclude>
                    <exclude>lib/**</exclude>
                </excludes>
                <targetPath>org/directgeoreference/help</targetPath>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <!-- Plugin para especificar la versión de la JVM con la que compilar -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <!-- Genera un jar pero sin empaquetar las dependencias -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>org.directgeoreference.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <!-- Plugin para ejecutar tareas de ant en este caso buscar y reemplazar antes de compilar -->
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.7</version>
                <executions>
                    <execution>
                        <phase>process-sources</phase>
                        <configuration>
                            <target>
                                <replace file="${src.dir}/interfaz/Ventana.java" token="panelImagen = new javax.swing.JPanel();" value="panelImagen = new PanelImagen();"/>
                                <replace file="${src.dir}/interfaz/Ventana.java" token="protected javax.swing.JPanel panelImagen;" value="protected PanelImagen panelImagen;"/>
                            </target>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <!-- Maven Shade Plugin para empaquetar las dependencias con la aplicacion -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>1.3.1</version>
                <executions>
                    <execution>
                        <!-- Define cuando se ha de ejecutar el plugin para que se lance cuando NetBeans construye el proyecto -->
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <minimizeJar>true</minimizeJar>
                            <transformers>
                                <!-- This bit sets the main class for the executable jar as you otherwise -->
                                <!-- would with the assembly plugin                                       -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Main-Class>org.directgeoreference.App</Main-Class>
                                        <Specification-Title>Java Advanced Imaging Image I/O Tools</Specification-Title>
                                        <Specification-Version>1.1</Specification-Version>
                                        <Specification-Vendor>Sun Microsystems, Inc.</Specification-Vendor>
                                        <Implementation-Title>com.sun.media.imageio</Implementation-Title>
                                        <Implementation-Version>1.1</Implementation-Version>
                                        <Implementation-Vendor>Sun Microsystems, Inc.</Implementation-Vendor>
                                        <Extension-Name>com.sun.media.imageio</Extension-Name>
                                    </manifestEntries>
                                </transformer>
                                <!-- This bit merges the various GeoTools META-INF/services files         -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>maven2-repository.dev.java.net</id>
            <name>Java.net repository</name>
            <url>http://download.java.net/maven/2/</url>
        </repository>
        <!-- Repositorios de Geotools ... -->
    </repositories>
 
    <dependencies>
        <!-- Dependencia de Swing-layaout (Matisse de Netbeans) -->
        <dependency>
            <groupId>org.swinglabs</groupId>
            <artifactId>swing-layout</artifactId>
            <version>1.0.3</version>
        </dependency>
        <!-- Dependencia de Java Help -->
        <dependency>
            <groupId>javax.help</groupId>
            <artifactId>javahelp</artifactId>
            <version>2.0.05</version>
        </dependency>
    </dependencies>
 
    <!-- Otras dependencias de Geo Tools ...-->
</project>

Espero que todo esto os haya servido de ayuda y no tengáis que pasar por la cantidad de problemas que me ha dado a mi generar el paquete jar.

Podéis dejar un comentario para cualquier duda que tengáis e intentaré resolverla.