DEV Community

Las interioridades de Maven

Hoy he perdido dos horas de mi precioso tiempo. Así es. Y todo porque Maven, a mi juicio, es peculiar.

Todo tiene que ver con uno de mis proyectos colaterales, llamado frover, un explorador de archivos construido con Java. Una cosa que me gusta de Java es que es muy, muy sencillo distribuirlo: solo tienes que hacerlo mediante un JAR. (De acuerdo, nadie o casi nadie lo va a usar excepto yo, pero si no es fácil de instalar, seguro que no lo usa nadie.)

¿Y por qué Maven como sistema de construcción de la app? Bueno, es una opción. Gradle siempre me ha parecido muy engorroso según mi experiencia con Android, pero bueno... Ant es un buen sistema de construcción, funciona bien, pero parece que ha sido relegado a proyectos antiguos o legacy. Así que Maven parecía la mejor opción.

Maven se utiliza a través de una herramienta de línea de comando, mvn, y un archivo POM.xml que describe las necesidades del proyecto.

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.devbaltasarq</groupId>
  <artifactId>frover</artifactId>
  <version>1</version>
  <packaging>jar</packaging>
  <properties>    
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    
    <maven.compiler.release>25</maven.compiler.release>
    <exec.mainClass>com.devbaltasarq.frover.Ppal</exec.mainClass>
  </properties>
  <name>frover</name>
  <description>A simple file manager.</description>
</project>
Enter fullscreen mode Exit fullscreen mode

¿Parece sencillo, no? Bueno, avancemos. El caso es que empiezas a crear la aplicación y la lanzas desde tu Netbeans (por ejemplo). De acuerdo, todo funciona, así que te planteas hacer dogfooding. Pero tu aplicación, es decir, tu JAR, no arranca. Extrañado, lo haces tú mismo desde la línea de comandos.

$ mvn package
$ java -jar target/frover-1.0.jar
no main manifest attribute, in frover-1.0.jar
Enter fullscreen mode Exit fullscreen mode

No funciona. Resulta que a nuestro JAR le falta un manifest... ¿Qué es esto? El archivo JAR necesita un archivo de texto especial llamado MANIFEST.MF, que debe estar dentro de un subdirectorio llamado META-INF. El contenido de este archivo de texto debería ser como sigue:

Manifest-Version: 1.0
Main-Class: com.devbaltasarq.frover.Ppal
Enter fullscreen mode Exit fullscreen mode

Los archivos JAR, no son más que archivos zip, en el fondo, así que...

$ unzip -v target/frover-1.0.jar                                                                                                                
Archive:  target/frover-1.0.jar
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2025-12-12 10:29 00000000  com/
       0  Stored        0   0% 2025-12-12 10:29 00000000  com/devbaltasarq/
       0  Stored        0   0% 2025-12-12 10:29 00000000  com/devbaltasarq/frover/
       0  Stored        0   0% 2025-12-12 10:29 00000000  com/devbaltasarq/frover/ui/
       0  Stored        0   0% 2025-12-12 10:29 00000000  com/devbaltasarq/frover/ui/mainwindow/
...

$ unzip -v target/frover-1.0.jar | grep -i manifest
$
Enter fullscreen mode Exit fullscreen mode

Pues es verdad... falta el archivo MANIFEST.MF. Es muy extraño, ¿no? Maven tiene ya todos los datos necesarios para crear este archivo... Al fin y al cabo, dentro del archivo XML, en properties.exec.mainClass tenemos el nombre de la clase principal. Está claro que esto lo necesita para ejecutar la aplicación (aunque es verdad que podría deducirlo de la existencia de un método public static void main() en alguna clase). Pero lo lógico es que, si tiene toda esta información, y además, está creando el JAR, también cree el archivo de manifiesto.

Quizás es que falta crear algún atributo dentro de properties, quizás algo así como <createManifest>true</createManifest>.

Pues resulta que no. Tenemos que incluir un plugin a Maven para que lo haga. Veamos:

      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.4.2</version>
            <configuration>
              <archive>
                <index>true</index>
                <manifest>
                  <mainClass>com.devbaltasarq.nottakapp.Ppal</mainClass>
                  <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                  <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                  <addClasspath>true</addClasspath>
                </manifest>
              </archive>
            </configuration>
          </plugin>
        </plugins>
  </build>
