mavenLogo

Recientemente he estado trabajando en un proyecto Java en el cual he tenido que usar bastantes dependencias (librerías externas) y algunas de ellas tenían a su vez dependencias transitivas. En esta ocasión os contaré mi experiencia con el proyecto y haré una pequeña guía para lo que más me ha costado, que ha sido empaquetar todo el proyecto en un único jar ejecutable.

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, pero pronto me dí cuenta del problema de las dependencias transitivas de las librerías que estaba usando y me volví loco para ver cuales eran e ir añadiéndolas al classpath del proyecto.

Me dí cuenta de que 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.

Para aquellos que no lo sepan Maven es una herramienta de construcción automática de proyectos tal como lo puede ser ant (del que ya hicimos una guía hace tiempo), 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á las dependencias transitivas.

dependencias

Como podemos ver se basa un poco en la filosofía de ant de usar un fichero xml de configuración, solo que en este caso se llamará pom.xml.

Puesto que 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.

El problema: Empaquetar el proyecto con sus dependencias en un único jar

Cuando el proyecto ya estaba completado solo faltaba distribuirlo y para ello lo más cómo es poner todo lo necesario dentro de un único jar, de manera que no tengas que distribuir todas las dependencias por separado. Conseguir esto me ha llevado un par de noches de madrugada y algo de desesperación, no me ha parecido que la documentación que he encontrado haya sido demasiado clara y por ello una vez que lo he conseguido creo que puedo explicar como lo he hecho para que otros no pasen por lo mismo que he pasado yo.

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

Como podéis ver son bastantes los problemas que me he encontrado y que tenía que resolver, cuando parecía que lo había conseguido aparecía un problema 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 del 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

En una de mis numerosas búsquedas vi una web que venía de la documentación de Geotools (no estaba buscando específicamente el problema 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.

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 era tan fácil porque si solo los definías los añadía a la raiz del jar y no era ese su sitio y además si no los excluías también añadía los ficheros java. 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>

Bueno, la aplicación por lo menos ya arrancaba, pero cuando fui a 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.

Repasando …

Ahora que ya hemos visto 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. Y a la vez comentar que esto me ha funcionado en Mac y en Windows con la versión 1.7.0_09 de la máquina virtual de Java.

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 los mismos problemas que me ha dado a mi, que ya véis que han sido bastantes.

Digo lo mismo de siempre, cualquier duda que tengáis no dudéis en dejar un comentario y haré todo lo que esté en mi mano para intentar ayudar.

Sin más que decir me despido hasta la próxima publicación 🙂

Centro de preferencias de privacidad

Cookies imprescindibles

No se captura IPs ni siquiera para el servicio de Analytics así que tu visita es privada.

gdpr

Advertising

Analytics

Cookies de terceros

Usamos cookies de terceros con servicios, también garantes de tu privacidad, que analizan tus usos de navegación para que podamos mejorar los contenidos, si ya estás suscrito al boletín y los elementos compartidos en redes sociales y el formulario de comentarios.

_gid,_gat,_ga
1P_JAR, CONSENT, NID, OGPC
disqus_unique, disqusauth

Pin It on Pinterest

Share This