Módulo 01: Maven y Gradle

Objetivos del módulo

  • Entender qué problemas resuelve una herramienta de construcción y por qué ningún proyecto profesional se compila “a mano”
  • Instalar Maven y crear proyectos a partir de arquetipos
  • Conocer la estructura estándar de directorios y cada sección del pom.xml
  • Dominar el ciclo de vida de Maven, sus fases y los plugins más usados
  • Gestionar dependencias: scopes, repositorios, dependencias transitivas, conflictos, exclusiones y BOM
  • Organizar proyectos multimódulo, usar perfiles y el Maven Wrapper
  • Construir el mismo proyecto con Gradle y su DSL de Kotlin
  • Trabajar con Maven y Gradle desde IntelliJ IDEA
  • Crear de principio a fin un proyecto real con dependencias de Maven Central

1. ¿Qué es una herramienta de construcción y por qué la necesitas?

Hasta ahora has compilado con javac o con el botón verde del IDE. Funciona con dos clases. Pero imagina un proyecto real: 400 clases, pruebas automáticas, 30 bibliotecas externas (cada una con sus propias dependencias), recursos, un .jar final que hay que publicar… y un equipo de 8 personas que debe obtener exactamente el mismo resultado en su ordenador, en el servidor de integración continua y en producción.

Sin herramienta, tendrías que hacer algo así:

# Descargar a mano jackson-databind, jackson-core, jackson-annotations, commons-csv, commons-io...
# (¿qué versiones? ¿de dónde? ¿y las dependencias de las dependencias?)
javac -d target/classes -cp "lib/jackson-databind-3.2.3.jar:lib/jackson-core-3.2.3.jar:lib/..." $(find src -name "*.java")
cp -r src/main/resources/* target/classes/
javac -d target/test-classes -cp "target/classes:lib/junit-...:lib/assertj-..." $(find test -name "*.java")
java -cp "..." org.junit.platform.console.ConsoleLauncher ...
jar --create --file target/app.jar --main-class es.curso.Main -C target/classes .

📘 Concepto: Una herramienta de construcción (build tool) automatiza todo el proceso que convierte el código fuente en un producto entregable: descargar dependencias, compilar, copiar recursos, ejecutar las pruebas, empaquetar, generar documentación, publicar… a partir de un fichero de configuración que forma parte del proyecto y se guarda en Git.

Tarea Sin herramienta Con Maven o Gradle
Conseguir bibliotecas Descargar .jar a mano, buscar sus dependencias Declaras groupId:artifactId:version y se descargan solas, con sus dependencias
Compilar Comando javac con un classpath kilométrico mvn compile
Ejecutar las pruebas Montar el classpath de test y lanzar JUnit mvn test
Empaquetar jar con las opciones correctas mvn package
Reproducibilidad “En mi máquina funciona” Mismo resultado en cualquier equipo
Integración con el IDE Configurar cada IDE a mano IntelliJ, Eclipse y VS Code leen el pom.xml

1.1. Un poco de historia

Herramienta Año Idea principal
Ant 2000 Un build.xml con tareas imperativas (“compila esto, copia aquello”). Sin gestión de dependencias
Maven 2004 Convención sobre configuración: estructura estándar, ciclo de vida fijo y gestión de dependencias con repositorios. Configuración declarativa en XML (pom.xml)
Gradle 2012 Lo mejor de ambos: convenciones de Maven + flexibilidad de un lenguaje de programación (Groovy o Kotlin) para describir la construcción. Construcción incremental y caché

Hoy Maven y Gradle se reparten prácticamente todo el mercado. Maven domina en aplicaciones empresariales y en la mayoría de proyectos Spring Boot; Gradle es obligatorio en Android y muy popular en proyectos grandes. En este curso usamos Maven como herramienta principal y en la sección 13 verás el mismo proyecto con Gradle.

💡 Consejo: Aunque el IDE ejecute Maven por ti, debes saber usarlo desde la terminal. Los servidores de integración continua (GitHub Actions, GitLab CI, Jenkins) y los Dockerfile construyen con mvn o ./mvnw, sin IDE.


2. Instalación de Maven

Maven es un programa Java: necesita un JDK instalado y la variable JAVA_HOME bien configurada. La versión estable actual es la 3.9.16 (mayo de 2026). Maven 4 está en fase de release candidate (4.0.0-rc-7 en septiembre de 2026) y todavía no es la versión recomendada para producción; todo lo que aprendas aquí sirve también para Maven 4.

Windows:

  1. Descarga el fichero apache-maven-3.9.16-bin.zip de maven.apache.org/download.cgi.
  2. Descomprímelo, por ejemplo en C:\herramientas\apache-maven-3.9.16.
  3. Añade C:\herramientas\apache-maven-3.9.16\bin a la variable de entorno Path (Configuración → Sistema → Información → Configuración avanzada del sistema → Variables de entorno).
  4. Abre una terminal nueva y comprueba:
mvn -v

Linux y macOS con SDKMAN!:

sdk install maven
mvn -v

Con Docker (sin instalar nada, como se han probado los ejemplos de este módulo):

docker run --rm -v "$PWD:/w" -v m2:/root/.m2 -w /w maven:3.9-eclipse-temurin-25 mvn -v

El volumen m2 conserva el repositorio local entre ejecuciones para no descargar las dependencias cada vez. La salida de mvn -v:

Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /usr/share/maven
Java version: 25.0.4.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.15.167.4-microsoft-standard-wsl2", arch: "amd64", family: "unix"

⚠️ Importante: Fíjate en la línea Java version: Maven compila con el JDK que indica JAVA_HOME, no necesariamente con el que tengas en el PATH. Si aparece una versión antigua, corrige JAVA_HOME.

💡 Consejo: IntelliJ IDEA trae Maven integrado (bundled), así que puedes trabajar desde el IDE sin instalarlo. Y en cuanto un proyecto tenga el Maven Wrapper (sección 11), ni siquiera hace falta tenerlo instalado para usarlo desde la terminal.


3. Tu primer proyecto: arquetipos

📘 Concepto: Un arquetipo (archetype) es una plantilla de proyecto Maven. El arquetipo maven-archetype-quickstart genera un proyecto Java mínimo con una clase App, una prueba y su pom.xml.

mvn archetype:generate \
    -DarchetypeGroupId=org.apache.maven.archetypes \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.5 \
    -DgroupId=es.curso -DartifactId=hola-maven -Dversion=1.0-SNAPSHOT \
    -Dpackage=es.curso -DinteractiveMode=false

(En PowerShell escribe el comando en una sola línea o usa el acento grave ` en lugar de \ para continuar.) Si omites -DinteractiveMode=false, Maven te pregunta los datos uno por uno. Salida real (resumida):

[INFO] Generating project in Batch mode
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/archetype-catalog.xml
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/archetype-catalog.xml (18 MB at 11 MB/s)
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.5
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: es.curso
[INFO] Parameter: artifactId, Value: hola-maven
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: es.curso
[INFO] Parameter: junitVersion, Value: 5.11.0
[INFO] Parameter: javaCompilerVersion, Value: 17
[INFO] Project created from Archetype in dir: /w/hola-maven
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Ficheros generados:

hola-maven/
├── .mvn/
│   ├── jvm.config
│   └── maven.config
├── pom.xml
└── src/
    ├── main/java/es/curso/App.java
    └── test/java/es/curso/AppTest.java

App.java es un “Hello World” y AppTest.java una prueba JUnit 5 que siempre pasa (assertTrue(true)). El pom.xml generado es este:

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <modelVersion>4.0.0</modelVersion>

  <groupId>es.curso</groupId>
  <artifactId>hola-maven</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>hola-maven</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>5.11.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- (también junit-jupiter-params) -->
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <!-- versiones fijadas de clean 3.4.0, resources 3.3.1, compiler 3.13.0, surefire 3.3.0,
           jar 3.4.2, install, deploy, site y project-info-reports -->
    </pluginManagement>
  </build>
</project>

(Se han resumido los bloques repetitivos marcados con comentarios en español.)

⚠️ Importante: Los arquetipos envejecen: este, el más reciente (1.5), genera Java 17 y JUnit 5.11. En cuanto lo generes, cambia maven.compiler.release a 25, actualiza la versión del BOM de JUnit (6.1.3 en octubre de 2026) y las de los plugins. Es lo primero que hacemos en el proyecto completo de la sección 12.

Construimos y ejecutamos:

cd hola-maven
mvn package
java -cp target/classes es.curso.App
[INFO] ------------------------< es.curso:hola-maven >-------------------------
[INFO] Building hola-maven 1.0-SNAPSHOT
[INFO] --- resources:3.3.1:resources (default-resources) @ hola-maven ---
[INFO] --- compiler:3.13.0:compile (default-compile) @ hola-maven ---
[INFO] Compiling 1 source file with javac [debug release 17] to target/classes
[INFO] --- resources:3.3.1:testResources (default-testResources) @ hola-maven ---
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ hola-maven ---
[INFO] --- surefire:3.3.0:test (default-test) @ hola-maven ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.4.2:jar (default-jar) @ hola-maven ---
[INFO] Building jar: /w/target/hola-maven-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
Hello World!

(Salida resumida; en la sección 6.2 tienes una completa y comentada.)

Lee la salida despacio: con una sola orden (package), Maven ha copiado recursos, compilado el código, compilado las pruebas, ejecutado las pruebas y creado el .jar. Cada línea --- plugin:versión:objetivo --- es un paso. Lo entenderás del todo en la sección 6.

💡 Consejo: Hay miles de arquetipos (mvn archetype:generate sin parámetros los lista), pero en la práctica casi nadie los usa más allá del quickstart: los proyectos Spring Boot se crean con Spring Initializr (módulo 18) y los demás, desde el asistente del IDE (sección 15).


4. Estructura estándar de directorios

📘 Concepto: Maven sigue el principio de convención sobre configuración: si colocas cada cosa donde Maven espera, no tienes que configurar nada. Cualquier programador Java que abra tu proyecto sabe dónde está cada fichero.

Estructura estándar de un proyecto Maven con pom.xml, src/main/java, src/main/resources, src/test y target

Figura 1. Estructura estándar de directorios de un proyecto Maven.

Ruta Contenido ¿Va en el .jar?
pom.xml Descriptor del proyecto Se incluye una copia en META-INF/maven/
src/main/java Código fuente de la aplicación, organizado por paquetes Sí (compilado a .class)
src/main/resources Ficheros de configuración, plantillas, CSV, imágenes… Sí, tal cual, en la raíz del classpath
src/test/java Pruebas (JUnit) No
src/test/resources Recursos solo para pruebas No
target/ Todo lo que genera Maven: clases, informes, .jar Es la salida; no se sube a Git

⚠️ Importante: Añade siempre target/ al .gitignore. Y nunca edites nada dentro de target: se borra con mvn clean y se regenera en cada construcción.

Para leer un recurso de src/main/resources desde el código, se usa el classpath, no una ruta de disco (cuando la aplicación esté empaquetada en un .jar, el fichero ya no estará en tu disco):

try (InputStream in = MiClase.class.getResourceAsStream("/empleados.csv")) { ... }

5. El pom.xml explicado

El POM (Project Object Model) describe el proyecto. Vamos a estudiar el pom.xml completo del proyecto que construiremos en la sección 12: una utilidad que lee los empleados de la base de datos EMPRESA desde un CSV y genera un informe en JSON. Usa tres bibliotecas reales: Jackson 3 (JSON), Apache Commons CSV y Apache Commons Lang.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 1. Coordenadas GAV: identifican el proyecto de forma única -->
    <groupId>es.curso.empresa</groupId>
    <artifactId>informe-empleados</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <!-- 2. Información descriptiva -->
    <name>Informe de empleados</name>
    <description>Lee los empleados de un CSV y genera un informe JSON por departamento</description>

    <!-- 3. Propiedades: valores reutilizables y configuración de plugins -->
    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <main.class>es.curso.empresa.InformeEmpleados</main.class>

        <jackson.version>3.2.3</jackson.version>
        <commons-lang3.version>3.21.0</commons-lang3.version>
        <commons-csv.version>1.14.1</commons-csv.version>
        <junit.version>6.1.3</junit.version>
        <assertj.version>3.27.7</assertj.version>
    </properties>

    <!-- 4. Gestión de versiones: BOM de JUnit (las dependencias de JUnit no indican versión) -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>${junit.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 5. Dependencias -->
    <dependencies>
        <!-- Jackson 3: JSON (paquetes tools.jackson.*) -->
        <dependency>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- Utilidades para cadenas, números, etc. -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>
        <!-- Lectura de ficheros CSV -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-csv</artifactId>
            <version>${commons-csv.version}</version>
        </dependency>

        <!-- Solo para las pruebas -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- 6. Construcción: plugins y su configuración -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.16.0</version>
                <configuration>
                    <compilerArgs>
                        <arg>-Xlint:all</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.6.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>${main.class}</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.6.2</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <shadedArtifactAttached>true</shadedArtifactAttached>
                            <shadedClassifierName>completo</shadedClassifierName>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                        <exclude>META-INF/versions/*/module-info.class</exclude>
                                        <exclude>module-info.class</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.6.4</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