Enter fullscreen mode Exit fullscreen mode

Resulta que, para dar solo ese nuevo paso extra, necesitamos añadir un plugin, maven-jar-plugin. Al final, para configurarlo, necesitamos prácticamente una extensión equivalente a lo que ya teníamos en nuestro pom.xml. Incluso, ¡tenemos que repetir el nombre de la clase principal! Leyendo la documentación, podemos sustituir el nombre con la variable ${exec.mainClass}. A continuación, aparece el pom.xml entero:

<?xml version="1.0" encoding="UTF-8"?>
<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.devbaltasarq</groupId>
    <artifactId>frover</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>25</maven.compiler.release>
        <exec.mainClass>com.devbaltasarq.frover.Ppal</exec.mainClass>
    </properties>
    <name>frover</name>
    <description>A simple file manager.</description>
    <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.4.2</version>
            <configuration>
              <archive>
                <index>true</index>
                <manifest>
                  <mainClass>${exec.mainClass}</mainClass>
                  <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                  <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                  <addClasspath>true</addClasspath>
                </manifest>
              </archive>
            </configuration>
          </plugin>
        </plugins>
  </build>
</project>
Enter fullscreen mode Exit fullscreen mode

Bueno, pues menos mal. Ahora nuestro JAR ya funciona, aunque es decepcionante tener que introducir todo este código XML para que lo haga. Además, las posibilidades de interiorizar todo esto son nulas; no se trata de un simple atributo, sino de todo un nuevo plugin, con su propio esquema de multitud de atributos.

En fin, paciencia. El desarrollo continúa, y tras un tiempo decidí crear un archivo de configuración. Solo se trataría de un pequeño diccionario que grabaría como JSON, mediante la librería Gson de Google. Una de las características de Maven es que se pueden indicar dependencias fácilmente, de manera que vaya a buscarlas cuando sea necesario. Para esto, es necesario modificar el archivo (lo adivinaste), pom.xml, para que ahora incluya una nueva sección de dependencias.

    ...
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.13.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
    ...
Enter fullscreen mode Exit fullscreen mode

El scope es la fase del empaquetamiento de la app en la que la dependencia va a ser utilizada. Por ejemplo, puede ser test (cuando solo va a ser utilizada durante el testing), runtime (cuando solo va a ser utilizada en tiempo de ejecución), o compile, cuando se va a utilizar en todas las fases (de hecho, es la posibilidad por defecto, podríamos eliminar esta entrada).

Pues genial... el único problema es que ahora, nuestro JAR ya no funciona.

$ java -jar target/frover-1.0.jar
frover v0.1 20250812
User path: `/home/baltasarq`
Setting given path: `/home/baltasarq/Prys`
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/gson/Gson
        at com.devbaltasarq.frover.core.Config.restore(Config.java:164)
...
Enter fullscreen mode Exit fullscreen mode

Esta claro que, cuando la clase Config trata de utilizar Gson para convertir el diccionario de configuración en JSON, la aplicación falla porque no encuentra el JAR de Gson. Esto no tiene sentido. Las dependencias las maneja Maven por sí mismo.

Podemos comprobarlo fácilmente:

$ unzip -v target/frover-1.0.jar | grep -i gson
$
Enter fullscreen mode Exit fullscreen mode

No, es verdad. Definitivamente, Gson no forma parte del JAR creado.

Y lo peor... no fui capaz de encontrar nada por Internet. Aparentemente, esto solo me pasaba a mi. Ninguna respuesta pasaba más allá de incluir la entrada de <dependency> dentro de pom.xml.

En este momento me planteaba varias opciones.

  • Utilizar un formato propio en lugar de JSON, algo parecido a los antiguos archivos INI de Windows. Esto eliminaría la necesidad de utilizar la dependencia. No soluciona el problema, pero lo evita. Como se dice en mi país: "Y muerto el perro, se acabó la rabia." Esto nos lleva a tratar de no utilizar dependencias en absoluto, y finalmente, al síndrome Not invented here.

  • Descargar el JAR y colocarlo en el directorio de recursos. El problema es que esto implica introducir un binario en el código fuente, y ni siquiera está garantizado que no dé problemas.

  • Resolver el problema, pero utilizando IA. Es lo único que me quedaba por probar para encontrar soluciones (evitando así tener que leerme TOOOODA la documentación de Maven).

