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
Dockerfileconstruyen conmvno./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:
- Descarga el fichero
apache-maven-3.9.16-bin.zipde maven.apache.org/download.cgi. - Descomprímelo, por ejemplo en
C:\herramientas\apache-maven-3.9.16. - Añade
C:\herramientas\apache-maven-3.9.16\bina la variable de entornoPath(Configuración → Sistema → Información → Configuración avanzada del sistema → Variables de entorno). - 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 indicaJAVA_HOME, no necesariamente con el que tengas en elPATH. Si aparece una versión antigua, corrigeJAVA_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-quickstartgenera un proyecto Java mínimo con una claseApp, una prueba y supom.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.releasea25, 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:generatesin 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.
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 detarget: se borra conmvn cleany 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,artifactIdyversion. Es como la dirección postal de un.jaren el mundo Maven:tools.jackson.core:jackson-databind:3.2.3identifica 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:
- Centralizar valores (versiones, nombre de la clase principal): cambias
jackson.versionen un sitio y afecta a todas las dependencias que la usen. - 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.releasey no el parsource/target.releaseademá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 usarIO.printlndarí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
runtimees el adecuado para los drivers JDBC (módulo 12): tu código solo usa la API estándarjava.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), comocompiler:compileojar: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.MFla clase principal (Main-Class). - shade se engancha a la fase
package(<execution><phase>package</phase>) y genera un segundo jar, con el clasificadorcompleto, que incluye todas las dependencias. Los filtros excluyen firmas digitales de otros jars (que invalidarían el resultado) y losmodule-info.classde las dependencias. - exec permite
mvn exec:javasin escribir el nombre de la clase.
⚠️ Importante: ¿Por qué hace falta shade? El
.jarnormal solo contiene tus clases. Si lo ejecutas conjava -jarsin 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) ysite(documentación).
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-compilees la ejecución por defecto;(default)en shade es la que declaramos sin<id>. cleanyresourcesaparecen 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-pommuestra 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 -DforceStdoutimprime1.0.0.
7. Repositorios: local, Maven Central y de empresa
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 WindowsC:\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 ensettings.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/repositoryy 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
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-textse compiló contracommons-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 conNoSuchMethodError. Es uno de los errores más desconcertantes del mundo Java, y siempre se diagnostica condependency: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
NoClassDefFoundErroren 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:
pluginManagementydependencyManagementen 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 despring-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.jarsirve 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 (
mvnwpara Linux/macOS ymvnw.cmdpara 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.cmdy la carpeta.mvn/) y usa./mvnwen losDockerfiley en la integración continua. Si en Linux da “Permission denied”, ejecutachmod +x mvnw(y guarda el permiso en Git congit 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.*atools.jackson.*, el mapper se crea con un builder y es inmutable, y las excepciones (JacksonException) son no comprobadas: por esowriteValueAsStringno obliga a untry/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 conjava -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 initpuede 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
implementationyapies importante en proyectos multimódulo: una dependenciaimplementationno 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 pluginjava-library) sí se expone. Maven no tiene esta distinción: todo lo que escompilese 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 ungradleinstalado globalmente. Sube a Gitgradlew,gradlew.baty la carpetagradle/wrapper/(incluido elgradle-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:
Figura 5. Esquema del asistente New Project de IntelliJ IDEA (no es una captura).
- Name y Location: nombre de la carpeta del proyecto y dónde crearla. Debajo se muestra la ruta final.
- Language: Java.
- Build system: elige Maven (o Gradle; IntelliJ es el sistema propio del IDE, que no sirve fuera de él).
- JDK: el JDK 25. Si no aparece, el desplegable ofrece Download JDK… o Add JDK from Disk….
- Add sample code: crea una clase
Mainde ejemplo. - Advanced Settings: despliégalo para escribir el GroupId y el ArtifactId.
- 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
Figura 6. Esquema del editor y la ventana de herramientas Maven de IntelliJ IDEA (no es una captura).
- Botón flotante de recarga (icono
mcon flechas): aparece en el editor cuando modificas elpom.xml. Púlsalo (oCtrl+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 losimporten rojo. - Reload All Maven Projects: lo mismo, desde la ventana Maven.
- Execute Maven Goal (
m): abre un cuadro para escribir cualquier orden (clean package -DskipTests,dependency:tree…). - Lifecycle: doble clic en una fase la ejecuta. La salida aparece en la ventana Run de abajo.
- Plugins: cada plugin con sus objetivos (
dependency:tree,exec:java…). - 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.xmlabierto,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)) niLATEST. - Centraliza versiones en
<properties>y usa BOM cuando existan. - Usa el scope correcto: JUnit siempre
test, drivers JDBCruntime. - Sube a Git el
pom.xmly el wrapper; nuncatarget/nibuild/. - 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 delisEqualTo(2400.0)). - AssertJ explica la diferencia: esperaba
2400.0y el valor real era2500.0. Las otras dos pruebas pasan (Tests run: 3, Failures: 1). - La construcción termina en
BUILD FAILUREen la fasetest, así que no se llega apackage: 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.txty un.xmlpor 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/ |