5.1. Coordenadas GAV

📘 Concepto: Todo artefacto Maven (tu proyecto y cada biblioteca) se identifica con sus coordenadas GAV: groupId, artifactId y version. Es como la dirección postal de un .jar en el mundo Maven: tools.jackson.core:jackson-databind:3.2.3 identifica un único fichero en todo Maven Central.

Elemento Qué es Convención Ejemplo
groupId Organización o grupo de proyectos Dominio invertido es.curso.empresa, org.apache.commons
artifactId Nombre del proyecto o módulo Minúsculas con guiones informe-empleados, commons-csv
version Versión Versionado semántico MAYOR.MENOR.PARCHE 1.0.0, 3.2.3
packaging Tipo de artefacto que se genera jar por defecto jar, war, pom
classifier Variante del mismo artefacto (opcional) — sources, javadoc, completo

Versiones SNAPSHOT. Una versión que termina en -SNAPSHOT (1.0-SNAPSHOT) indica “en desarrollo”: puede cambiar en cualquier momento y Maven comprueba periódicamente si hay una más nueva. Una versión sin SNAPSHOT (1.0.0) es una versión publicada y es inmutable: nunca debe cambiar su contenido.

Versionado semántico (SemVer): se incrementa el MAYOR cuando hay cambios incompatibles, el MENOR al añadir funcionalidad compatible y el PARCHE al corregir errores. Por eso pasar de Jackson 2 a Jackson 3 implica cambios en tu código (hasta los paquetes cambian), y de 3.2.2 a 3.2.3 no.

5.2. Propiedades

La sección <properties> define variables que se usan con la sintaxis ${nombre}. Sirven para dos cosas:

  1. Centralizar valores (versiones, nombre de la clase principal): cambias jackson.version en un sitio y afecta a todas las dependencias que la usen.
  2. Configurar plugins mediante propiedades estándar.
Propiedad Para qué sirve
maven.compiler.release Versión de Java para la que se compila (25). Sustituye a las antiguas maven.compiler.source y maven.compiler.target
project.build.sourceEncoding Codificación de los ficheros fuente. Ponla siempre a UTF-8
project.reporting.outputEncoding Codificación de los informes
skipTests true para no ejecutar las pruebas (se suele pasar en la línea de órdenes)

Además hay propiedades predefinidas: ${project.version}, ${project.artifactId}, ${project.basedir}, ${project.build.directory} (normalmente target), ${java.version}, ${user.home}, ${env.JAVA_HOME}…

⚠️ Importante: Usa maven.compiler.release y no el par source/target. release además comprueba que no uses API de versiones más nuevas que la indicada (por ejemplo, compilar con el JDK 25 para Java 17 y usar IO.println daría error de compilación, como debe ser).

5.3. Dependencias y scopes

Cada <dependency> declara una biblioteca por sus coordenadas. El ámbito (scope) indica en qué classpath debe estar:

Scope Compilar main Compilar y ejecutar test En ejecución ¿Se propaga (transitiva)? Ejemplo típico
compile (por defecto) ✔ ✔ ✔ Sí Jackson, Commons, Hibernate
provided ✔ ✔ ✘ (lo aporta el servidor) No API de Servlet en un .war, Lombok
runtime ✘ ✔ ✔ Sí Driver JDBC de MySQL
test ✘ ✔ ✘ No JUnit, AssertJ, Mockito
system ✔ ✔ ✘ No Un .jar local por ruta (desaconsejado)
import Solo en <dependencyManagement> con <type>pom</type>: importa un BOM       junit-bom, jackson-bom

💡 Consejo: El scope runtime es el adecuado para los drivers JDBC (módulo 12): tu código solo usa la API estándar java.sql, así que no debe compilar contra clases de MySQL; el driver solo hace falta al ejecutar. Así es imposible acoplarte por error a una clase interna del driver.

Otros elementos de <dependency>: <type> (por defecto jar), <classifier>, <optional>true</optional> (no se propaga a quien dependa de ti) y <exclusions> (sección 8).

5.4. Plugins

📘 Concepto: En Maven todo lo hace un plugin: compilar (maven-compiler-plugin), ejecutar pruebas (maven-surefire-plugin), crear el .jar (maven-jar-plugin)… Maven en sí solo sabe leer el POM, resolver dependencias y ejecutar plugins en el orden del ciclo de vida. Cada plugin ofrece uno o varios objetivos (goals), como compiler:compile o jar:jar.

Los plugins básicos se usan aunque no los declares (vienen del “super POM” de Maven con una versión por defecto). Declararlos sirve para fijar su versión (construcciones reproducibles) y para configurarlos:

Plugin Objetivo principal Configuración habitual
maven-compiler-plugin compile, testCompile release, compilerArgs (-Xlint:all, -parameters), procesadores de anotaciones
maven-surefire-plugin test Qué pruebas incluir o excluir, paralelismo, -DskipTests
maven-jar-plugin jar <archive><manifest><mainClass> para que java -jar sepa qué ejecutar
maven-shade-plugin shade Crea un fat jar o uber jar: tu código y todas las dependencias en un único .jar ejecutable
maven-assembly-plugin single Alternativa a shade: jar-with-dependencies, ZIP de distribución con scripts…
exec-maven-plugin java, exec Ejecutar la clase principal desde Maven: mvn exec:java
maven-resources-plugin resources Codificación, filtrado de recursos (sección 10)
maven-dependency-plugin tree, list, analyze, copy-dependencies Inspeccionar y copiar dependencias
maven-failsafe-plugin integration-test, verify Pruebas de integración (*IT.java), módulo 11
maven-enforcer-plugin enforce Obligar a una versión mínima de Java o de Maven, prohibir dependencias…

En nuestro POM:

  • compiler activa todas las advertencias del compilador con -Xlint:all.
  • jar escribe en el MANIFEST.MF la clase principal (Main-Class).
  • shade se engancha a la fase package (<execution><phase>package</phase>) y genera un segundo jar, con el clasificador completo, que incluye todas las dependencias. Los filtros excluyen firmas digitales de otros jars (que invalidarían el resultado) y los module-info.class de las dependencias.
  • exec permite mvn exec:java sin escribir el nombre de la clase.

⚠️ Importante: ¿Por qué hace falta shade? El .jar normal solo contiene tus clases. Si lo ejecutas con java -jar sin poner las dependencias en el classpath, falla en cuanto usa una clase de una biblioteca. Lo verás con una salida real en la sección 12.


6. El ciclo de vida y sus fases

📘 Concepto: Maven define ciclos de vida formados por fases ordenadas. Cuando pides una fase, Maven ejecuta todas las anteriores del mismo ciclo, y en cada fase ejecuta los objetivos de plugins asociados a ella. Hay tres ciclos: clean (borrar), default (construir) y site (documentación).

Fases del ciclo de vida default de Maven con los objetivos de plugin asociados a cada una

Figura 2. Ciclo de vida default de Maven para un proyecto de empaquetado jar.

Las fases principales del ciclo default (hay más intermedias, como process-resources o generate-sources):

Orden Fase Qué ocurre
1 validate Comprueba que el proyecto es correcto
2 compile Copia recursos y compila src/main/java en target/classes
3 test-compile Compila las pruebas en target/test-classes
4 test Ejecuta las pruebas unitarias con Surefire. Si una falla, la construcción se detiene
5 package Empaqueta en .jar (o .war)
6 verify Pruebas de integración y comprobaciones de calidad
7 install Copia el artefacto en tu repositorio local (~/.m2/repository) para que otros proyectos tuyos lo usen
8 deploy Lo sube a un repositorio remoto (Nexus, Artifactory, Maven Central)

6.1. Órdenes que usarás a diario