Opté por seguir luchando, y utilizando DeepSeek* (yo siempre llevando la contraria), le pegué el texto de mi pom.xml y la descripción del problema. Y... ¡sabía la respuesta! En su infinita sabiduría, los creadores de **Maven decidieron que, para introducir las dependencias dentro del JAR, sería necesario... otro plugin.

Huelga decir que para mi no tiene ningún sentido. Estás indicando que la dependencia se necesita en todas las fases, incluyendo runtime, pero oye, para llevarlo a cabo, apáñetelas tú como veas.

En cualquier caso, de hecho la IA me ofreció dos soluciones, es decir, dos plugins: uno llamado Shade (que era el recomendado), y otro que era el propio assembly de Maven. A pesar de las recomendaciones de la IA, decidí optar por el plugin de Maven. Como no me fío ni un pelo, coloqué ambos archivos pom.xml uno al lado del otro, y... allí, en el medio, estaba el nuevo plugin con varias referencias a Shade (?). Le afeé el atrevimiento a la IA, a lo que respondió dándome la razón (como siempre), y dándome, esta vez, la respuesta casi correcta (tuve que editar el archivo debido a otras alucinaciones que pude eliminar sin mayores consecuencias).

Pues aquí está el nuevo plugin a añadir.

            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.7.1</version>
                <configuration>
                    <descriptorRefs>
                        <!-- This creates a JAR with all dependencies -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>${exec.mainClass}</mainClass>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- Bind to the package phase -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
Enter fullscreen mode Exit fullscreen mode

Pues eso, otro plugin más. Aquí aparece el archivo pom.xml definitivo.

<?xml version="1.0" encoding="UTF-8"?>
<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.devbaltasarq</groupId>
    <artifactId>frover</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <exec.mainClass>com.devbaltasarq.frover.Ppal</exec.mainClass>
        <maven.compiler.plugin.version>3.14.0</maven.compiler.plugin.version>
        <maven.jar.plugin.version>3.4.2</maven.jar.plugin.version>
    </properties>

    <name>frover</name>
    <description>A simple file browser.</description>

    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.plugin.version}</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <release>25</release>
                </configuration>
            </plugin>

            <!-- Maven Jar Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>${maven.jar.plugin.version}</version>
                <configuration>
                    <archive>
                        <index>true</index>
                        <manifest>
                            <mainClass>${exec.mainClass}</mainClass>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                            <addClasspath>true</addClasspath>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!-- Maven Assembly Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.7.1</version>
                <configuration>
                    <descriptorRefs>
                        <!-- This creates a JAR with all dependencies -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>${exec.mainClass}</mainClass>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- Bind to the package phase -->
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.13.2</version>
        </dependency>
    </dependencies>
</project>
Enter fullscreen mode Exit fullscreen mode

Ah, sí, la IA señaló la necesidad de aún otro plugin: el del compilador. Por eso, en total, hay tres.

Recordemos que el objetivo es, tan solo, tener un JAR que funcione. Dicho de otra forma, las modificaciones son necesarias para poder tener un JAR que sirva. O dicho de otra manera, el 75% de los proyectos van a necesitar estos plugins, o una solución alternativa similar.

No soy un experto en Maven. Quizás lo que he mostrado aquí no son sino una cascada de meteduras de pata, y pueda resolverse mucho más fácilmente. O a lo mejor, es que espero demasiado de las herramientas. El caso es que me parece absurdo y esperpéntico. El XML necesario debería ser mucho más parecido al primero que aparece en este texto. Quizás debería haber un par de atributos más, digamos que en lugar de <packaging>jar</packaging> quizás debería ser algo así como:

    <packaging
         format="jar"
         includeManifest="true"
         includeDependencies="true" />
Enter fullscreen mode Exit fullscreen mode

O quizás una herramienta en la que, marcando unas opciones, o pasándole unos argumentos, se consiga lo mismo, pero yo la desconozco.

O a lo mejor es que soy raro, yo qué sé.

Top comments (0)