4. Maven#

Maven es un gestor de proyectos escritos en Java, esto es, una herramienta que automatiza la construcción y la gestión de aplicaciones escritas en Java, lo cual implica simplificar las tareas de creación, gestión de dependencias, compilación, pruebas, empaquetado y despliegue de la aplicación.

Para ello provee de una orden (mvn) provista de subórdenes que modularizan las distintas tareas de las que se encarga.

Nota

Visual Studio Code tiene una extensión que nos abstrae de todos estos comandos, por lo que este apéndice no tiene más interés qué tener una idea de qué es lo que subyace por debajo.

4.1. Creación#

Se impone una estructura estandarizada para el proyecto que podemos resumir en el siguiente gráfico:

+-- src
|    +-- main/
|    |     +-- java/edu/acceso/miapp/  (Código de la aplicación)
|    |     +-- resources/              Archivos de configuración, etc.
|    |
|    +-- test/                         Reproduce la estruct. de main para pruebas
|
+-- target/                            Código generado
+-- pom.xml                            Configuración del proyecto.

El subcomando asociado a esta fase es archetype:generate:

$ mvn archetype:generate -DgroupId=edu.acceso.test \
    -DartifactId=miapp
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -Dversion=1.0.0 \
    -DinteractiveMode=false

Pero lo habitual es que el IDE nos abstraiga de esta fase y se encargue el mismo de ejecutar la orden cuando le pidamos iniciar el proyecto.

4.2. Dependencias#

También facilita el gestor la tarea de obtener las dependencias de nuestro proyecto descargándolas directamente del Repositorio General de Maven. Basta incluirla en pom.xml. Por ejemplo:

<dependencies>
   <dependency>
       <groupId>jakarta.persistence</groupId>
       <artifactId>jakarta.persistence-api</artifactId>
       <version>3.2.0</version>
   </dependency>

   <!-- Otras dependencias -->
</dependencies>

La página tiene un buscador y puede buscarse a través de él la librería. La del ejemplo se haya en la dirección persistence-api, y escogiendo la versión deseada, se obtiene el código exacto que debe añadirse al archivo. Las librerías, además, pueden a su vez contener dependencias, pero se calculan si necesidad de incluirlas explícitamente.

La suborden asociada a la obtención de dependencias es:

$ mvn dependency:resolve
$ mvn dependency:tree         # Muestra el árbol de dependencias.

Sin embargo, no es necesario efectuar esta operación explícitamente, porque se realiza automáticamente al realizar operaciones posteriores.

4.3. Compilación#

La compilación, esto es, la generación de bytecode se realiza a través del plugin maven-compiler-plugin. Hacer la operación, en principio, es sencillo:

$ mvn compile                  # Genera el bytecode.
$ mvn clean compile            # Genera borrando antes el código generado previo

Ahora bien, hay distintas versiones de Java y, al respecto, el compilador (javac) necesita que se le indiquen dos cosas:

  1. Para qué versión de Java se realiza la comprobación estática de código.

  2. Para qué versión de Java se genera el bytecode[1].

En ausencia de configuración al respecto, el plugin le indica al compilador que use para ambas cosas la antigua version 1.7[2], por lo que es muy recomendable indicar explícitamente en el archivo pom.xml una versión algo más moderna, e indispensable si se van a usar características del lenguaje más modernas. Puede hacerse como una propiedad:

<properties>
   <maven.compiler.release>21</maven.compiler.release>
</properties>

o directamente configurando explíciamente el plugin (que en principio no necesita ser declarado):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.14.0</version>
            <configuration>
                <release>21</release>
            </configuration>
        </plugin>
    </plugins>
</build>

Consejo

Configurarlo como propiedad permite usar el valor declarado de la versión en otras partes del archivo, que necesiten indicar también la versión. Por ejemplo, al configurar el plugin para generar la documentación con Javadoc.

Nota

La opción --release que engloba ambos aspectos de la compilación apareció en Java 9. Antes no existía y se usaban en su lugar --source y --target (que, no obstante, siguen existiendo), por lo que si se quiere mantener compatibilidad con un JDK antiguo podemos hacer:

<properties>
   <maven.compiler.source>21</maven.compiler.source>
   <maven.compiler.target>21</maven.compiler.target>
</properties>

o la configuración correspondiente directamente en el plugin.

4.4. Pruebas#

El código que prueba la aplicación debe escribirse bajo src/test/ con la misma estructura que se uso en src/main/.

Las subórdenes asociadas a esta fases son:

$ mvn test              # Compila todo el código (principal y pruebas) y ejecuta las pruebas
$ mvn test-compile      # Sólo compila las pruebas sin llegar a ejecutarlas.

4.5. Empaquetado#

El software compilado suele distribuirse en formato JAR, un archivo zip que contiene las clases compiladas (archivos .class) y algunos otros archivos adicionales. Para crearlo:

$ mvn clean package

Para la generación del paquete, Maven usa el plugin maven-jar-plugin, que no incluye dependencias. Para más información sobre ello y cómo incluirlas consulte este gist sobre generación de paquetes.

4.6. Arquetipos#

Al crear un nuevo proyecto, Maven define su estructura inicial tomando como referencia lo que se llama un arquetipo (o ninguno). Como es probable que para resolver las tareas del módulo partamos siempre de una misma base, nos ahorrará mucho tiempo crear un arquetipo personal, así que pongámonos a ellos.

Lo primero es crear un arquetipo en un directorio:

$ mvn archetype:generate \
   -DgroupId=edu.acceso.arquetipo \
   -DartifactId=acceso_datos
   -DarchetypeArtifactId=maven-archetype-archetype \  # Arquetipo base
   -DarchetypeVersion=1.5 \         # Versión del arquetipo base
   -Dversion=1.0 \                  # Versión de este nuevo arquetipo.
   -DinteractiveMode=false

Esto definirá la estructura básica del arquetipo en el directorio acceso_datos:

+-- pom.xml   (pom.xml del propio arquetipo)
|
+-- src/
     +-- main/resources/
     |               +-- archetype-resources/
     |                                    +-- pom.xml  (pom.xml del proyecto)
     |                                    +-- src/ ...
     |               +-- META-INF/maven/
     |                               +-- archetype-metadata.xml
     +-- pom.xml
     +-- test/ ...

Como no nos interesan las pruebas podemos eliminar src/test/ y debemos centrarmos en:

  1. El pom.xml del propio arquetipo.

  2. El contenido de src/main/resources/archetype-resources/ que contiene los archivos que contendrá nuestro proyecto.

  3. El archivo src/main/resources/META-INF/maven/archetype-metadata.xml que contiene la configuración para generar el nuevo proyecto.

En consecuencia, todos los archivos que queramos que acaben en nuestro proyecto deberán incluirse dentro de la segunda ubicación; y en el tercer archivo deberemos definir la configuración:

archetype-metadata.xml#
<?xml version="1.0" encoding="UTF-8"?>
<archetype-descriptor name="acceso-datos"
  xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.1.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.1.0 http://maven.apache.org/xsd/archetype-descriptor-1.1.0.xsd">
  
  <fileSets>
     <fileSet filtered="true" packaged="false">
        <directory></directory>
        <includes>
           <include>README.md</include>
           <include>LICENSE</include>
        </includes>
     </fileSet>
     <fileSet filtered="true" packaged="false">
        <directory>.vscode</directory>
        <includes>
           <include>**/*.json</include>
        </includes>
     </fileSet>
     <fileSet filtered="true" packaged="true">
        <directory>src/main/java</directory>
        <includes>
           <include>**/*.java</include>
        </includes>
     </fileSet>
  </fileSets>
  
  <requiredProperties>
    <requiredProperty key="groupId" />
    <requiredProperty key="artifactId" />
    <requiredProperty key="version">
       <defaultValue>1.0.0</defaultValue>
    </requiredProperty>
    <requiredProperty key="dollar">
       <defaultValue>$</defaultValue>
    </requiredProperty>
    <requiredProperty key="hash">
       <defaultValue>#</defaultValue>
    </requiredProperty>
  </requiredProperties>
</archetype-descriptor>

En la configuración tenemos dos partes fundamentales:

  1. Por un lado declaramos los archivos que queremos que se incluyan en el proyecto. En la configuración hay tres bloques de archivos. La indicación filtered significa que queremos que el plugin analice el archivo en busca de propiedades para que las sustituya por su valor; y packaged que queremos que se añada la declaración de paquete: package edu.acceso.paquete...; al comienzo del archivo.

  2. Por otro, definimos cuáles son las propiedades y los valores por defecto que tienen. Por ejemplo, la variable artifactId podrá referirse en los archivos como ${artifactId} y, en ese caso, se sustituirá por el valor asignado.

Advertencia

Hay archivos como .gitignore que el plugin no incluirá en el proyecto aunque lo creemos.

Pues bien, una vez creado el arquetipo deberíamos retocar su contenido para ajustarlo a nuestras necesidades.

Nota

Se incluye un arquetipo preparado que puede descargar, modificar a su gusto e instalar.

Una vez que tengamos preparado el arquetipo, debemos acceder a su directorio e instalarlo:

$ cd acceso_datos/
$ mvn clean install

Truco

Con clean nos aseguramos de que cualquier compilación anterior del arquetipo no interfiera.

Instalado, podemos comprobar que realmente lo está y registrarlo para que el IDE nos permita seleccionarlo:

$ ls ~/.m2/repository/edu/acceso/arquetipo/acceso_datos/  # Si instalado, existirá.
$ mvn archetype:crawl                                     # Lo registramos.
$ cat ~/.m2/repository/archetype-catalog.xml              # Comprobamos el registro.

En caso de que quisiéramos eliminarlo podríamo eliminar a mano:

$ rm -rf ~/.m2/repository/edu/acceso/arquetipo/acceso_datos/
$ mvn archetype:crawl

En principio, podríamos probar a generar un nuevo proyecto sin llegar a usar el IDE:

$ mvn archetype:generate \
    -DarchetypeGroupId=edu.acceso.arquetipo \
    -DarchetypeArtifactId=acceso_datos \
    -DarchetypeVersion=1.0 \          # Versión de arquetipo (en su pom.xml)
    -DgroupId=edu.acceso.test_test \
    -DartifactId=test_test \
    -DinteractiveMode=false

Prudencia

Téngase presente que hay dos números de versión: la versión del propio arquetipo que es la que se indica aquí y que se define en el archivo pom.xml del propio arquetipo (el que se encuentra en el directorio raíz, acceso_datos en nuestro ejemplo) y la versión de la aplicación que pretendemos desarrollar, que se puede proporcionar mediante -Dversion=, pero que no hemos fijado y hemos preferido indicar en el archivo archetype-metadata.xml a través de la propiedad correspondiente.

También podremos crear el proyecto a través del IDE siempre que nos hayamos asegurado de que el arquetipo se registró.

Notas al pie