Orden Efecto
mvn compile Compila
mvn test Compila y ejecuta las pruebas
mvn package …y empaqueta
mvn clean package Borra target y construye desde cero (el ciclo clean primero, luego default hasta package)
mvn clean install Construye e instala en el repositorio local
mvn package -DskipTests Empaqueta sin ejecutar las pruebas (sí las compila)
mvn package -Dmaven.test.skip=true Ni las compila ni las ejecuta
mvn test -Dtest=EstadisticasTest Ejecuta solo una clase de prueba (-Dtest=Clase#metodo para un método)
mvn -q ... Modo silencioso: solo errores
mvn -o ... Modo offline: no accede a Internet
mvn -U ... Fuerza a comprobar actualizaciones de las versiones SNAPSHOT
mvn -X ... Modo depuración: muestra todo (útil cuando algo falla y no sabes por qué)

También puedes llamar a un objetivo directamente, sin fase: mvn dependency:tree, mvn exec:java, mvn help:effective-pom. La sintaxis es plugin:objetivo (o groupId:artifactId:versión:objetivo en su forma larga).

6.2. Salida real de mvn clean package

[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< es.curso.empresa:informe-empleados >-----------------
[INFO] Building Informe de empleados 1.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ informe-empleados ---
[INFO]
[INFO] --- resources:3.4.0:resources (default-resources) @ informe-empleados ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ informe-empleados ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 5 source files with javac [debug release 25] to target/classes
[INFO]
[INFO] --- resources:3.4.0:testResources (default-testResources) @ informe-empleados ---
[INFO] skip non existing resourceDirectory /m/informe-empleados/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ informe-empleados ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ informe-empleados ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running es.curso.empresa.EstadisticasTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 15.88 s -- in es.curso.empresa.EstadisticasTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] --- jar:3.5.1:jar (default-jar) @ informe-empleados ---
[INFO] Building jar: /m/informe-empleados/target/informe-empleados-1.0.0.jar
[INFO]
[INFO] --- shade:3.6.2:shade (default) @ informe-empleados ---
[WARNING] commons-codec-1.19.0.jar, commons-csv-1.14.1.jar, commons-io-2.20.0.jar, commons-lang3-3.21.0.jar, informe-empleados-1.0.0.jar, jackson-annotations-2.22.jar, jackson-core-3.2.3.jar, jackson-databind-3.2.3.jar define 1 overlapping resource:
[WARNING]   - META-INF/MANIFEST.MF
[WARNING] jackson-annotations-2.22.jar, jackson-core-3.2.3.jar, jackson-databind-3.2.3.jar define 2 overlapping resources:
[WARNING]   - META-INF/LICENSE
[WARNING]   - META-INF/NOTICE
[WARNING] maven-shade-plugin has detected that some files are present in two or more JARs. (...)
[INFO] Attaching shaded artifact.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  05:04 min
[INFO] Finished at: 2026-10-04T12:06:58Z
[INFO] ------------------------------------------------------------------------

Observaciones:

  • El formato de cada paso es --- plugin:versión:objetivo (id-de-ejecución) @ proyecto ---. default-compile es la ejecución por defecto; (default) en shade es la que declaramos sin <id>.
  • clean y resources aparecen con versiones que no hemos declarado (3.2.0 y 3.4.0): son las que fija Maven 3.9.16 por defecto. Para que la construcción no dependa de la versión de Maven instalada, en proyectos serios se fijan todas (o se hereda de un POM padre, como hace Spring Boot).
  • Las advertencias de shade sobre ficheros repetidos (LICENSE, MANIFEST.MF) son normales y, como dice el propio mensaje, normalmente inofensivas.
  • El tiempo total (5 minutos) incluye la descarga de todas las dependencias y plugins la primera vez. La siguiente construcción tarda segundos.

💡 Consejo: mvn help:effective-pom muestra el POM efectivo: el tuyo combinado con el super POM de Maven y los POM padres. Es la mejor forma de saber qué versión de cada plugin se usa realmente y de dónde sale cada configuración. Y para consultar un solo valor: mvn -q help:evaluate -Dexpression=project.version -DforceStdout imprime 1.0.0.


7. Repositorios: local, Maven Central y de empresa

Flujo de resolución de dependencias: proyecto, repositorio local ~/.m2 y Maven Central

Figura 3. Cómo obtiene Maven las dependencias.

📘 Concepto: Un repositorio Maven es un almacén de artefactos organizado por coordenadas. El repositorio local (~/.m2/repository, en Windows C:\Users\<tú>\.m2\repository) es una caché en tu equipo. Maven Central es el repositorio público por defecto, con prácticamente todas las bibliotecas Java de código abierto.

Las coordenadas se traducen en una ruta: tools.jackson.core:jackson-databind:3.2.3 está en

~/.m2/repository/tools/jackson/core/jackson-databind/3.2.3/jackson-databind-3.2.3.jar
https://repo.maven.apache.org/maven2/tools/jackson/core/jackson-databind/3.2.3/jackson-databind-3.2.3.jar

Junto al .jar se descarga su .pom, que lista sus dependencias: así funcionan las dependencias transitivas (Jackson Databind trae Jackson Core y Jackson Annotations sin que las declares).

Para buscar una biblioteca y copiar su <dependency>, usa central.sonatype.com o mvnrepository.com.

7.1. mvn install: tu proyecto en el repositorio local

mvn install -DskipTests
[INFO] --- install:3.1.4:install (default-install) @ informe-empleados ---
[INFO] Installing /m/informe-empleados/pom.xml to /root/.m2/repository/es/curso/empresa/informe-empleados/1.0.0/informe-empleados-1.0.0.pom
[INFO] Installing /m/informe-empleados/target/informe-empleados-1.0.0.jar to /root/.m2/repository/es/curso/empresa/informe-empleados/1.0.0/informe-empleados-1.0.0.jar
[INFO] Installing /m/informe-empleados/target/informe-empleados-1.0.0-completo.jar to /root/.m2/repository/es/curso/empresa/informe-empleados/1.0.0/informe-empleados-1.0.0-completo.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Desde ese momento, cualquier otro proyecto de tu equipo puede declarar es.curso.empresa:informe-empleados:1.0.0 como dependencia.

7.2. Repositorios adicionales y settings.xml

Las empresas suelen tener un repositorio propio (Nexus, Artifactory, GitHub Packages) que hace de proxy de Maven Central y aloja sus artefactos privados. Se configura en ~/.m2/settings.xml (configuración personal: credenciales, mirrors) o en el pom.xml (sección <repositories>):

<!-- ~/.m2/settings.xml -->
<settings>
    <mirrors>
        <mirror>
            <id>nexus-empresa</id>
            <mirrorOf>*</mirrorOf>                     <!-- todo pasa por el Nexus de la empresa -->
            <url>https://nexus.miempresa.es/repository/maven-public/</url>
        </mirror>
    </mirrors>
    <!-- y en <servers> las credenciales, con ${env.NEXUS_USER} y ${env.NEXUS_PASSWORD} -->
</settings>

⚠️ Importante: Nunca pongas contraseñas en el pom.xml: se sube a Git. Las credenciales van en settings.xml (fuera del proyecto) o en variables de entorno.

💡 Consejo: Si una construcción falla con errores extraños de dependencias (ficheros corruptos por una descarga interrumpida), borra la carpeta de esa biblioteca en ~/.m2/repository y vuelve a construir. Es el equivalente Maven de “apagar y encender”.


8. Dependencias transitivas, conflictos y gestión de versiones

8.1. Ver el árbol de dependencias

mvn dependency:tree
[INFO] --- dependency:3.7.0:tree (default-cli) @ informe-empleados ---
[INFO] es.curso.empresa:informe-empleados:jar:1.0.0
[INFO] +- tools.jackson.core:jackson-databind:jar:3.2.3:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
[INFO] |  \- tools.jackson.core:jackson-core:jar:3.2.3:compile
[INFO] +- org.apache.commons:commons-lang3:jar:3.21.0:compile
[INFO] +- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] |  +- commons-io:commons-io:jar:2.20.0:compile
[INFO] |  \- commons-codec:commons-codec:jar:1.19.0:compile
[INFO] +- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] |  +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[INFO] |  |  +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO] |  |  +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
[INFO] |  |  +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] |  |  \- org.jspecify:jspecify:jar:1.0.0:test
[INFO] |  +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
[INFO] |  \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO] |     \- org.junit.platform:junit-platform-engine:jar:6.1.3:test
[INFO] \- org.assertj:assertj-core:jar:3.27.7:test
[INFO]    \- net.bytebuddy:byte-buddy:jar:1.18.3:test

Hemos declarado 5 dependencias y el proyecto usa 18. Cada línea indica groupId:artifactId:tipo:versión:scope. Curiosidad: Jackson 3 sigue usando jackson-annotations de la rama 2 (paquete com.fasterxml.jackson.annotation), para que las anotaciones @JsonProperty y compañía sean compatibles entre las dos versiones mayores.

mvn dependency:list muestra la lista plana (con el nombre de módulo JPMS de cada jar) y mvn dependency:analyze detecta dependencias declaradas que no usas y dependencias usadas pero no declaradas (que te llegan de forma transitiva y conviene declarar).

8.2. Conflictos de versiones y mediación

¿Qué pasa si dos caminos del árbol llevan a versiones distintas de la misma biblioteca? En el classpath solo puede haber una. Maven aplica la regla de la más cercana gana (nearest wins): elige la versión que esté a menos saltos de tu proyecto y, si empatan, la primera declarada en el POM.

Proyecto de prueba con commons-text 1.15.0 (que depende de commons-lang3 3.20.0) y, además, commons-lang3 3.17.0 declarado directamente:

mvn dependency:tree -Dverbose
[INFO] es.curso:conflictos:jar:1.0.0
[INFO] +- org.apache.commons:commons-text:jar:1.15.0:compile
[INFO] |  \- (org.apache.commons:commons-lang3:jar:3.20.0:compile - omitted for conflict with 3.17.0)
[INFO] +- org.apache.commons:commons-lang3:jar:3.17.0:compile
[INFO] \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO]    +- commons-io:commons-io:jar:2.20.0:compile
[INFO]    \- commons-codec:commons-codec:jar:1.19.0:compile

Árbol de dependencias con un conflicto: commons-lang3 3.17.0 directa gana a la 3.20.0 transitiva

Figura 4. Mediación de versiones: gana la más cercana, aunque sea más antigua.

⚠️ Importante: Maven no elige la versión más alta, sino la más cercana. Aquí commons-text se compiló contra commons-lang3 3.20.0, pero en ejecución tendrá la 3.17.0. Si usa un método añadido en la 3.18, la aplicación compilará y arrancará… y fallará más tarde con NoSuchMethodError. Es uno de los errores más desconcertantes del mundo Java, y siempre se diagnostica con dependency:tree -Dverbose.

Para buscar una biblioteca concreta en un árbol grande: mvn dependency:tree -Dincludes=org.apache.commons:commons-lang3.

8.3. Exclusiones

Si no quieres una dependencia transitiva (porque no la usas, porque choca con otra o porque prefieres otra implementación), exclúyela:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.14.1</version>
    <exclusions>
        <exclusion>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </exclusion>
    </exclusions>
</dependency>

⚠️ Importante: Excluye solo lo que estés seguro de que no se usa en tu caso. Si la biblioteca necesita la clase excluida, obtendrás un NoClassDefFoundError en ejecución.

8.4. dependencyManagement: fijar versiones

La sección <dependencyManagement> no añade dependencias: fija la versión que se usará si la dependencia aparece, directa o transitivamente. Es la forma correcta de resolver conflictos:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.21.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>   <!-- sin versión: la toma de dependencyManagement -->
    </dependency>
</dependencies>

En el ejercicio 3 verás el árbol resultante.

8.5. BOM (Bill of Materials)

📘 Concepto: Un BOM es un POM especial que solo contiene un <dependencyManagement> con las versiones de un conjunto de artefactos que se publican juntos y están probados entre sí. Se importa con <type>pom</type> y <scope>import</scope>, y después declaras esas dependencias sin versión.

Lo has visto en nuestro proyecto con JUnit (org.junit:junit-bom:6.1.3): junit-jupiter, junit-jupiter-api, junit-jupiter-engine y junit-platform-* quedan alineados a la misma versión. Otros BOM habituales: tools.jackson:jackson-bom (Jackson 3), org.testcontainers:testcontainers-bom y, sobre todo, el de Spring Boot, que fija las versiones de cientos de bibliotecas (módulo 18).

8.6. Mantener las versiones al día

El plugin versions-maven-plugin informa de las actualizaciones disponibles:

mvn versions:display-dependency-updates
mvn versions:display-plugin-updates

En GitHub, Dependabot o Renovate abren automáticamente pull requests cuando sale una versión nueva de una dependencia.


9. Proyectos multimódulo

Cuando un proyecto crece, se divide en módulos: por ejemplo, empresa-modelo (entidades), empresa-servicio (lógica) y empresa-app (interfaz). Cada módulo es un proyecto Maven con su pom.xml, y un POM padre con <packaging>pom</packaging> los agrupa.

empresa-multi/
├── pom.xml                  ← padre (packaging pom)
├── empresa-modelo/
│   ├── pom.xml
│   └── src/main/java/es/curso/empresa/modelo/Departamento.java
└── empresa-app/
    ├── pom.xml              ← depende de empresa-modelo
    └── src/main/java/es/curso/empresa/app/Aplicacion.java

El POM padre cumple dos funciones distintas que conviene no confundir:

Función Elemento Qué consigue
Agregación <modules> Un mvn install en la raíz construye todos los módulos en el orden correcto (el reactor)
Herencia <parent> en cada hijo Los hijos heredan groupId, version, propiedades, dependencyManagement, pluginManagement y dependencias comunes
<!-- Fragmento del pom.xml padre -->
<groupId>es.curso.empresa</groupId>
<artifactId>empresa-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>

<modules>
    <module>empresa-modelo</module>
    <module>empresa-app</module>
</modules>
<!-- pom.xml de empresa-modelo: hereda groupId y version -->
<parent>
    <groupId>es.curso.empresa</groupId>
    <artifactId>empresa-parent</artifactId>
    <version>1.0.0</version>
</parent>
<artifactId>empresa-modelo</artifactId>

Al construir desde la raíz, Maven calcula el orden a partir de las dependencias entre módulos:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO] empresa-parent                                                     [pom]
[INFO] empresa-modelo                                                     [jar]
[INFO] empresa-app                                                        [jar]
[INFO] ... (se construyen los módulos en ese orden) ...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for empresa-parent 1.0.0:
[INFO] empresa-parent ..................................... SUCCESS [ 11.707 s]
[INFO] empresa-modelo ..................................... SUCCESS [05:21 min]
[INFO] empresa-app ........................................ SUCCESS [ 48.451 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Orden útil en multimódulo Efecto
mvn install (en la raíz) Construye todos los módulos
mvn -pl empresa-app package Solo el módulo indicado (project list)
mvn -pl empresa-app -am package El módulo y los que necesita (also make)
mvn -rf :empresa-app install Reanuda desde un módulo tras un fallo (resume from)

💡 Consejo: pluginManagement y dependencyManagement en el padre no aplican nada por sí solos: solo fijan versiones y configuración para cuando un hijo declare ese plugin o esa dependencia. Lo que pones en <dependencies> o <plugins> del padre, en cambio, lo heredan todos los hijos. Spring Boot funciona exactamente así: tu proyecto hereda de spring-boot-starter-parent.

El proyecto multimódulo completo (con un perfil y filtrado de recursos) es la solución del ejercicio 4.


10. Perfiles y filtrado de recursos

📘 Concepto: Un perfil (profile) es un bloque del POM que modifica la construcción solo cuando se activa: cambia propiedades, añade dependencias o plugins… Se usa para construir variantes: desarrollo y producción, con o sin pruebas lentas, para distintos sistemas operativos.

<properties>
    <entorno>desarrollo</entorno>
</properties>

<profiles>
    <profile>
        <id>prod</id>
        <properties>
            <entorno>producción</entorno>
        </properties>
    </profile>
</profiles>

Se activa con -P:

mvn package -Pprod
mvn help:active-profiles -Pprod    # comprueba qué perfiles están activos

Los perfiles también pueden activarse solos con <activation>: <activeByDefault>true</activeByDefault>, por una propiedad (<property><name>ci</name></property> se activa con -Dci), por la versión de JDK (<jdk>[25,)</jdk>), por sistema operativo (<os><family>windows</family></os>) o por la existencia de un fichero.

10.1. Filtrado de recursos

Con <filtering>true</filtering>, Maven sustituye las expresiones ${...} de los ficheros de recursos por el valor de las propiedades al copiarlos a target/classes:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
# src/main/resources/app.properties
app.nombre=${project.artifactId}
app.version=${project.version}
app.entorno=${entorno}

Al construir, el plugin de recursos avisa: “The encoding used to copy filtered properties files has not been set”. Desde la versión 3.2 del plugin, los .properties filtrados tienen su propio parámetro de codificación; para quitar la ambigüedad, configúralo en el POM padre:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.5.0</version>
    <configuration>
        <propertiesEncoding>UTF-8</propertiesEncoding>
    </configuration>
</plugin>

Resultado real en target/classes/app.properties al construir con -Pprod, y salida de la aplicación:

# target/classes/app.properties
app.nombre=empresa-app
app.version=1.0.0
app.entorno=producción

# salida de la aplicación
empresa-app 1.0.0 (entorno: producción)
[{"id":10,"nombre":"DIRECCIÓN","localidad":"Madrid"},{"id":30,"nombre":"INFORMÁTICA","localidad":"Valencia"}]

⚠️ Importante: En aplicaciones Spring Boot no se usan perfiles de Maven para separar desarrollo y producción, sino los perfiles de Spring (application-prod.yml, módulo 18), que se eligen al ejecutar y no al construir. Así el mismo .jar sirve para todos los entornos. Los perfiles de Maven quedan para variaciones de la construcción.


11. Maven Wrapper

📘 Concepto: El Maven Wrapper son unos pequeños scripts (mvnw para Linux/macOS y mvnw.cmd para Windows) y un fichero de configuración que se suben a Git con el proyecto. Al ejecutarlos, descargan (la primera vez) la versión exacta de Maven que fija el proyecto y la usan. Nadie necesita instalar Maven y todos usan la misma versión.

Se genera con:

mvn wrapper:wrapper
[INFO] --- wrapper:3.3.4:wrapper (default-cli) @ informe-empleados ---
[INFO] Unpacked only-script type wrapper distribution org.apache.maven.wrapper:maven-wrapper-distribution:zip:only-script:3.3.4
[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.9.16 and download from https://repo.maven.apache.org/maven2
[INFO] BUILD SUCCESS

Crea tres ficheros: mvnw (11 790 bytes, ejecutable), mvnw.cmd y .mvn/wrapper/maven-wrapper.properties, que fija la versión:

wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip

La primera vez que se ejecuta, ./mvnw descarga esa versión de Maven en ~/.m2/wrapper/dists y la usa:

$ ./mvnw -v
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /root/.m2/wrapper/dists/apache-maven-3.9.16/56ba1f9f
Java version: 25.0.4.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.15.167.4-microsoft-standard-wsl2", arch: "amd64", family: "unix"

A partir de ahora se usa ./mvnw (Linux, macOS, Git Bash) o mvnw.cmd / .\mvnw (Windows) en lugar de mvn:

./mvnw clean package
.\mvnw.cmd clean package

💡 Consejo: Los proyectos que genera Spring Initializr ya traen el wrapper. Súbelo siempre a Git (mvnw, mvnw.cmd y la carpeta .mvn/) y usa ./mvnw en los Dockerfile y en la integración continua. Si en Linux da “Permission denied”, ejecuta chmod +x mvnw (y guarda el permiso en Git con git update-index --chmod=+x mvnw).


12. Proyecto completo: informe de empleados

Ponemos todo junto. El programa lee empleados.csv (un extracto de la tabla empleados de la base de datos EMPRESA) con Commons CSV, agrupa por departamento, da formato a los nombres con Commons Lang y genera el informe en JSON con Jackson 3.

12.1. Código

src/main/resources/empleados.csv (extracto):

id,nombre,apellidos,puesto,salario,departamento
100,Ana,García López,DIRECTOR GENERAL,6500,DIRECCIÓN
101,Luis,Martínez Ruiz,DIRECTOR,4800,VENTAS
102,Marta,Sánchez Gil,DIRECTOR,5000,INFORMÁTICA
103,Pedro,Gómez Díaz,DIRECTOR,4200,CONTABILIDAD
110,Carmen,Ruiz Ortega,VENDEDOR,2200,VENTAS
...
131,Jorge,Vázquez Ramos,ADMINISTRATIVO,1650,CONTABILIDAD

Los dos records del modelo:

package es.curso.empresa;

/** Un empleado tal y como aparece en el fichero CSV. */
public record Empleado(int id, String nombre, String apellidos, String puesto,
                       double salario, String departamento) {
}
package es.curso.empresa;

import java.util.List;

/** Resultado del informe para un departamento (se serializa a JSON). */
public record ResumenDepartamento(String departamento, int empleados, double salarioMedio,
                                  double masaSalarial, List<String> plantilla) {
}

La lectura del CSV con Apache Commons CSV:

package es.curso.empresa;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;

/** Convierte un CSV con cabecera en una lista de empleados usando Apache Commons CSV. */
public final class LectorCsv {

    private LectorCsv() {
    }

    public static List<Empleado> leer(Reader entrada) throws IOException {
        CSVFormat formato = CSVFormat.DEFAULT.builder()
                .setHeader()                 // la primera línea es la cabecera
                .setSkipHeaderRecord(true)
                .get();
        List<Empleado> empleados = new ArrayList<>();
        for (CSVRecord fila : formato.parse(entrada)) {
            empleados.add(new Empleado(
                    Integer.parseInt(fila.get("id")),
                    fila.get("nombre"),
                    fila.get("apellidos"),
                    fila.get("puesto"),
                    Double.parseDouble(fila.get("salario")),
                    fila.get("departamento")));
        }
        return empleados;
    }
}

Los cálculos, con Apache Commons Lang (StringUtils):

package es.curso.empresa;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;

/** Cálculos del informe. */
public final class Estadisticas {

    private Estadisticas() {
    }

    /** Agrupa por departamento (ordenado alfabéticamente) y calcula los totales. */
    public static List<ResumenDepartamento> porDepartamento(List<Empleado> empleados) {
        Map<String, List<Empleado>> grupos = new TreeMap<>();
        for (Empleado e : empleados) {
            grupos.computeIfAbsent(e.departamento(), k -> new ArrayList<>()).add(e);
        }
        List<ResumenDepartamento> resumen = new ArrayList<>();
        for (var entrada : grupos.entrySet()) {
            List<Empleado> lista = entrada.getValue();
            double masa = 0;
            List<String> plantilla = new ArrayList<>();
            for (Empleado e : lista) {
                masa += e.salario();
                plantilla.add(nombreCorto(e));
            }
            double media = Math.round(masa / lista.size() * 100) / 100.0;
            resumen.add(new ResumenDepartamento(entrada.getKey(), lista.size(), media, masa, plantilla));
        }
        return resumen;
    }

    /** "Ana" + "García López" + "DIRECTOR GENERAL" -> "Ana García L. (Director general)". */
    public static String nombreCorto(Empleado e) {
        String[] partes = StringUtils.split(e.apellidos());
        String apellidos = partes.length > 1 ? partes[0] + " " + partes[1].charAt(0) + "." : partes[0];
        String puesto = StringUtils.capitalize(StringUtils.lowerCase(e.puesto()));
        return e.nombre() + " " + apellidos + " (" + puesto + ")";
    }
}

(En el módulo 04 reescribirás porDepartamento en tres líneas con streams y groupingBy.)

La clase principal, con Jackson 3 (JsonMapper está en tools.jackson.databind.json):

package es.curso.empresa;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

/** Punto de entrada: lee empleados.csv (del classpath) y escribe el informe en JSON. */
public class InformeEmpleados {

    public static void main(String[] args) throws IOException {
        List<Empleado> empleados;
        try (InputStream in = InformeEmpleados.class.getResourceAsStream("/empleados.csv");
             Reader lector = new InputStreamReader(in, StandardCharsets.UTF_8)) {
            empleados = LectorCsv.leer(lector);
        }
        System.out.println("Empleados leídos: " + empleados.size());

        List<ResumenDepartamento> informe = Estadisticas.porDepartamento(empleados);

        JsonMapper mapper = JsonMapper.builder()
                .enable(SerializationFeature.INDENT_OUTPUT)   // JSON con sangría
                .build();
        String json = mapper.writeValueAsString(informe.subList(0, 2));
        System.out.println(json);

        Path salida = Path.of(args.length > 0 ? args[0] : "informe.json");
        mapper.writeValue(salida.toFile(), informe);
        System.out.println("Informe completo guardado en " + salida.toAbsolutePath());
    }
}

📘 Concepto: En Jackson 3 los paquetes pasan de com.fasterxml.jackson.* a tools.jackson.*, el mapper se crea con un builder y es inmutable, y las excepciones (JacksonException) son no comprobadas: por eso writeValueAsString no obliga a un try/catch. Spring Boot 4 usa Jackson 3; en el módulo 08 lo estudiarás a fondo.

Y las pruebas (JUnit 6 + AssertJ) en src/test/java/es/curso/empresa/EstadisticasTest.java:

package es.curso.empresa;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import org.junit.jupiter.api.Test;

class EstadisticasTest {

    private static final String CSV = """
            id,nombre,apellidos,puesto,salario,departamento
            1,Ana,García López,DIRECTOR,3000,VENTAS
            2,Luis,Pérez,VENDEDOR,2000,VENTAS
            3,Eva,Ruiz Gil,ANALISTA,2500,INFORMÁTICA
            """;

    @Test
    void leeTodasLasFilasDelCsv() throws IOException {
        List<Empleado> empleados = LectorCsv.leer(new StringReader(CSV));
        assertThat(empleados).hasSize(3);
        assertThat(empleados.getFirst().salario()).isEqualTo(3000.0);
    }

    @Test
    void agrupaPorDepartamentoOrdenado() throws IOException {
        var resumen = Estadisticas.porDepartamento(LectorCsv.leer(new StringReader(CSV)));
        assertThat(resumen).extracting(ResumenDepartamento::departamento)
                .containsExactly("INFORMÁTICA", "VENTAS");
        ResumenDepartamento ventas = resumen.get(1);
        assertThat(ventas.empleados()).isEqualTo(2);
        assertThat(ventas.salarioMedio()).isEqualTo(2500.0);
        assertThat(ventas.masaSalarial()).isEqualTo(5000.0);
    }

    @Test
    void abreviaElSegundoApellido() {
        var e = new Empleado(1, "Ana", "García López", "DIRECTOR GENERAL", 1, "X");
        assertThat(Estadisticas.nombreCorto(e)).isEqualTo("Ana García L. (Director general)");
        var sinSegundo = new Empleado(2, "Luis", "Pérez", "VENDEDOR", 1, "X");
        assertThat(Estadisticas.nombreCorto(sinSegundo)).isEqualTo("Luis Pérez (Vendedor)");
    }
}

12.2. Construir y ejecutar

mvn clean package (salida completa en la sección 6.2) deja en target dos jars:

-rw-r--r-- 1 root root 4397527 Oct  4 12:06 informe-empleados-1.0.0-completo.jar
-rw-r--r-- 1 root root   10717 Oct  4 12:06 informe-empleados-1.0.0.jar

El normal ocupa 10 KB: solo tiene nuestras clases, el CSV y el manifiesto:

jar tf target/informe-empleados-1.0.0.jar
META-INF/
META-INF/MANIFEST.MF
es/
es/curso/
es/curso/empresa/
META-INF/maven/
META-INF/maven/es.curso.empresa/
META-INF/maven/es.curso.empresa/informe-empleados/
empleados.csv
es/curso/empresa/Empleado.class
es/curso/empresa/Estadisticas.class
es/curso/empresa/InformeEmpleados.class
es/curso/empresa/LectorCsv.class
es/curso/empresa/ResumenDepartamento.class
META-INF/maven/es.curso.empresa/informe-empleados/pom.xml
META-INF/maven/es.curso.empresa/informe-empleados/pom.properties
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 25
Build-Jdk-Spec: 25
Main-Class: es.curso.empresa.InformeEmpleados

Si lo ejecutamos, Java encuentra la clase principal gracias a Main-Class, pero no las bibliotecas:

java -jar target/informe-empleados-1.0.0.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/csv/CSVFormat
	at es.curso.empresa.LectorCsv.leer(LectorCsv.java:17)
	at es.curso.empresa.InformeEmpleados.main(InformeEmpleados.java:20)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.csv.CSVFormat
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
	... 2 more

El jar “completo” de 4,3 MB (generado por shade, con 2471 entradas) incluye también org/apache/commons/csv/..., tools/jackson/..., etc., y funciona en cualquier equipo con Java 25:

java -jar target/informe-empleados-1.0.0-completo.jar /tmp/informe.json
Empleados leídos: 13
[ {
  "departamento" : "CONTABILIDAD",
  "empleados" : 3,
  "salarioMedio" : 2816.67,
  "masaSalarial" : 8450.0,
  "plantilla" : [ "Pedro Gómez D. (Director)", "Isabel Domínguez V. (Contable)", "Jorge Vázquez R. (Administrativo)" ]
}, {
  "departamento" : "DIRECCIÓN",
  "empleados" : 1,
  "salarioMedio" : 6500.0,
  "masaSalarial" : 6500.0,
  "plantilla" : [ "Ana García L. (Director general)" ]
} ]
Informe completo guardado en /tmp/informe.json

Durante el desarrollo es más cómodo ejecutar con el plugin exec, que monta el classpath con las dependencias automáticamente:

mvn -q exec:java
mvn -q exec:java -Dexec.args="salida/informe.json"     # con argumentos

La salida es la misma de antes, con el informe guardado en la carpeta del proyecto.

💡 Consejo: ¿Fat jar o no? Para herramientas de línea de órdenes y para microservicios es lo más práctico (un único fichero). Spring Boot tiene su propio formato de jar ejecutable con las dependencias dentro (módulo 23). Otra alternativa es distribuir el jar normal junto a una carpeta lib/ con las dependencias (mvn dependency:copy-dependencies) y lanzar con java -cp "app.jar:lib/*" ....


13. Gradle

📘 Concepto: Gradle es una herramienta de construcción que adopta las convenciones de Maven (misma estructura src/main/java, mismos repositorios, mismas coordenadas) pero describe la construcción con código en lugar de XML: un DSL en Kotlin (build.gradle.kts, el recomendado) o en Groovy (build.gradle). Su unidad de trabajo es la tarea (task), y las tareas forman un grafo de dependencias.

La versión actual es Gradle 9.8.0 (septiembre de 2026). Gradle 9 requiere Java 17 o superior para ejecutarse.

13.1. Crear un proyecto con gradle init

gradle init --type java-application --dsl kotlin --test-framework junit-jupiter \
            --package es.curso --project-name hola-gradle --java-version 25 \
            --no-split-project --no-incubating --use-defaults
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :init
Learn more about Gradle by exploring our Samples at https://docs.gradle.org/9.8.0/samples/sample_building_java_applications.html

BUILD SUCCESSFUL in 4m 33s
1 actionable task: 1 executed

./app/build.gradle.kts
./app/src/main/java/es/curso/App.java
./app/src/test/java/es/curso/AppTest.java
./.gitattributes
./.gitignore
./gradle/libs.versions.toml
./gradle.properties
./gradlew
./gradlew.bat
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./settings.gradle.kts

Gradle separa el proyecto raíz (settings.gradle.kts) del subproyecto app (con su build.gradle.kts), y usa un catálogo de versiones (gradle/libs.versions.toml) para centralizar las versiones, de forma parecida a dependencyManagement. El app/build.gradle.kts generado es muy parecido al de la sección 13.2, pero declara las dependencias a través del catálogo de versiones (implementation(libs.guava), testImplementation(libs.junit.jupiter)). Ese catálogo, gradle/libs.versions.toml, es este:

# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format

[versions]
guava = "33.5.0-jre"
junit-jupiter = "6.0.1"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

⚠️ Importante: Igual que los arquetipos de Maven, gradle init puede generar versiones que no son las últimas (aquí Guava 33.5.0 y JUnit 6.0.1, cuando ya existen la 33.7.2 y la 6.1.3). Revísalas siempre.

13.2. build.gradle.kts del proyecto de informes

El mismo proyecto de la sección 12, ahora con Gradle (mismo src/, solo cambian los ficheros de construcción):

// settings.gradle.kts
rootProject.name = "informe-empleados"
// build.gradle.kts: el equivalente al pom.xml, escrito en Kotlin
plugins {
    application                       // añade las tareas run, installDist, distZip...
}

group = "es.curso.empresa"
version = "1.0.0"

repositories {
    mavenCentral()                    // el mismo repositorio que usa Maven
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

dependencies {
    implementation("tools.jackson.core:jackson-databind:3.2.3")
    implementation("org.apache.commons:commons-lang3:3.21.0")
    implementation("org.apache.commons:commons-csv:1.14.1")

    testImplementation(platform("org.junit:junit-bom:6.1.3"))   // BOM
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    testImplementation("org.assertj:assertj-core:3.27.7")
}

application {
    mainClass = "es.curso.empresa.InformeEmpleados"
}

tasks.test {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
    options.compilerArgs.add("-Xlint:all")
}

// Una tarea propia
tasks.register("saludar") {
    group = "curso"
    description = "Muestra un saludo con la versión del proyecto"
    val version = project.version
    doLast {
        println("Hola desde Gradle, versión $version")
    }
}

// "Fat jar" sin plugins externos: incluye las dependencias en el jar
tasks.register<Jar>("jarCompleto") {
    group = "build"
    description = "Genera un jar ejecutable con todas las dependencias"
    archiveClassifier = "completo"
    manifest { attributes["Main-Class"] = application.mainClass }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from(sourceSets.main.get().output)
    from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) {
        exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "**/module-info.class")
    }
}
Concepto Maven Equivalente Gradle
<groupId>, <version> group = "...", version = "..."
maven.compiler.release java { toolchain { languageVersion = JavaLanguageVersion.of(25) } }
<scope>compile</scope> implementation(...) (o api(...) en bibliotecas)
<scope>provided</scope> compileOnly(...)
<scope>runtime</scope> runtimeOnly(...)
<scope>test</scope> testImplementation(...) / testRuntimeOnly(...)
BOM con <scope>import</scope> implementation(platform("grupo:bom:versión"))
exec-maven-plugin Plugin application y tarea run
maven-shade-plugin Plugin externo Shadow (com.gradleup.shadow) o una tarea Jar propia como la anterior
mvn clean package gradle clean build

📘 Concepto: La diferencia entre implementation y api es importante en proyectos multimódulo: una dependencia implementation no se expone a quien dependa de tu módulo (no aparece en su classpath de compilación), lo que acelera la compilación y evita acoplamientos. api (del plugin java-library) sí se expone. Maven no tiene esta distinción: todo lo que es compile se propaga.

13.3. Tareas

gradle build
> Task :compileJava
> Task :processResources
> Task :classes
> Task :jar
> Task :startScripts
> Task :distTar
> Task :distZip
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test

EstadisticasTest > abreviaElSegundoApellido() PASSED

EstadisticasTest > leeTodasLasFilasDelCsv() PASSED

EstadisticasTest > agrupaPorDepartamentoOrdenado() PASSED

> Task :check
> Task :build

BUILD SUCCESSFUL in 33m 52s
8 actionable tasks: 8 executed

(Los casi 34 minutos se deben a la primera descarga de Gradle y de todas las dependencias en un equipo muy cargado; en condiciones normales son segundos.) Gradle muestra cada tarea (> Task :compileJava). La tarea build depende de assemble (compilar y empaquetar) y check (pruebas). Si vuelves a ejecutar gradle build sin cambiar nada, las tareas aparecen como UP-TO-DATE: Gradle detecta que sus entradas no han cambiado y no las repite (construcción incremental). Es una de sus grandes ventajas en proyectos grandes:

> Task :compileJava UP-TO-DATE
...
> Task :test UP-TO-DATE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE

BUILD SUCCESSFUL in 1m 21s
8 actionable tasks: 8 up-to-date

Otras tareas:

gradle run -q ejecuta la aplicación con la misma salida que mvn exec:java en la sección 12.2.

gradle saludar
> Task :saludar
Hola desde Gradle, versión 1.0.0

BUILD SUCCESSFUL in 35s
1 actionable task: 1 executed
gradle tasks              # lista las tareas disponibles
Application tasks
-----------------
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the classes of the 'main' feature.
jarCompleto - Genera un jar ejecutable con todas las dependencias
testClasses - Assembles test classes.

Curso tasks
-----------
saludar - Muestra un saludo con la versión del proyecto

(...siguen Build Setup, Distribution, Documentation, Help y Verification tasks...)
gradle dependencies --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- tools.jackson.core:jackson-databind:3.2.3
|    +--- com.fasterxml.jackson.core:jackson-annotations:2.22
|    +--- tools.jackson.core:jackson-core:3.2.3
|    |    \--- tools.jackson:jackson-bom:3.2.3
|    |         +--- com.fasterxml.jackson.core:jackson-annotations:2.22 (c)
|    |         +--- tools.jackson.core:jackson-core:3.2.3 (c)
|    |         \--- tools.jackson.core:jackson-databind:3.2.3 (c)
|    \--- tools.jackson:jackson-bom:3.2.3 (*)
+--- org.apache.commons:commons-lang3:3.21.0
\--- org.apache.commons:commons-csv:1.14.1
     +--- commons-io:commons-io:2.20.0
     \--- commons-codec:commons-codec:1.19.0

(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

Fíjate en que Gradle marca las dependencias repetidas con (*), que muestra como restricciones (c) las versiones que impone el BOM de Jackson (Jackson 3 lo declara como dependencia de sus propios módulos) y que, a diferencia de Maven, ante un conflicto elige la versión más alta.

gradle jarCompleto
ls -la build/libs
java -jar build/libs/informe-empleados-1.0.0-completo.jar | head -3
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :jarCompleto

BUILD SUCCESSFUL in 3m 23s
3 actionable tasks: 1 executed, 2 up-to-date
total 4268
drwxr-xr-x 1 root root    4096 Oct  4 12:55 .
drwxr-xr-x 1 root root    4096 Oct  4 12:43 ..
-rw-r--r-- 1 root root 4356161 Oct  4 12:57 informe-empleados-1.0.0-completo.jar
-rw-r--r-- 1 root root    8335 Oct  4 12:31 informe-empleados-1.0.0.jar
Empleados leídos: 13
[ {
  "departamento" : "CONTABILIDAD",

La salida de Gradle va a build/ (no a target/): clases en build/classes, jars en build/libs, informes de pruebas en build/reports/tests/test/index.html.

13.4. Gradle Wrapper

Como en Maven, el wrapper fija la versión de Gradle del proyecto. gradle init ya lo crea; en un proyecto existente se genera con:

gradle wrapper --gradle-version 9.8.0
cat gradle/wrapper/gradle-wrapper.properties
./gradlew --version
> Task :wrapper

BUILD SUCCESSFUL in 1m 19s
1 actionable task: 1 executed
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./gradlew
./gradlew.bat
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Fetching distribution.
Downloading https://services.gradle.org/distributions/gradle-9.8.0-bin.zip

------------------------------------------------------------
Gradle 9.8.0
------------------------------------------------------------

Kotlin:        2.4.10
Launcher JVM:  25.0.4.1 (Eclipse Adoptium 25.0.4.1+1-LTS)
Daemon JVM:    /opt/java/openjdk (no Daemon JVM specified, using current Java home)
OS:            Linux 5.15.167.4-microsoft-standard-WSL2 amd64

💡 Consejo: En Gradle el wrapper es aún más importante que en Maven, porque cada versión mayor de Gradle cambia cosas del DSL. Usa siempre ./gradlew, nunca un gradle instalado globalmente. Sube a Git gradlew, gradlew.bat y la carpeta gradle/wrapper/ (incluido el gradle-wrapper.jar).


14. Maven frente a Gradle

Aspecto Maven Gradle
Configuración XML declarativo (pom.xml) Código Kotlin o Groovy (build.gradle.kts)
Curva de aprendizaje Suave: todo proyecto es igual Más pronunciada: mucha flexibilidad
Modelo Ciclo de vida fijo con fases Grafo de tareas
Rendimiento Correcto Superior en proyectos grandes: construcción incremental, caché de construcción, demonio
Conflictos de versiones Gana la más cercana Gana la más alta
Separación API/implementación No Sí (api / implementation)
Personalización Mediante plugins (escribir uno propio es laborioso) Tareas propias en el propio fichero
Legibilidad del fichero Verboso pero predecible Conciso, aunque puede volverse críptico
Uso típico Aplicaciones empresariales, Spring Boot, ciclos formativos Android (obligatorio), proyectos grandes, Kotlin
Soporte en IDE y CI Excelente Excelente

💡 Consejo: No hay una opción “mejor”: domina Maven (es lo que más vas a encontrar en empresas Java y en este curso) y sé capaz de leer y modificar un build.gradle.kts. Spring Initializr te deja elegir cualquiera de los dos.


15. Maven y Gradle en IntelliJ IDEA

15.1. Crear un proyecto Maven

En File → New → Project… (o New Project en la pantalla de bienvenida) se abre el asistente:

Esquema del asistente New Project de IntelliJ IDEA con Build system Maven y JDK 25 seleccionados

Figura 5. Esquema del asistente New Project de IntelliJ IDEA (no es una captura).

  1. Name y Location: nombre de la carpeta del proyecto y dónde crearla. Debajo se muestra la ruta final.
  2. Language: Java.
  3. Build system: elige Maven (o Gradle; IntelliJ es el sistema propio del IDE, que no sirve fuera de él).
  4. JDK: el JDK 25. Si no aparece, el desplegable ofrece Download JDK… o Add JDK from Disk….
  5. Add sample code: crea una clase Main de ejemplo.
  6. Advanced Settings: despliégalo para escribir el GroupId y el ArtifactId.
  7. Create.

En el panel izquierdo, la sección Generators ofrece asistentes específicos: Maven Archetype (el archetype:generate de la sección 3), Spring Boot, Jakarta EE, JavaFX… Algunos, como Spring Boot o Jakarta EE, solo están disponibles con la suscripción de pago de IntelliJ IDEA (gratuita para estudiantes y docentes con la licencia educativa).

Si eliges Gradle, aparece además la opción Gradle DSL (Kotlin o Groovy); elige Kotlin.

15.2. La ventana Maven

Esquema de IntelliJ IDEA con el pom.xml abierto, el botón de recarga y la ventana Maven con Lifecycle, Plugins y Dependencies

Figura 6. Esquema del editor y la ventana de herramientas Maven de IntelliJ IDEA (no es una captura).

  1. Botón flotante de recarga (icono m con flechas): aparece en el editor cuando modificas el pom.xml. Púlsalo (o Ctrl+Mayús+O) para que el IDE descargue las nuevas dependencias y actualice el proyecto. Si no recargas, el IDE no “ve” las dependencias nuevas y marca los import en rojo.
  2. Reload All Maven Projects: lo mismo, desde la ventana Maven.
  3. Execute Maven Goal (m): abre un cuadro para escribir cualquier orden (clean package -DskipTests, dependency:tree…).
  4. Lifecycle: doble clic en una fase la ejecuta. La salida aparece en la ventana Run de abajo.
  5. Plugins: cada plugin con sus objetivos (dependency:tree, exec:java…).
  6. Dependencies: el árbol de dependencias, con las transitivas al desplegar.

La ventana se abre desde la barra lateral derecha (icono m) o con View → Tool Windows → Maven. Otros detalles útiles:

  • La barra de herramientas tiene un botón Toggle ‘Skip Tests’ Mode (equivale a -DskipTests) y Toggle Offline Mode.
  • Con el pom.xml abierto, Alt+Insert → Dependency busca bibliotecas en Maven Central y escribe el bloque <dependency>.
  • Clic derecho en el pom.xml → Maven → Show Diagram dibuja el grafo de dependencias (función de la edición de pago).
  • En Settings → Build, Execution, Deployment → Build Tools → Maven eliges qué Maven usar (Bundled o el wrapper) y su JDK (Runner → JRE).

Gradle tiene una ventana equivalente (icono de elefante) con las tareas agrupadas (build, application, verification, curso…) y el mismo botón de recarga al modificar build.gradle.kts.

15.3. Abrir un proyecto existente

File → Open y selecciona la carpeta del proyecto (o directamente su pom.xml / build.gradle.kts). IntelliJ detecta el sistema de construcción y lo importa. Nunca copies las carpetas .idea o *.iml de otro equipo: el proyecto se define por el pom.xml, y el IDE regenera su configuración.

💡 Consejo: Desde File → New → Project from Version Control… puedes clonar un repositorio de Git y el IDE lo importará como proyecto Maven o Gradle en un paso.


16. Buenas prácticas y errores comunes

Problema Causa Solución
package org.apache.commons... does not exist en el IDE No se ha recargado el proyecto tras editar el pom.xml Botón Load Maven Changes / Reload All Maven Projects
NoClassDefFoundError al hacer java -jar El jar no incluye las dependencias Fat jar (shade/assembly) o -cp con las dependencias
no main manifest attribute, in app.jar Falta Main-Class en el manifiesto Configura maven-jar-plugin (<mainClass>)
release version 25 not supported Maven usa un JDK antiguo Corrige JAVA_HOME (o el JDK de Maven en el IDE)
NoSuchMethodError en ejecución Conflicto de versiones: hay una versión más antigua de la esperada mvn dependency:tree -Dverbose y fija la versión con dependencyManagement
Tildes rotas en mensajes o recursos Codificación no declarada project.build.sourceEncoding=UTF-8
Could not resolve dependencies Coordenadas mal escritas, versión inexistente o sin red Comprueba en central.sonatype.com; mvn -U; revisa proxy/settings.xml
Construcción que funciona en tu equipo pero no en CI Versiones de plugins no fijadas o Maven distinto Fija versiones y usa el wrapper
Pruebas que fallan y bloquean test es parte de package Arregla la prueba; -DskipTests solo como recurso puntual

Buenas prácticas:

  • Fija la versión de todos los plugins y dependencias; nunca uses rangos ([1.0,2.0)) ni LATEST.
  • Centraliza versiones en <properties> y usa BOM cuando existan.
  • Usa el scope correcto: JUnit siempre test, drivers JDBC runtime.
  • Sube a Git el pom.xml y el wrapper; nunca target/ ni build/.
  • Revisa periódicamente las dependencias (versions:display-dependency-updates, Dependabot) y sus vulnerabilidades.

17. Ejercicios

Ejercicio 1: Utilidades de texto con Guava

Crea un proyecto Maven es.curso:texto-utils:1.0-SNAPSHOT para Java 25 con la dependencia Google Guava (com.google.guava:guava:33.7.2-jre) y el plugin exec-maven-plugin. Escribe una clase TextoUtils que use Splitter (trocear "Ana; Luis ;; Marta ;Pedro" recortando espacios y descartando vacíos), Joiner (unir ciudades saltándose los null), ArrayListMultimap (empleados por departamento) y CaseFormat (pasar FECHA_ALTA_EMPLEADO a camelCase). Ejecútala con mvn compile exec:java y muestra el árbol de dependencias.

Ver solución
<!-- pom.xml: coordenadas es.curso:texto-utils:1.0-SNAPSHOT y propiedades como en la sección 5 -->
<dependencies>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>33.7.2-jre</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>3.6.4</version>
            <configuration>
                <mainClass>es.curso.TextoUtils</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>
package es.curso;

import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.util.List;

public class TextoUtils {
    public static void main(String[] args) {
        String linea = "Ana;  Luis ;; Marta ;Pedro";
        // Splitter: trocea, recorta y descarta vacíos (String.split no hace nada de esto por sí solo)
        List<String> nombres = Splitter.on(';').trimResults().omitEmptyStrings().splitToList(linea);
        System.out.println("Splitter: " + nombres);

        // Joiner: une saltándose los null
        System.out.println("Joiner:   " + Joiner.on(", ").skipNulls().join("Madrid", null, "Valencia", "Sevilla"));

        // Multimap: una clave con varios valores (sin Map<String, List<String>>)
        Multimap<String, String> porDepartamento = ArrayListMultimap.create();
        porDepartamento.put("VENTAS", "Carmen");
        porDepartamento.put("VENTAS", "David");
        porDepartamento.put("INFORMÁTICA", "Raquel");
        System.out.println("Multimap: " + porDepartamento + " · VENTAS tiene " + porDepartamento.get("VENTAS").size());

        // CaseFormat: convierte entre convenciones de nombres
        System.out.println("CaseFormat: " + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "FECHA_ALTA_EMPLEADO"));
    }
}
mvn -q compile exec:java
mvn dependency:tree
Splitter: [Ana, Luis, Marta, Pedro]
Joiner:   Madrid, Valencia, Sevilla
Multimap: {VENTAS=[Carmen, David], INFORMÁTICA=[Raquel]} · VENTAS tiene 2
CaseFormat: fechaAltaEmpleado

[INFO] es.curso:texto-utils:jar:1.0-SNAPSHOT
[INFO] \- com.google.guava:guava:jar:33.7.2-jre:compile
[INFO]    +- com.google.guava:failureaccess:jar:1.0.3:compile
[INFO]    +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile
[INFO]    +- org.jspecify:jspecify:jar:1.0.1:compile
[INFO]    +- com.google.errorprone:error_prone_annotations:jar:2.50.0:compile
[INFO]    \- com.google.j2objc:j2objc-annotations:jar:3.1:compile

Guava trae cinco dependencias transitivas pequeñas (anotaciones y utilidades). Con exec:java no hace falta ningún fat jar: el plugin monta el classpath con todas ellas.

Ejercicio 2: Fat jar con maven-assembly-plugin

Sustituye en el proyecto de informes el maven-shade-plugin por maven-assembly-plugin (versión 3.8.0) con el descriptor predefinido jar-with-dependencies, de forma que se ejecute en la fase package y el jar resultante sea ejecutable con java -jar.

Ver solución
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifest>
                <mainClass>${main.class}</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>jar-completo</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>
mvn package -DskipTests
ls -la target/*.jar
java -jar target/informe-empleados-1.0.0-jar-with-dependencies.jar /tmp/x.json | head -3
[INFO] Building jar: /m/ej2-assembly/target/informe-empleados-1.0.0.jar
[INFO] --- assembly:3.8.0:single (jar-completo) @ informe-empleados ---
[INFO] Building jar: /m/ej2-assembly/target/informe-empleados-1.0.0-jar-with-dependencies.jar
[INFO] BUILD SUCCESS
-rw-r--r-- 1 root root   10618 Oct  4 13:06 target/informe-empleados-1.0.0.jar
-rw-r--r-- 1 root root 4358420 Oct  4 13:07 target/informe-empleados-1.0.0-jar-with-dependencies.jar
Empleados leídos: 13
[ {
  "departamento" : "CONTABILIDAD",

El jar se llama como el proyecto con el sufijo -jar-with-dependencies. Diferencias con shade: assembly es más general (también crea ZIP o TAR de distribución con scripts y ficheros de configuración mediante un descriptor propio), mientras que shade está especializado en fat jars y además sabe combinar ficheros que varias bibliotecas comparten (por ejemplo, los META-INF/services de ServiceLoader) y renombrar paquetes (relocation) para evitar conflictos. Para aplicaciones con muchas dependencias, shade es más seguro.

Ejercicio 3: Resolver un conflicto de versiones

En el proyecto conflictos de la sección 8.2, commons-text necesita commons-lang3 3.20.0 pero gana la 3.17.0 declarada directamente. Corrígelo para que todo el proyecto use la 3.21.0 mediante dependencyManagement, y excluye además commons-codec (que trae commons-csv y no se usa). Comprueba el resultado con dependency:tree -Dverbose.

Ver solución
<!-- Una sola versión de commons-lang3 para todo el proyecto, la usen directa o transitivamente -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.21.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>1.15.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>   <!-- sin versión: la toma de dependencyManagement -->
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-csv</artifactId>
        <version>1.14.1</version>
        <exclusions>
            <!-- no queremos commons-codec en el proyecto -->
            <exclusion>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>
[INFO] es.curso:conflictos:jar:1.0.0
[INFO] +- org.apache.commons:commons-text:jar:1.15.0:compile
[INFO] |  \- (org.apache.commons:commons-lang3:jar:3.21.0:compile - version managed from 3.20.0; omitted for duplicate)
[INFO] +- org.apache.commons:commons-lang3:jar:3.21.0:compile
[INFO] \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO]    \- commons-io:commons-io:jar:2.20.0:compile

Ahora la transitiva de commons-text aparece como version managed from 3.20.0 (la versión la decide dependencyManagement, no la mediación) y commons-codec ha desaparecido del árbol. Si commons-lang3 no estuviera declarada directamente, dependencyManagement seguiría imponiendo la 3.21.0 a la transitiva: esa es su gran utilidad.

Ejercicio 4: Proyecto multimódulo con perfiles

Crea el proyecto empresa-multi de la sección 9: un padre empresa-parent (packaging pom) que fije Java 25, centralice con dependencyManagement la versión del módulo de modelo, el BOM de Jackson 3 (tools.jackson:jackson-bom:3.2.3) y el BOM de JUnit, y fije en pluginManagement las versiones de los plugins. Dos módulos: empresa-modelo (record Departamento y una prueba) y empresa-app (lee app.properties filtrado y muestra los departamentos en JSON). Añade un perfil prod que cambie la propiedad entorno.

Ver solución

pom.xml del padre:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>es.curso.empresa</groupId>
    <artifactId>empresa-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>                 <!-- el padre no genera .jar -->

    <modules>
        <module>empresa-modelo</module>
        <module>empresa-app</module>
    </modules>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <entorno>desarrollo</entorno>
    </properties>

    <!-- Versiones centralizadas: los hijos declaran la dependencia SIN versión -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>es.curso.empresa</groupId>
                <artifactId>empresa-modelo</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>tools.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>3.2.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.1.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- Dependencias que heredan TODOS los módulos -->
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.16.0</version>
                </plugin>
                <!-- surefire 3.6.0, jar 3.5.1 y resources 3.5.0, igual que el compiler
                     (a resources conviene añadirle propertiesEncoding: sección 10.1) -->
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.6.4</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <!-- Perfiles: configuraciones alternativas que se activan con -P -->
    <profiles>
        <profile>
            <id>prod</id>
            <properties>
                <entorno>producción</entorno>
            </properties>
        </profile>
    </profiles>
</project>

empresa-modelo/pom.xml, su record y su prueba:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>es.curso.empresa</groupId>
        <artifactId>empresa-parent</artifactId>
        <version>1.0.0</version>
    </parent>
    <artifactId>empresa-modelo</artifactId>   <!-- groupId y version se heredan -->
</project>
package es.curso.empresa.modelo;

/** Departamento de la base de datos EMPRESA. */
public record Departamento(int id, String nombre, String localidad) {
}

(La prueba DepartamentoTest comprueba con assertEquals que dos departamentos con los mismos datos son iguales.)

empresa-app/pom.xml:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>es.curso.empresa</groupId>
        <artifactId>empresa-parent</artifactId>
        <version>1.0.0</version>
    </parent>
    <artifactId>empresa-app</artifactId>

    <dependencies>
        <!-- Otro módulo del mismo proyecto: la versión viene del dependencyManagement -->
        <dependency>
            <groupId>es.curso.empresa</groupId>
            <artifactId>empresa-modelo</artifactId>
        </dependency>
        <!-- Versión gestionada por el BOM de Jackson -->
        <dependency>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <build>
        <!-- Filtrado: sustituye ${...} en los recursos por el valor de las propiedades -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <configuration>
                    <mainClass>es.curso.empresa.app.Aplicacion</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

empresa-app/src/main/resources/app.properties es el de la sección 10.1, y la clase principal:

package es.curso.empresa.app;

import es.curso.empresa.modelo.Departamento;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Properties;
import tools.jackson.databind.json.JsonMapper;

public class Aplicacion {
    public static void main(String[] args) throws IOException {
        Properties config = new Properties();
        try (InputStream in = Aplicacion.class.getResourceAsStream("/app.properties")) {
            config.load(new InputStreamReader(in, StandardCharsets.UTF_8));   // el fichero está en UTF-8
        }
        System.out.printf("%s %s (entorno: %s)%n", config.getProperty("app.nombre"),
                config.getProperty("app.version"), config.getProperty("app.entorno"));

        List<Departamento> departamentos = List.of(
                new Departamento(10, "DIRECCIÓN", "Madrid"),
                new Departamento(30, "INFORMÁTICA", "Valencia"));
        System.out.println(JsonMapper.builder().build().writeValueAsString(departamentos));
    }
}
mvn install                                   # en la raíz: construye los dos módulos
mvn -q -pl empresa-app exec:java              # perfil por defecto
mvn -q -Pprod package -DskipTests
mvn -q -pl empresa-app -Pprod exec:java       # perfil prod
empresa-app 1.0.0 (entorno: desarrollo)
[{"id":10,"nombre":"DIRECCIÓN","localidad":"Madrid"},{"id":30,"nombre":"INFORMÁTICA","localidad":"Valencia"}]
empresa-app 1.0.0 (entorno: producción)
[{"id":10,"nombre":"DIRECCIÓN","localidad":"Madrid"},{"id":30,"nombre":"INFORMÁTICA","localidad":"Valencia"}]

Fíjate en tres detalles: la dependencia de empresa-app sobre empresa-modelo no lleva versión (la da el padre); jackson-databind tampoco (la da el BOM importado); y Properties.load recibe un Reader UTF-8, porque con un InputStream lee en ISO-8859-1 y “producción” saldría con caracteres extraños.

Ejercicio 5: El proyecto de informes con Gradle

Convierte el proyecto de informes de la sección 12 a Gradle con DSL de Kotlin: mismo código en src/, settings.gradle.kts, build.gradle.kts con el plugin application, toolchain de Java 25, las mismas dependencias (con el BOM de JUnit como platform), una tarea saludar propia en el grupo curso y una tarea jarCompleto que genere un fat jar. Ejecuta gradle build, gradle run y el jar.

Ver solución

La solución completa es el build.gradle.kts de la sección 13.2 (más settings.gradle.kts con rootProject.name = "informe-empleados"), y las salidas reales están en la sección 13.3. Los puntos clave son el plugin application con su mainClass, el toolchain de Java 25, el BOM de JUnit con platform(...) y esto:

dependencies {
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
    useJUnitPlatform()          // sin esto, Gradle no ejecuta las pruebas de JUnit 5/6
}

Dos trampas frecuentes al pasar de Maven a Gradle: olvidar useJUnitPlatform() (las pruebas “pasan” porque no se ejecuta ninguna) y olvidar testRuntimeOnly("org.junit.platform:junit-platform-launcher"), que Gradle 9 exige para lanzar JUnit Platform. En la tarea saludar, project.version se lee fuera de doLast (en la fase de configuración) para que la tarea sea compatible con la caché de configuración de Gradle.

Ejercicio 6: Interpretar una prueba que falla

En una copia del proyecto de informes, cambia en EstadisticasTest el valor esperado del salario medio de VENTAS de 2500.0 a 2400.0, ejecuta mvn test y explica la salida: ¿qué prueba falla, en qué línea, qué esperaba y qué obtuvo? ¿Se ha generado el jar? ¿Dónde están los informes detallados?

Ver solución
mvn test
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running es.curso.empresa.EstadisticasTest
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 26.93 s <<< FAILURE! -- in es.curso.empresa.EstadisticasTest
[ERROR] es.curso.empresa.EstadisticasTest.agrupaPorDepartamentoOrdenado -- Time elapsed: 2.633 s <<< FAILURE!
org.opentest4j.AssertionFailedError: 

expected: 2400.0
 but was: 2500.0
	at es.curso.empresa.EstadisticasTest.agrupaPorDepartamentoOrdenado(EstadisticasTest.java:33)

[INFO] Results:
[ERROR] Failures: 
[ERROR]   EstadisticasTest.agrupaPorDepartamentoOrdenado:33 
expected: 2400.0
 but was: 2500.0
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
  • Falla EstadisticasTest.agrupaPorDepartamentoOrdenado, en la línea 33 (la del isEqualTo(2400.0)).
  • AssertJ explica la diferencia: esperaba 2400.0 y el valor real era 2500.0. Las otras dos pruebas pasan (Tests run: 3, Failures: 1).
  • La construcción termina en BUILD FAILURE en la fase test, así que no se llega a package: no hay jar. Es exactamente lo que queremos: código que no pasa sus pruebas no se empaqueta.
  • Los informes detallados están en target/surefire-reports/ (un .txt y un .xml por clase de prueba); el XML es lo que leen los servidores de integración continua.
  • Para ejecutar solo esa prueba mientras la corriges: mvn test -Dtest=EstadisticasTest#agrupaPorDepartamentoOrdenado.

Ejercicio 7 (propuesto): Biblioteca y aplicación

Crea una biblioteca Maven es.curso:validaciones:1.0.0 con una clase Validador (métodos estáticos para validar NIF, email y código postal, con sus pruebas JUnit). Instálala con mvn install. Después crea otro proyecto independiente es.curso:alta-clientes que la use como dependencia y valide los clientes de la tabla clientes de EMPRESA. Comprueba en ~/.m2/repository/es/curso/validaciones qué ficheros se han instalado.

Ejercicio 8 (propuesto): Perfil de pruebas lentas

Al proyecto de informes, añade pruebas “lentas” en clases cuyo nombre termine en LentoTest. Configura maven-surefire-plugin para excluirlas por defecto (<excludes>) y crea un perfil todas que las incluya. Comprueba con mvn test y mvn test -Ptodas cuántas pruebas se ejecutan en cada caso.

Ejercicio 9 (propuesto): maven-enforcer-plugin

Añade al proyecto de informes el plugin maven-enforcer-plugin (versión 3.6.3) con las reglas requireJavaVersion (Java 25 o superior), requireMavenVersion (3.9 o superior) y dependencyConvergence (que falle si hay conflictos de versiones). Prueba la última regla añadiendo commons-text 1.15.0 y commons-lang3 3.17.0 como en la sección 8.2: ¿qué mensaje da?

Ejercicio 10 (propuesto): Ejecutable con dependencias en lib/

Sin usar shade ni assembly, configura el proyecto de informes para que mvn package copie las dependencias a target/lib (maven-dependency-plugin, objetivo copy-dependencies) y el manifiesto del jar incluya un Class-Path con ellas (<addClasspath>true</addClasspath> y <classpathPrefix>lib/</classpathPrefix> en maven-jar-plugin). Comprueba que java -jar target/informe-empleados-1.0.0.jar funciona si la carpeta lib está al lado.

Ejercicio 11 (propuesto): Multimódulo con Gradle

Reproduce el proyecto multimódulo del ejercicio 4 con Gradle: settings.gradle.kts con include("empresa-modelo", "empresa-app"), un catálogo de versiones en gradle/libs.versions.toml y en empresa-app una dependencia implementation(project(":empresa-modelo")). Usa el plugin java-library en el modelo y explica qué cambiaría si declarases Jackson con api en lugar de implementation.

Ejercicio 12 (propuesto): Proyecto desde IntelliJ IDEA

Crea desde el asistente New Project de IntelliJ IDEA un proyecto Maven con JDK 25, GroupId es.curso.empresa y ArtifactId consultas-empresa. Añade con Alt+Insert → Dependency la biblioteca com.mysql:mysql-connector-j (búscala en Maven Central y elige el scope adecuado, justificándolo) y recarga el proyecto. Ejecuta package desde la ventana Maven, activa el modo Skip Tests y comprueba en la ventana Run que Surefire ya no ejecuta pruebas. Guárdalo: lo usarás en el módulo 12.


Resumen

Concepto Maven Gradle
Fichero de construcción pom.xml build.gradle.kts + settings.gradle.kts
Coordenadas groupId:artifactId:version (GAV) group, name, version
Compilar para Java 25 <maven.compiler.release>25</maven.compiler.release> java { toolchain { languageVersion = JavaLanguageVersion.of(25) } }
Dependencia normal / test <scope>compile</scope> / <scope>test</scope> implementation(...) / testImplementation(...)
Construir mvn clean package ./gradlew clean build
Ejecutar mvn exec:java ./gradlew run
Árbol de dependencias mvn dependency:tree -Dverbose ./gradlew dependencies
Conflicto de versiones Gana la más cercana Gana la más alta
Fijar versiones dependencyManagement, BOM (<scope>import</scope>) platform(...), catálogo libs.versions.toml
Excluir transitiva <exclusions> exclude(group = ..., module = ...)
Multimódulo POM padre pom + <modules> + <parent> include(...) en settings.gradle.kts
Variantes de construcción Perfiles -Pprod Propiedades y tareas propias
Fat jar maven-shade-plugin / maven-assembly-plugin Plugin Shadow o tarea Jar propia
Wrapper mvn wrapper:wrapper → ./mvnw gradle wrapper → ./gradlew
Repositorio local ~/.m2/repository ~/.gradle/caches
Salida target/ build/