La lección anterior terminó con una pregunta incómoda: ¿qué es exactamente mvn test?
Lo has escrito cinco veces. Has puesto <scope>test</scope> en una dependencia sin saber qué significa. Has visto aparecer spring-boot-starter-parent, maven-surefire-plugin y spring-boot-maven-plugin en un pom.xml y los has dado por buenos. Has ejecutado mvn spring-boot:run y mvn dependency:tree. Y en 11-01 se habló de coordenadas groupId:artifactId:version como preparación de esta lección.
Esta lección explica la herramienta que ha estado sosteniendo las tres anteriores.
Maven es la herramienta de construcción estándar del ecosistema Java. Su trabajo es convertir tu código fuente y un fichero de configuración en un artefacto ejecutable, resolviendo por el camino las dependencias, ejecutando las pruebas y garantizando que el proceso da el mismo resultado en cualquier máquina.
Ese último punto es el que más se subestima. Hasta el módulo 10, BiblioTech se compilaba con javac y un classpath escrito a mano. Con los cuarenta jars que ha traído Spring Boot, eso ya no es tedioso: es imposible. Y hay una razón de fondo que va más allá de la comodidad, y que enlaza con Log4Shell (11-01): la capacidad de responder a una vulnerabilidad depende de poder cambiar una versión con una línea y verificar con un comando que nada se rompió. La segunda mitad la conseguiste en 11-04 con JUnit. Aquí llega la primera.
Al terminar entenderás qué problema resuelve una herramienta de construcción; sabrás leer y escribir un pom.xml completo; dominarás las dependencias, sus ámbitos, la transitividad y la resolución de conflictos; conocerás el ciclo de vida y sus fases; sabrás qué es un plugin y qué hacen los que usas; manejarás perfiles y proyectos multimódulo; y tendrás criterio para elegir entre Maven y Gradle.
Y BiblioTech será un proyecto Maven completo y reproducible.
Contenido
- El problema:
javaca mano - Qué resuelve una herramienta de construcción
- Convención sobre configuración
- La estructura estándar de directorios
- Instalación y comprobación
- El POM: coordenadas
SNAPSHOTfrente a versión liberadaproperties: variables del POM- El
pom.xmlcompleto de BiblioTech - Dependencias: declaración y repositorios
- Ámbitos de dependencia
- Dependencias transitivas
- Resolución de conflictos: la definición más cercana
mvn dependency:treeen la prácticaexclusionsdependencyManagementfrente adependencies- El BOM y el
spring-boot-starter-parent - El ciclo de vida: tres cadenas
- Las fases de la cadena por defecto
- Plugins y goals
- Los plugins que usa BiblioTech
surefirefrente afailsafe- Empaquetar:
maven-jar-plugin,shadeyspring-boot-maven-plugin - Perfiles
- Proyectos multimódulo
settings.xmly las credenciales- El Maven Wrapper
- Comandos del día a día
- Reproducibilidad y fijación de versiones
- Maven frente a Gradle
- BiblioTech como proyecto Maven completo
- Errores Comunes y Consejos
- Ejercicios
- El problema:
javac a mano
javac a manoVolvamos al módulo 1. Así se compilaba y ejecutaba BiblioTech:
javac -d clases src/com/nexussoftware/bibliotech/*.java
java -cp clases com.nexussoftware.bibliotech.MainFunciona con diez ficheros y cero dependencias. Ahora mira lo que haría falta hoy:
javac -d target/classes \
-cp "lib/spring-core-6.1.11.jar:lib/spring-context-6.1.11.jar:lib/spring-beans-6.1.11.jar:lib/spring-aop-6.1.11.jar:lib/spring-boot-3.3.2.jar:lib/spring-boot-autoconfigure-3.3.2.jar:lib/hibernate-core-6.5.2.jar:lib/jakarta.persistence-api-3.1.0.jar:lib/h2-2.2.224.jar:lib/jackson-databind-2.17.2.jar:lib/slf4j-api-2.0.13.jar:lib/logback-classic-1.5.6.jar:..." \
$(find src/main/java -name "*.java")Y eso solo compila. Después habría que:
- Copiar los recursos de
src/main/resourcesatarget/classes. - Compilar las pruebas con otro classpath (el de producción más JUnit, AssertJ y Mockito).
- Ejecutar las pruebas invocando el lanzador de JUnit a mano.
- Empaquetar en un jar con un
MANIFEST.MFcorrecto. - Y antes de todo eso, descargar los cuarenta jars a mano, con sus versiones exactas y compatibles entre sí.
Ese punto 5 es el asesino. Descargar spring-boot-starter-data-jpa significa averiguar que necesita Hibernate, que Hibernate necesita jakarta.persistence-api, ByteBuddy, Jandex, Classmate y Antlr, que ByteBuddy necesita... y hacerlo con versiones que no choquen. A mano, es un trabajo de días que hay que rehacer en cada actualización.
Se llama el infierno de las dependencias (dependency hell), y es el problema principal que Maven resuelve.
- Qué resuelve una herramienta de construcción
| Problema | Sin herramienta | Con Maven |
|---|---|---|
| Compilar | javac con classpath a mano |
mvn compile |
| Dependencias | Descargar jars a mano | Declararlas en el POM |
| Dependencias transitivas | Averiguarlas y descargarlas una a una | Automático |
| Conflictos de versiones | Prueba y error | Regla determinista |
| Ejecutar pruebas | Invocar el lanzador a mano | mvn test, automático |
| Empaquetar | jar cvfm con manifiesto a mano |
mvn package |
| Recursos | cp a mano |
Automático |
| Reproducibilidad | Depende de la máquina | Garantizada |
| Integración con IDE | Configurar cada uno | El IDE lee el POM |
| Integración continua | Script a medida | mvn verify |
Y una consecuencia cultural que importa: cualquier proyecto Maven se compila igual. Te dan un repositorio desconocido, ves un pom.xml, escribes mvn package y funciona. Sin leer documentación, sin preguntar a nadie.
- Convención sobre configuración
Este es el principio de diseño de Maven, y explica por qué su configuración es tan corta.
Si sigues las convenciones, no tienes que configurar nada. Solo configuras lo que se aparta de ellas.
Maven asume que:
- El código fuente está en
src/main/java. - Los recursos están en
src/main/resources. - Las pruebas están en
src/test/java. - Los recursos de prueba están en
src/test/resources. - Todo lo generado va a
target/. - Todo fichero
*Test.javaes una prueba.
Por eso en 11-04 pusiste las pruebas en src/test/java y mvn test las encontró sin que configuraras nada. No era magia: era la convención.
La alternativa —configuración explícita, como Ant— exige declarar cada ruta, cada tarea y cada dependencia entre tareas. El resultado son ficheros de construcción de trescientas líneas, distintos en cada proyecto y que hay que leer antes de entender nada.
El compromiso es real: si tu proyecto no encaja en las convenciones, Maven se vuelve incómodo. Es el precio de la uniformidad, y para la inmensa mayoría de los proyectos Java compensa.
- La estructura estándar de directorios
bibliotech/
├── pom.xml <- EL fichero de configuración
├── mvnw, mvnw.cmd <- Maven Wrapper (apartado 27)
├── .mvn/wrapper/ <- configuración del wrapper
├── src/
│ ├── main/
│ │ ├── java/ <- código de producción
│ │ │ └── com/nexussoftware/bibliotech/
│ │ └── resources/ <- application.yml, data.sql, logback.xml
│ └── test/
│ ├── java/ <- pruebas
│ │ └── com/nexussoftware/bibliotech/
│ └── resources/ <- application-test.yml, CSV de prueba
└── target/ <- TODO lo generado (nunca en git)
├── classes/ <- .class de producción + recursos
├── test-classes/ <- .class de pruebas
├── surefire-reports/ <- informes de pruebas (11-04)
├── generated-sources/ <- código generado (Lombok, MapStruct)
└── bibliotech-1.0.0-SNAPSHOT.jargraph TD
A["src/main/java"] -->|compile| B["target/classes"]
R["src/main/resources"] -->|process-resources| B
C["src/test/java"] -->|test-compile| D["target/test-classes"]
TR["src/test/resources"] -->|process-test-resources| D
B --> D
D -->|test: surefire| E["target/surefire-reports"]
B -->|package| F["target/bibliotech-1.0.0-SNAPSHOT.jar"]
F -->|install| G["~/.m2/repository"]
Dos reglas de oro:
target/nunca se sube a git. Es todo regenerable. Tu.gitignoredebe tenertarget/.src/nunca se toca a mano desde la construcción. Es la fuente, y es lo único que hay en el repositorio junto con el POM.
- Instalación y comprobación
Apache Maven 3.9.8
Maven home: /opt/maven
Java version: 17.0.11, vendor: Eclipse Adoptium
Default locale: es_ES, platform encoding: UTF-8
OS name: "linux", version: "6.12.0", arch: "amd64"Comprueba tres cosas: la versión de Maven (3.9.x es la actual), la de Java (17 o superior para este curso) y la codificación de la plataforma, que debe ser UTF-8 para evitar problemas con acentos.
Si no lo tienes instalado, el apartado 27 te dará una razón muy buena para no necesitarlo.
Crear un proyecto desde cero:
mvn archetype:generate \
-DgroupId=com.nexussoftware \
-DartifactId=bibliotech \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=falseEn la práctica, para un proyecto Spring Boot se usa Spring Initializr (start.spring.io), que genera el POM, el wrapper y la clase principal ya configurados.
- El POM: coordenadas
El POM (Project Object Model) es el pom.xml: la descripción completa del proyecto.
<?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> <!-- siempre 4.0.0 -->
<!-- LAS COORDENADAS: identifican el artefacto de forma única -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Gestión de la biblioteca técnica interna de Nexus Software</description>
</project>| Coordenada | Qué es | Convención |
|---|---|---|
groupId |
La organización | Dominio invertido: com.nexussoftware |
artifactId |
El módulo | Minúsculas con guiones: bibliotech-core |
version |
La versión | SemVer (11-01): 1.0.0 |
packaging |
Qué se produce | jar (por defecto), war, pom |
Los tres primeros forman las coordenadas groupId:artifactId:version, que ya viste en 11-01. Determinan la ruta en el repositorio:
Valores de packaging:
| Valor | Produce | Uso |
|---|---|---|
jar |
Un .jar |
Librerías y aplicaciones (lo normal) |
war |
Un .war |
Aplicaciones web para servidor externo (poco usado hoy) |
pom |
Solo el POM | Proyecto padre o agregador (apartado 25) |
SNAPSHOT frente a versión liberada
SNAPSHOT frente a versión liberadaLa versión tiene dos naturalezas muy distintas:
| Tipo | Ejemplo | Significado | Comportamiento |
|---|---|---|---|
| SNAPSHOT | 1.0.0-SNAPSHOT |
En desarrollo | Mutable: Maven vuelve a descargarla periódicamente |
| Liberada | 1.0.0 |
Publicada | Inmutable: se descarga una vez y se cachea para siempre |
La consecuencia práctica: si dependes de bibliotech-core:1.0.0-SNAPSHOT, Maven comprueba a diario si hay una versión más nueva. Si dependes de 1.0.0, la descarga una vez y no vuelve a mirar.
Regla profesional: una versión liberada nunca se sobrescribe. Si 1.0.0 tiene un fallo, se publica 1.0.1. Republicar 1.0.0 con contenido distinto rompe la reproducibilidad de todo el que ya la tenía cacheada, y es una de las peores cosas que se pueden hacer en un repositorio compartido.
Y su corolario: nunca dependas de un SNAPSHOT en producción. Compila hoy y mañana con contenido distinto.
Forzar la actualización de snapshots:
properties: variables del POM
properties: variables del POM<properties>
<java.version>17</java.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- versiones centralizadas, para no repetirlas -->
<assertj.version>3.25.3</assertj.version>
<mockito.version>5.12.0</mockito.version>
</properties>Se usan con ${nombre}:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>Ventaja: cuando quince dependencias comparten versión (todo el ecosistema de Jackson, por ejemplo), se cambia en un sitio.
project.build.sourceEncoding es importante de verdad: sin ella, Maven usa la codificación de la plataforma, y compilar en una máquina con Windows en español y en un servidor Linux con UTF-8 puede producir resultados distintos. Maven avisa con un WARNING si falta.
También existen propiedades predefinidas útiles: ${project.version}, ${project.artifactId}, ${project.basedir}, ${maven.build.timestamp}.
- El
pom.xml completo de BiblioTech
pom.xml completo de BiblioTechEste es el POM completo y comentado, con todo lo que el proyecto necesita tras cuatro lecciones:
<?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>
<!-- ================= PADRE ================= -->
<!-- Aporta el BOM (versiones compatibles de TODO) y configuración de plugins -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- vacío: búscalo en el repositorio, no en el disco -->
</parent>
<!-- ================= IDENTIDAD ================= -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Gestión de la biblioteca técnica interna de Nexus Software</description>
<!-- ================= PROPIEDADES ================= -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- ================= DEPENDENCIAS ================= -->
<dependencies>
<!-- Núcleo de Spring: contenedor IoC, autoconfiguración, logging (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Validación: @NotNull, @Min en @ConfigurationProperties (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- AOP: @Transactional y aspectos propios (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- JPA + Hibernate + Spring Data + transacciones (11-03) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2: base de datos en memoria. runtime: no hace falta al compilar -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Metadatos de @ConfigurationProperties: autocompletado en el IDE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Pruebas: JUnit 5, AssertJ, Mockito, spring-test (11-04, 11-06) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ================= CONSTRUCCIÓN ================= -->
<build>
<plugins>
<!-- Empaqueta el jar ejecutable y da el goal spring-boot:run (11-02) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Pruebas UNITARIAS: *Test.java -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integracion</excludedGroups> <!-- @Tag de 11-04 -->
</configuration>
</plugin>
<!-- Pruebas de INTEGRACIÓN: *IT.java, en la fase verify -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- ================= PERFILES ================= -->
<profiles>
<profile>
<id>rapido</id> <!-- mvn package -Prapido : sin pruebas lentas -->
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>
</project>Detalle importante: ninguna dependencia de Spring lleva <version>. Las gestiona el padre. Volveremos a ello en el apartado 17.
- Dependencias: declaración y repositorios
Una dependencia se declara con sus coordenadas:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
<scope>compile</scope> <!-- por defecto; se puede omitir -->
</dependency>¿De dónde sale el jar? Maven busca en tres sitios, en este orden:
graph LR
A["mvn compile"] --> B{"¿Está en<br/>~/.m2/repository?"}
B -->|Sí| E["Se usa"]
B -->|No| C{"¿Hay repositorio<br/>corporativo (Nexus,<br/>Artifactory)?"}
C -->|Sí| D["Se descarga de él"]
C -->|No| F["Se descarga de<br/>Maven Central"]
D --> G["Se guarda en ~/.m2"]
F --> G
G --> E
| Repositorio | Qué es | Cuándo |
|---|---|---|
Local (~/.m2/repository) |
Caché en tu disco | Siempre se mira primero |
| Central | El repositorio público (11-01) | Por defecto |
| Corporativo | Nexus, Artifactory: espejo y artefactos privados | En empresa |
Añadir un repositorio adicional (evítalo si puedes):
<repositories>
<repository>
<id>nexus-interno</id>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</repository>
</repositories>Consejo profesional: cuantos menos repositorios, mejor. Cada uno añade una fuente de artefactos que hay que confiar. Lo habitual en empresa es configurar un solo espejo corporativo en settings.xml (apartado 26) que a su vez replica Central: así hay un único punto de control, cacheado y auditable.
- Ámbitos de dependencia
El ámbito (scope) determina en qué classpath está disponible una dependencia y si se empaqueta.
| Ámbito | Compilar | Probar | Ejecutar | ¿Transitivo? | Ejemplo |
|---|---|---|---|---|---|
compile (por defecto) |
Sí | Sí | Sí | Sí | Spring, Jackson |
provided |
Sí | Sí | No | No | API de Servlet en un .war; Lombok |
runtime |
No | Sí | Sí | Sí | Driver de H2, Logback |
test |
No | Sí | No | No | JUnit, AssertJ, Mockito |
system |
Sí | Sí | No | No | Obsoleto, no lo uses |
import |
— | — | — | — | Solo en dependencyManagement: importa un BOM |
Los dos que ya has usado sin saberlo:
runtime para H2. Tu código nunca escribe import org.h2.Driver. Solo lo necesita la JVM en ejecución, cuando Hibernate carga el driver por su nombre. Ponerlo en runtime significa que no puedes usarlo por accidente en tu código: si alguien escribe una clase que importa algo de H2, no compila. Eso es una barrera arquitectónica gratis.
test para JUnit y AssertJ. Están disponibles al compilar y ejecutar pruebas, y no se empaquetan en el jar final. Es lo que evita que tu aplicación en producción lleve dentro un framework de pruebas — con su peso y su superficie de ataque.
provided merece una nota: significa "en tiempo de ejecución esto ya estará ahí, no lo empaquetes". Su caso clásico era la API de Servlet en un .war, que la proporcionaba el servidor de aplicaciones. Hoy, con servidores embebidos, se usa sobre todo para Lombok (11-07), que solo actúa en compilación.
Ver el classpath resultante:
- Dependencias transitivas
Aquí está la característica que hace que Maven merezca la pena.
Cuando declaras una dependencia, Maven descarga también las suyas, y las de aquellas, recursivamente.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Esa única declaración trae, entre otros:
spring-boot-starter-data-jpa
├── spring-boot-starter-aop
│ ├── spring-aop
│ └── aspectjweaver
├── spring-boot-starter-jdbc
│ ├── HikariCP <- pool de conexiones
│ └── spring-jdbc
├── jakarta.persistence-api <- la especificación (11-03)
├── jakarta.transaction-api
├── hibernate-core <- la implementación (11-03)
│ ├── byte-buddy <- genera los proxies de la carga perezosa
│ ├── jandex
│ ├── classmate
│ └── antlr4-runtime <- parsea el JPQL
├── spring-data-jpa
│ ├── spring-orm
│ └── spring-tx
└── spring-aspectsCasi treinta artefactos que tú no has nombrado. Sin Maven, tendrías que averiguar cada uno, encontrar la versión compatible y descargarlo a mano.
Y ahí se ve algo que 11-03 dio por supuesto: ByteBuddy está en tu proyecto porque Hibernate lo usa para generar las subclases proxy de la carga perezosa. Es el mecanismo del módulo 10, con nombre y coordenadas.
Regla de la transitividad, importante: las dependencias test y provided no se propagan. Si BiblioTech dependiera de una librería que usa JUnit en sus pruebas, tú no heredarías JUnit. Es lo correcto: las dependencias de prueba de otro no son asunto tuyo.
- Resolución de conflictos: la definición más cercana
Con treinta dependencias transitivas, es inevitable que dos pidan versiones distintas de lo mismo:
Solo puede haber una versión de una clase en el classpath. ¿Cuál gana?
La regla de la definición más cercana (nearest definition wins): gana la versión que está a menos saltos de tu POM. Si empatan, gana la declarada primero.
tu-proyecto (profundidad 0)
├── libreria-a (1)
│ └── jackson-databind:2.15.0 (2)
├── jackson-databind:2.17.2 (1) <- GANA: está más cerca
└── libreria-b (1)
└── jackson-databind:2.16.0 (2)Consecuencia práctica muy útil: declarar explícitamente una dependencia en tu POM te da control absoluto sobre su versión, porque estará siempre a profundidad 1 y ganará a cualquier transitiva.
La regla es determinista, pero puede sorprender: no gana "la más nueva", gana "la más cercana". Si una transitiva cercana pide una versión antigua, esa antigua gana, y puedes acabar con NoSuchMethodError en ejecución — el síntoma clásico de un conflicto de versiones.
Diagnóstico:
[INFO] +- com.ejemplo:libreria-a:jar:1.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.15.0:compile
- omitted for conflict with 2.17.2)Ese omitted for conflict with te dice exactamente qué versión se descartó y por cuál.
mvn dependency:tree en la práctica
mvn dependency:tree en la prácticaEs la herramienta de diagnóstico que más veces te va a sacar de un apuro. Cuatro usos concretos:
1. Ver todo el árbol:
2. Buscar quién arrastra un artefacto (la pregunta de seguridad de 11-01):
[INFO] com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
[INFO] \- com.ejemplo:libreria-legacy:jar:2.1.0:compile
[INFO] \- org.apache.logging.log4j:log4j-core:jar:2.14.1:compileAhí tienes la respuesta a "¿tengo Log4j y quién lo ha metido?", que en diciembre de 2021 costó semanas a media industria.
3. Ver conflictos resueltos:
4. Detectar dependencias declaradas y no usadas, o usadas y no declaradas:
[WARNING] Used undeclared dependencies found:
[WARNING] org.slf4j:slf4j-api:jar:2.0.13:compile
[WARNING] Unused declared dependencies found:
[WARNING] com.ejemplo:utilidades:jar:1.0.0:compileEse primer aviso señala un riesgo real: estás usando SLF4J en tu código pero lo obtienes de forma transitiva. Si mañana Spring Boot deja de traerlo, tu código deja de compilar sin que hayas tocado nada. Todo lo que uses directamente, decláralo directamente.
exclusions
exclusionsA veces hay que quitar algo que llega transitivamente. Casos reales:
<!-- Caso 1: sustituir Logback por Log4j2 en Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency><!-- Caso 2: quitar Tomcat para usar Jetty o Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency><!-- Caso 3: quitar una implementación de logging duplicada que causa conflicto -->
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>libreria-legacy</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>Ese tercer caso es importante y lo verás en 11-07: dos implementaciones de logging en el mismo classpath se pelean y el resultado es que no se registra nada, o se registra dos veces.
Para excluir todo el árbol de una dependencia de golpe:
Úsalo con cuidado: es fácil quitar algo que sí hacía falta y descubrirlo en ejecución.
dependencyManagement frente a dependencies
dependencyManagement frente a dependenciesDos secciones que se confunden constantemente.
| Sección | Qué hace |
|---|---|
<dependencies> |
Añade la dependencia al proyecto |
<dependencyManagement> |
Solo declara la versión que se usará si alguien la añade |
<dependencyManagement>
<dependencies>
<!-- NO añade Jackson. Solo dice: "si se usa, será la 2.17.2" -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- ESTO sí la añade. Sin version: la toma de dependencyManagement -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>Para qué sirve, tres usos:
- Centralizar versiones en un proyecto multimódulo: el padre las declara, los módulos las usan sin versión.
- Forzar la versión de una transitiva sin declararla como dependencia directa. Muy útil para parchear una vulnerabilidad: si una transitiva tiene un CVE, aquí impones la versión corregida.
- Importar un BOM, que es lo del apartado siguiente.
El punto 2 merece un ejemplo, porque es la herramienta de respuesta rápida ante vulnerabilidades:
<dependencyManagement>
<dependencies>
<!-- Una transitiva trae la 2.14.1, vulnerable. Forzamos la corregida -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
</dependencies>
</dependencyManagement>dependencyManagement gana siempre sobre la resolución transitiva, sin importar la profundidad. Es la forma correcta de parchear sin esperar a que la librería intermedia se actualice.
- El BOM y el
spring-boot-starter-parent
spring-boot-starter-parentUn BOM (Bill of Materials) es un POM de tipo pom que solo contiene dependencyManagement: un catálogo de versiones probadas juntas.
Se usa de dos formas.
Forma 1: heredar del padre (lo que hace BiblioTech):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>Eso te da:
| Herencia | Qué aporta |
|---|---|
dependencyManagement |
Versiones compatibles de más de 400 artefactos |
pluginManagement |
Versiones y configuración de los plugins habituales |
| Propiedades | java.version, codificación, etc. |
| Filtrado de recursos | application.yml con @propiedad@ sustituible |
| Configuración de plugins | spring-boot-maven-plugin ya enganchado a package |
Forma 2: importar el BOM (cuando ya tienes otro padre, típico en empresa):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope> <!-- el ámbito import del apartado 11 -->
</dependency>
</dependencies>
</dependencyManagement>Y ahora se entiende del todo la advertencia que aparecía en 11-02:
No pongas
<version>en las dependencias que gestiona el BOM.
Si escribes <version>2.15.0</version> en Jackson, estás anulando la versión que el equipo de Spring Boot ha probado con esa versión de Spring, de Hibernate y del resto del ecosistema. Puedes acabar con incompatibilidades sutiles: un NoSuchMethodError en ejecución, seis meses después, en producción.
Si de verdad necesitas otra versión, la forma correcta es sobrescribir la propiedad:
<properties>
<jackson.version>2.17.2</jackson.version> <!-- nombre definido por el BOM -->
</properties>
- El ciclo de vida: tres cadenas
Maven tiene tres ciclos de vida independientes, cada uno con sus fases:
| Ciclo | Para qué | Fases principales |
|---|---|---|
clean |
Limpiar | pre-clean, clean, post-clean |
default |
Construir | validate ... deploy (la importante) |
site |
Documentación | pre-site, site, post-site, site-deploy |
mvn clean # ejecuta el ciclo clean: borra target/
mvn package # ejecuta el ciclo default hasta la fase package
mvn clean package # los dos, en ese ordenmvn site genera un sitio web con informes del proyecto. Se usa poco hoy; la mayoría de los equipos usan otras herramientas de informes.
- Las fases de la cadena por defecto
Y aquí está la idea clave de Maven:
Ejecutar una fase ejecuta todas las fases anteriores.
Por eso mvn test compiló tu código en 11-04 sin que se lo pidieras: test viene después de compile.
graph TD
A["validate<br/>El proyecto es correcto"] --> B["compile<br/>src/main/java -> target/classes"]
B --> C["test<br/>Ejecuta *Test con surefire"]
C --> D["package<br/>Empaqueta en jar/war"]
D --> E["verify<br/>Pruebas de integración (failsafe)<br/>y comprobaciones de calidad"]
E --> F["install<br/>Copia a ~/.m2/repository"]
F --> G["deploy<br/>Sube al repositorio remoto"]
La lista completa, con las fases intermedias que también existen:
| # | Fase | Qué hace |
|---|---|---|
| 1 | validate |
Comprueba que el POM es correcto |
| 2 | generate-sources |
Genera código fuente |
| 3 | process-resources |
Copia src/main/resources a target/classes |
| 4 | compile |
Compila src/main/java |
| 5 | process-test-resources |
Copia src/test/resources |
| 6 | test-compile |
Compila src/test/java |
| 7 | test |
Ejecuta las pruebas unitarias (surefire) |
| 8 | package |
Empaqueta en .jar |
| 9 | pre-integration-test |
Prepara el entorno de integración |
| 10 | integration-test |
Ejecuta las pruebas de integración (failsafe) |
| 11 | post-integration-test |
Limpia el entorno |
| 12 | verify |
Comprueba los resultados de integración |
| 13 | install |
Instala el artefacto en ~/.m2/repository |
| 14 | deploy |
Sube al repositorio remoto |
Cuál usar en cada situación:
| Situación | Comando |
|---|---|
| Desarrollo, ciclo rápido | mvn test |
| Generar el jar | mvn package |
| Integración continua | mvn verify |
| Publicar para otros módulos locales | mvn install |
| Publicar al repositorio corporativo | mvn deploy |
Un matiz sobre install: mucha gente escribe mvn clean install por costumbre para todo. Es más lento de lo necesario y llena ~/.m2 de artefactos locales. Para verificar, mvn verify basta; install solo hace falta cuando otro proyecto local va a consumir tu artefacto.
- Plugins y goals
Y aquí está la segunda idea clave:
Maven no hace nada por sí mismo. Todo lo hacen los plugins.
Una fase no tiene código: es un punto de enganche. Lo que ocurre en compile es que se ejecuta el goal compile del plugin maven-compiler-plugin.
| Concepto | Qué es |
|---|---|
| Plugin | Un artefacto Maven con tareas ejecutables |
| Goal | Una tarea concreta: compiler:compile, surefire:test |
| Vinculación | La asociación entre un goal y una fase |
Los enlaces por defecto para packaging=jar:
| Fase | Plugin:goal |
|---|---|
process-resources |
maven-resources-plugin:resources |
compile |
maven-compiler-plugin:compile |
test-compile |
maven-compiler-plugin:testCompile |
test |
maven-surefire-plugin:test |
package |
maven-jar-plugin:jar |
install |
maven-install-plugin:install |
deploy |
maven-deploy-plugin:deploy |
Un goal también se puede invocar directamente, sin ciclo de vida, con la sintaxis plugin:goal:
mvn dependency:tree # plugin dependency, goal tree
mvn spring-boot:run # plugin spring-boot, goal run (11-02)
mvn versions:display-dependency-updates
mvn help:effective-pom # muestra el POM RESULTANTE tras la herenciaEse último es una joya de diagnóstico: mvn help:effective-pom muestra el POM completo después de aplicar la herencia del padre, con todas las versiones y configuraciones resueltas. Cuando no entiendas de dónde sale una configuración, mira ahí.
- Los plugins que usa BiblioTech
maven-compiler-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release> <!-- mejor que source+target -->
<parameters>true</parameters> <!-- conserva nombres de parámetros -->
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg> <!-- avisos = errores. Estricto pero sano -->
</compilerArgs>
</configuration>
</plugin>Sobre <release>17</release> frente a <source>/<target>: release es mejor porque además de fijar el nivel del lenguaje, verifica que solo usas API existente en Java 17. Con source/target puedes compilar con JDK 21 código que usa un método de Java 21 y generar bytecode 17 que fallará en ejecución con NoSuchMethodError. release lo impide en compilación.
Sobre <parameters>true</parameters>: conserva los nombres de los parámetros en el bytecode. Lo necesitan Spring (para @Value sin nombre explícito) y Jackson (11-07) para mapear constructores.
spring-boot-maven-plugin
Ya lo usaste en 11-02. Aporta:
| Goal | Qué hace |
|---|---|
spring-boot:run |
Ejecuta la aplicación sin empaquetar |
spring-boot:repackage |
Convierte el jar normal en jar ejecutable (enganchado a package) |
spring-boot:build-image |
Construye una imagen de contenedor (12-06) |
maven-surefire-plugin y maven-failsafe-plugin
Los dos ejecutan pruebas. Su diferencia es el apartado siguiente.
surefire frente a failsafe
surefire frente a failsafe| surefire | failsafe | |
|---|---|---|
| Tipo de prueba | Unitarias | Integración |
| Fase | test |
integration-test + verify |
| Patrón de nombres | *Test.java, Test*.java, *Tests.java |
*IT.java, IT*.java, *ITCase.java |
| Si fallan | Se detiene inmediatamente | Registra el fallo, ejecuta post-integration-test y falla en verify |
| Velocidad | Milisegundos | Segundos |
Esa diferencia de comportamiento ante el fallo no es un capricho. Una prueba de integración puede haber arrancado una base de datos o un contenedor en pre-integration-test. Si failsafe se detuviera en seco, esos recursos quedarían colgados. Por eso failsafe siempre deja que se ejecute la limpieza y solo falla al llegar a verify.
Consecuencia práctica en la que cae todo el mundo:
mvn test # ejecuta SOLO las unitarias. Las *IT NO se ejecutan.
mvn verify # ejecuta las unitarias Y las de integración.Si escribes PrestamoRepositoryIT.java y ejecutas mvn test, no se ejecuta y no hay ningún aviso. Parece que todo va bien.
Aplicado a BiblioTech, con las etiquetas de 11-04:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integracion,lenta</excludedGroups>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<groups>integracion</groups>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>Así el ciclo rápido de desarrollo (mvn test) tarda segundos, y la integración continua (mvn verify) lo ejecuta todo.
- Empaquetar:
maven-jar-plugin, shade y spring-boot-maven-plugin
maven-jar-plugin, shade y spring-boot-maven-pluginTres formas de producir un artefacto ejecutable, con propiedades distintas.
maven-jar-plugin: el jar simple
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>Produce un jar con solo tus clases. Para ejecutarlo hay que aportar el classpath completo:
Es lo que se usa para publicar librerías, donde las dependencias las resuelve el consumidor.
maven-shade-plugin: el uber-jar
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>Descomprime todas las dependencias y las mete mezcladas en un solo jar. Funciona, pero tiene un problema conocido: si dos jars tienen ficheros con el mismo nombre en META-INF/services —muy habitual—, uno pisa al otro y algo deja de funcionar de forma misteriosa. Se resuelve con transformers, pero hay que saberlo.
spring-boot-maven-plugin: el jar en capas
Es el que usa BiblioTech. Ya viste su estructura en 11-02: mantiene cada dependencia como jar independiente dentro de BOOT-INF/lib/ y aporta su propio cargador de clases.
| jar simple | shade | Spring Boot | |
|---|---|---|---|
| Dependencias | Fuera | Descomprimidas y mezcladas | Jars intactos dentro |
| Colisión de recursos | — | Sí, riesgo real | No |
Ejecutable con java -jar |
Solo con classpath | Sí | Sí |
| Capas para Docker | No | No | Sí (12-06) |
| Uso típico | Librerías | Aplicaciones sin Spring | Aplicaciones Spring Boot |
Esa fila de las capas importa para 12-06: el jar de Spring Boot se puede descomponer en capas (dependencias, dependencias snapshot, recursos, clases de la aplicación) de modo que una imagen Docker solo tenga que reconstruir la capa que cambió. Como las dependencias cambian mucho menos que tu código, el despliegue pasa de subir 50 MB a subir 200 KB.
exec-maven-plugin
Para ejecutar una clase cualquiera sin Spring Boot:
Útil para utilidades y scripts de mantenimiento dentro del proyecto.
- Perfiles
Un perfil activa configuración condicional. No confundir con los perfiles de Spring de 11-02: aquellos afectan a los beans en ejecución; estos afectan a la construcción.
<profiles>
<profile>
<id>rapido</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
<profile>
<id>ci</id>
<activation>
<property><name>env.CI</name></property> <!-- si existe la variable CI -->
</activation>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <!-- cobertura, 12-05 -->
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>produccion</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>Activación:
| Forma | Sintaxis |
|---|---|
| Explícita | mvn package -Prapido |
| Desactivar | mvn package -P!rapido |
| Por propiedad | <activation><property>... |
| Por variable de entorno | env.CI |
| Por sistema operativo | <activation><os><family>windows |
| Por versión de JDK | <activation><jdk>17</jdk> |
| Por defecto | <activeByDefault>true</activeByDefault> |
Ver qué perfiles están activos:
Aviso: no metas lógica de negocio en perfiles de Maven. Si el artefacto que construyes es distinto según el perfil, ya no estás probando lo que despliegas. La práctica moderna es construir un solo artefacto y configurarlo en ejecución con variables de entorno (11-02, 12-06).
- Proyectos multimódulo
Cuando un proyecto crece, se divide en módulos. Es lo que necesitará BiblioTech en 12-01.
bibliotech/
├── pom.xml <- padre agregador, packaging: pom
├── bibliotech-dominio/
│ ├── pom.xml
│ └── src/main/java/... <- entidades y reglas. SIN dependencias de framework
├── bibliotech-persistencia/
│ ├── pom.xml
│ └── src/main/java/... <- repositorios JPA. Depende de dominio
├── bibliotech-servicio/
│ ├── pom.xml
│ └── src/main/java/... <- lógica. Depende de dominio y persistencia
└── bibliotech-app/
├── pom.xml
└── src/main/java/... <- @SpringBootApplication. Depende de todosEl POM padre:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging> <!-- NO produce jar: agrega -->
<modules>
<module>bibliotech-dominio</module>
<module>bibliotech-persistencia</module>
<module>bibliotech-servicio</module>
<module>bibliotech-app</module>
</modules>
<dependencyManagement>
<dependencies>
<!-- Versiones de los módulos internos, centralizadas -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-dominio</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-persistencia</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>Un módulo hijo:
<project>
<parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>bibliotech-persistencia</artifactId> <!-- groupId y version heredados -->
<dependencies>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-dominio</artifactId> <!-- sin version: la da el padre -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>Desde la raíz:
mvn clean install # construye TODOS los módulos, en orden de dependencias
mvn -pl bibliotech-servicio install # solo ese módulo
mvn -pl bibliotech-servicio -am install # ese módulo Y aquellos de los que dependeEl beneficio real no es organizativo, es arquitectónico: bibliotech-dominio no puede usar Spring ni JPA, porque no los tiene en su classpath. Maven impone la separación de capas que en un proyecto de un solo módulo depende de la disciplina del equipo. Es un límite verificado por la construcción, y es muy potente.
Se desarrolla en 12-01.
settings.xml y las credenciales
settings.xml y las credencialesMientras el pom.xml describe el proyecto y va en git, settings.xml describe tu máquina y nunca va en git.
Ubicaciones: ~/.m2/settings.xml (usuario) y ${maven.home}/conf/settings.xml (global).
<settings>
<!-- Espejo corporativo: todo pasa por él -->
<mirrors>
<mirror>
<id>nexus-corporativo</id>
<mirrorOf>*</mirrorOf>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</mirror>
</mirrors>
<!-- CREDENCIALES: por eso este fichero NO va al repositorio -->
<servers>
<server>
<id>nexus-corporativo</id>
<username>${env.NEXUS_USUARIO}</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
</servers>
<proxies>
<proxy>
<id>proxy-empresa</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.nexussoftware.com</host>
<port>8080</port>
</proxy>
</proxies>
</settings>Advertencia de seguridad. Las credenciales nunca se ponen en el
pom.xml, porque el POM va al repositorio de código y queda en su historial para siempre. Van ensettings.xml, y preferiblemente leídas de variables de entorno como en el ejemplo. Maven ofrecemvn --encrypt-passwordpara cifrarlas con una clave maestra, aunque no sustituye a un gestor de secretos.Y una regla que sí es absoluta: un secreto que ha estado en git se considera comprometido para siempre. Borrarlo en un commit posterior no lo elimina del historial. Si ocurre, hay que rotar la credencial. La gestión de secretos se trata en 12-07.
- El Maven Wrapper
Problema real: tú usas Maven 3.9.8, un compañero tiene 3.6.3, el servidor de integración continua tiene 3.8.1. Algo falla solo en una de las tres. Diagnosticarlo cuesta un día.
El Maven Wrapper lo resuelve: un script en el repositorio que descarga y usa la versión exacta de Maven que el proyecto declara.
Genera:
mvnw <- script Unix
mvnw.cmd <- script Windows
.mvn/wrapper/maven-wrapper.properties <- la versión declaradaY a partir de ahí:
Ventajas:
- Todo el mundo usa la misma versión, sin excepción.
- No hace falta instalar Maven para compilar el proyecto. Solo Java.
- La versión está en git, así que cambiarla es un commit revisable.
- La integración continua no necesita configurar nada.
Estos ficheros sí van a git (a diferencia de settings.xml). Spring Initializr los genera por defecto, y es una práctica estándar hoy: un repositorio Java moderno se compila con ./mvnw verify sin instalar nada más que un JDK.
- Comandos del día a día
| Comando | Qué hace |
|---|---|
mvn clean |
Borra target/ |
mvn compile |
Compila el código de producción |
mvn test |
Compila y ejecuta las pruebas unitarias |
mvn package |
Genera el jar |
mvn verify |
Todo, incluidas las pruebas de integración |
mvn install |
Instala en ~/.m2 |
mvn clean install |
Reconstruye desde cero e instala |
mvn -DskipTests package |
Empaqueta sin ejecutar las pruebas (las compila) |
mvn -Dmaven.test.skip=true package |
Ni las compila |
mvn -o package |
Sin conexión: solo el repositorio local |
mvn -U package |
Fuerza la actualización de snapshots |
mvn -X package |
Salida de depuración completa |
mvn -q package |
Silencioso: solo errores |
mvn -T 1C package |
Paralelo: 1 hilo por núcleo |
mvn -pl modulo -am install |
Un módulo y sus dependencias |
mvn dependency:tree |
El árbol de dependencias |
mvn dependency:analyze |
Declaradas y no usadas / usadas y no declaradas |
mvn versions:display-dependency-updates |
Qué está desactualizado |
mvn versions:display-plugin-updates |
Plugins desactualizados |
mvn help:effective-pom |
El POM resultante tras la herencia |
mvn help:active-profiles |
Perfiles activos |
Tres notas prácticas:
-DskipTestsfrente a-Dmaven.test.skip=true: el primero compila las pruebas pero no las ejecuta (detecta errores de compilación en ellas); el segundo ni las compila. Prefiere el primero.-o(offline) es útil en un tren o con la red caída, si ya tienes todo en~/.m2.-T 1Cpuede reducir a la mitad el tiempo de un proyecto multimódulo grande. Es seguro si los plugins que usas son compatibles con la ejecución paralela, y los habituales lo son.
Y para la actualización periódica de dependencias:
[INFO] The following dependencies have newer versions:
[INFO] com.h2database:h2 ................................. 2.2.224 -> 2.3.230
[INFO] org.springframework.boot:spring-boot-starter ....... 3.3.2 -> 3.3.3Ejecutarlo cada pocas semanas y aplicar los parches es una de las prácticas de higiene más rentables que existen, y la respuesta directa a la lección de Log4Shell.
- Reproducibilidad y fijación de versiones
La misma etiqueta de git debe producir el mismo artefacto, hoy y dentro de tres años. Eso es reproducibilidad, y es lo que permite reconstruir una versión antigua para depurar un incidente.
Cosas que la rompen:
| Práctica | Por qué rompe | Alternativa |
|---|---|---|
<version>LATEST</version> |
Cambia con el tiempo. Eliminado en Maven 3 | Versión concreta |
<version>RELEASE</version> |
Igual. Eliminado | Versión concreta |
<version>[1.0,2.0)</version> (rango) |
Resuelve distinto según el día | Versión concreta |
Dependencias -SNAPSHOT |
Mutables por definición | Versión liberada |
| No fijar versiones de plugins | Maven elige y puede cambiar | Fijarlas (o heredar del BOM) |
| Depender de la codificación de la plataforma | Distinta por máquina | project.build.sourceEncoding |
| Depender de la versión de JDK instalada | Distinta por máquina | <release>17</release> |
Ese penúltimo punto es real y se olvida siempre: si no fijas la versión de un plugin, Maven usa la última disponible y tu compilación puede cambiar de comportamiento sin que hayas tocado nada. El spring-boot-starter-parent fija las de los plugins habituales; para los demás, ponles versión.
La reproducibilidad no es teórica. El día que haya un incidente en la versión 1.4.2 que se desplegó hace ocho meses, tendrás que reconstruirla exactamente para depurarla.
- Maven frente a Gradle
Las dos herramientas dominantes. Comparación honesta:
| Aspecto | Maven | Gradle |
|---|---|---|
| Configuración | XML declarativo | DSL en Groovy o Kotlin (imperativo) |
| Verbosidad | Alta (XML) | Baja |
| Curva de aprendizaje | Baja: convenciones fijas | Media-alta: hay que aprender el DSL |
| Predecibilidad | Muy alta: ciclo de vida fijo | Menor: cualquiera puede escribir lógica |
| Compilación incremental | No | Sí |
| Caché de construcción | No | Sí, local y remota |
| Velocidad en proyectos grandes | Menor | Bastante mayor |
| Demonio en segundo plano | No | Sí |
| Flexibilidad | Limitada (plugins) | Total (es código) |
| Legibilidad por terceros | Alta: todos los POM se parecen | Variable: puede haber lógica arbitraria |
| Adopción en empresa | Mayoritaria | Creciente |
| Android | No | Estándar |
| Herramientas y documentación | Enorme | Amplia |
Un ejemplo del mismo proyecto en los dos:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Gradle es evidentemente más conciso. Y también es donde está su riesgo: como el fichero de construcción es código, un equipo puede acabar con lógica condicional, tareas propias y comportamiento que hay que leer y entender antes de tocar nada. Un pom.xml de trescientas líneas es aburrido pero se entiende de un vistazo; un build.gradle.kts de cien líneas puede hacer cualquier cosa.
Por qué este curso usa Maven:
- Es lo mayoritario en back-end empresarial Java: lo que te vas a encontrar.
- Es más fácil de aprender: no hay que aprender un lenguaje además de la herramienta.
- Es predecible: el ciclo de vida es siempre el mismo, y eso es exactamente lo que quieres cuando estás aprendiendo lo demás.
- La documentación de Spring Boot usa Maven en sus ejemplos principales.
Y una recomendación profesional: aprende Maven primero, y Gradle cuando lo necesites. Los conceptos —coordenadas, ámbitos, transitividad, ciclo de vida, plugins— son los mismos en las dos. Cambia la sintaxis.
- BiblioTech como proyecto Maven completo
El resultado. Estructura final:
bibliotech/
├── .gitignore <- target/, *.iml, .idea/
├── .mvn/wrapper/
│ └── maven-wrapper.properties
├── mvnw, mvnw.cmd
├── pom.xml
└── src/
├── main/
│ ├── java/com/nexussoftware/bibliotech/
│ │ ├── BiblioTechApplication.java
│ │ ├── config/ PropiedadesBiblioTech, ConfiguracionBiblioTech
│ │ ├── dominio/ Material, Libro, Revista, Dvd, Empleado, Prestamo, Reserva
│ │ ├── servicio/ GestorPrestamos, CalculadoraMultas, ServicioAvisos
│ │ ├── persistencia/ MaterialRepository, EmpleadoRepository, PrestamoRepository
│ │ ├── red/ ClienteMetadatos, ServidorCatalogo
│ │ ├── infraestructura/ AspectoCronometro
│ │ └── presentacion/ ArranqueBiblioTech
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── data.sql
└── test/
├── java/com/nexussoftware/bibliotech/
│ ├── dominio/ PrestamoTest, MaterialTest, IsbnTest
│ ├── servicio/ CalculadoraMultasTest, GestorPrestamosTest
│ ├── persistencia/ EscrituraAtomicaTest, PrestamoRepositoryIT
│ └── BiblioTechApplicationTests
└── resources/
└── application-test.ymlEl .gitignore mínimo:
Y el ciclo completo:
# Compilación limpia con todas las pruebas
./mvnw clean verify
# Desarrollo
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Empaquetar y ejecutar
./mvnw clean package
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --spring.profiles.active=prod
# Diagnóstico
./mvnw dependency:tree
./mvnw versions:display-dependency-updatesSalida de ./mvnw clean verify:
[INFO] --- clean:3.2.0:clean (default-clean) @ bibliotech ---
[INFO] Deleting /home/marta/bibliotech/target
[INFO] --- resources:3.3.1:resources (default-resources) @ bibliotech ---
[INFO] Copying 4 resources from src/main/resources
[INFO] --- compiler:3.13.0:compile (default-compile) @ bibliotech ---
[INFO] Compiling 31 source files with javac [debug release 17]
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ bibliotech ---
[INFO] Compiling 9 source files with javac [debug release 17]
[INFO] --- surefire:3.2.5:test (default-test) @ bibliotech ---
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.4.1:jar (default-jar) @ bibliotech ---
[INFO] Building jar: /home/marta/bibliotech/target/bibliotech-1.0.0-SNAPSHOT.jar
[INFO] --- spring-boot:3.3.2:repackage (repackage) @ bibliotech ---
[INFO] Replacing main artifact with repackaged archive
[INFO] --- failsafe:3.2.5:integration-test (default) @ bibliotech ---
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- failsafe:3.2.5:verify (default) @ bibliotech ---
[INFO] BUILD SUCCESS
[INFO] Total time: 24.183 sEn esa salida está toda la lección: las fases en orden, los plugins que ejecuta cada una, surefire con las 41 pruebas unitarias, el jar, el repackage de Spring Boot y failsafe con las 3 de integración.
Comparación con el punto de partida:
| Aspecto | javac a mano (módulos 1-10) |
Maven |
|---|---|---|
| Compilar | Comando de 15 líneas | ./mvnw compile |
| Dependencias | Descarga manual de 40 jars | 6 declaraciones en el POM |
| Transitivas | A mano, una a una | Automático |
| Conflictos | Prueba y error | Regla determinista |
| Pruebas | No había forma cómoda | ./mvnw test |
| Empaquetar | jar con manifiesto a mano |
./mvnw package |
| Otra máquina | "Funciona en la mía" | ./mvnw verify, y ya |
| Cambiar una versión | Descargar, sustituir, rezar | Una línea, verify |
| Auditoría de seguridad | Imposible | dependency:tree |
Esa penúltima fila es la respuesta completa a Log4Shell: una línea para cambiar la versión, un comando para verificar que nada se rompió.
- Errores Comunes y Consejos
Error: poner <version> a dependencias que gestiona el BOM. Anulas versiones probadas y provocas incompatibilidades sutiles que aparecen en ejecución. Si necesitas otra, sobrescribe la propiedad.
Error: usar rangos de versiones o LATEST. Rompen la reproducibilidad. Versiones concretas siempre.
Error: creer que mvn test ejecuta las pruebas de integración. Las *IT las ejecuta failsafe, en mvn verify. Y no hay ningún aviso de que no se ejecutaron.
Error: no fijar la versión de los plugins. Maven usa la última disponible y tu compilación cambia sin que toques nada.
Error: subir target/ a git. Es todo regenerable, ocupa mucho y provoca conflictos constantes.
Error: credenciales en el pom.xml. El POM va a git y queda en el historial para siempre. Van en settings.xml, y un secreto que ha estado en git hay que rotarlo.
Error: mvn clean install para todo. Es más lento de lo necesario y llena ~/.m2. Para verificar, mvn verify.
Error: -DskipTests en la integración continua. Ahí es exactamente donde deben ejecutarse.
Error: no declarar una dependencia que usas directamente. Si te llega de forma transitiva y mañana el intermediario deja de traerla, tu código deja de compilar. mvn dependency:analyze lo detecta.
Error: source/target en vez de release. Permite compilar código que usa API inexistente en la versión destino, y el fallo aparece en ejecución.
Error: olvidar project.build.sourceEncoding. Los acentos se corrompen de forma distinta según la máquina.
Consejo: usa siempre el Maven Wrapper. Elimina toda una clase de problemas de "en mi máquina funciona" y no exige instalar nada.
Consejo: aprende a leer dependency:tree. Es lo que más veces te sacará de un apuro, y es la herramienta de respuesta ante vulnerabilidades.
Consejo: mvn help:effective-pom cuando no entiendas de dónde sale algo. Muestra el POM tras la herencia, con todo resuelto.
Consejo: mvn versions:display-dependency-updates cada pocas semanas. Actualizar parches con regularidad es mucho más barato que un salto grande de emergencia.
Consejo: separa *Test de *IT. El ciclo rápido tarda segundos; la integración continua lo ejecuta todo.
Consejo: mvn -T 1C en proyectos grandes. Puede reducir el tiempo a la mitad.
Consejo: cuando algo falle de forma inexplicable, mvn -X. La salida de depuración dice de dónde sale cada configuración y cada artefacto.
- Ejercicios
Ejercicio 1: leer y arreglar un POM
Un compañero de Nexus Software ha escrito este pom.xml para un microservicio nuevo. Compila, pero tiene siete problemas. Encuéntralos, explica la consecuencia de cada uno y escribe la versión corregida.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>servicio-catalogo</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-comun</artifactId>
<version>2.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>Ejercicio 2: resolver un conflicto de dependencias
BiblioTech lanza esta excepción en ejecución, no al compilar:
java.lang.NoSuchMethodError: 'com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.databind.json.JsonMapper$Builder.build()'El árbol de dependencias, recortado:
com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
+- org.springframework.boot:spring-boot-starter-web:jar:3.3.2:compile
| \- org.springframework.boot:spring-boot-starter-json:jar:3.3.2:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
+- com.ejemplo:cliente-informes:jar:4.2.0:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
\- com.ejemplo:utilidades-nexus:jar:1.8.0:compile
\- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compileSe pide:
- Explica por qué el error aparece en ejecución y no al compilar.
- Aplica la regla de resolución: ¿qué versión gana y por qué?
- Da tres soluciones distintas con su XML, indicando ventajas e inconvenientes.
- Elige una y justifícala.
- Escribe el comando que confirma que el conflicto se ha resuelto.
Ejercicio 3: separar las pruebas y añadir cobertura
BiblioTech tiene 41 pruebas unitarias (11-04) y va a añadir pruebas de integración contra H2. Actualmente mvn test tarda 6 segundos; con las de integración pasaría a 45.
Configura el pom.xml para que:
mvn testejecute solo las unitarias (rápido, para desarrollo).mvn verifyejecute unitarias e integración.- Las de integración se llamen
*IT.javay estén etiquetadas con@Tag("integracion"). - Exista un perfil
cique además genere el informe de cobertura con JaCoCo y falle si la cobertura de instrucciones baja del 70 %. - Exista un perfil
rapidoque salte las pruebas de integración incluso enmvn verify. - Escribe los comandos de cada escenario: desarrollo, antes de subir cambios, integración continua y empaquetado urgente.
Soluciones
Solución 1
Los siete problemas:
| # | Problema | Consecuencia |
|---|---|---|
| 1 | <version>3.2.0</version> en spring-boot-starter-data-jpa |
Anula el BOM del padre (3.3.2). Mezcla Spring Data 3.2 con Spring Framework 6.1.11: incompatibilidades sutiles en ejecución |
| 2 | <version>LATEST</version> |
Eliminado en Maven 3; y aunque funcionara, rompe la reproducibilidad por completo |
| 3 | H2 sin <scope>runtime</scope> |
Se empaqueta en el classpath de compilación: alguien puede acoplarse a H2 por accidente, y la BD de pruebas viaja a producción |
| 4 | junit-jupiter sin <scope>test</scope> |
JUnit acaba dentro del jar de producción: peso y superficie de ataque innecesarios |
| 5 | JUnit con versión explícita | El BOM ya la gestiona. Y además debería usarse spring-boot-starter-test, que trae AssertJ y Mockito |
| 6 | Dependencia -SNAPSHOT en un proyecto con versión liberada 1.0.0 |
La versión 1.0.0 no es reproducible: depende de un artefacto mutable. Nunca se libera dependiendo de un SNAPSHOT |
| 7 | <source>/<target> en vez de <release> |
Permite compilar con JDK 21 código que usa API de Java 21 y generar bytecode 17: NoSuchMethodError en ejecución. Además, el padre ya lo configura con java.version |
Un octavo, menor: falta <relativePath/> en el <parent>, lo que hace que Maven busque primero un POM en ../pom.xml. No falla, pero emite un aviso y puede sorprender.
Versión corregida:
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- (8) búscalo en el repositorio -->
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>servicio-catalogo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version> <!-- (7) el padre lo aplica al compilador -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- (1) sin version: la gestiona el BOM del padre -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- (2) sin version: Jackson también lo gestiona el BOM -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- (3) runtime: no se usa al compilar -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- (4)(5) el starter de pruebas: JUnit + AssertJ + Mockito, ámbito test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- (6) versión LIBERADA de la librería interna -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-comun</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Nota sobre el punto 6: si bibliotech-comun:2.1.0 aún no está publicada, la solución correcta es publicarla antes de liberar el servicio. Mientras tanto, el servicio debe ser 1.0.0-SNAPSHOT. Es la regla de que una versión liberada solo puede depender de versiones liberadas.
Solución 2
1. Por qué en ejecución y no al compilar.
En compilación, Maven usa la versión resuelta —2.9.8, como se ve en el punto 2— y el compilador solo verifica las firmas que tu código usa. cliente-informes ya está compilado: su bytecode contiene una llamada a JsonMapper$Builder.build() que existía en 2.17 pero no en 2.9.8.
En ejecución, la JVM intenta enlazar esa llamada contra la clase que hay en el classpath, no la encuentra y lanza NoSuchMethodError. Los errores de enlace son de ejecución, no de compilación: es el mismo mecanismo del módulo 10 al hablar de la carga de clases.
2. Qué versión gana.
Las tres están a profundidad 2 o 3:
| Ruta | Profundidad | Versión |
|---|---|---|
starter-web → starter-json → jackson-databind |
3 | 2.17.2 |
cliente-informes → jackson-databind |
2 | 2.9.8 |
utilidades-nexus → jackson-databind |
2 | 2.13.0 |
Empatan cliente-informes (2.9.8) y utilidades-nexus (2.13.0) a profundidad 2. En caso de empate gana la declarada primero en el POM, y cliente-informes aparece antes. Gana 2.9.8, la más antigua de las tres.
Ese es exactamente el caso que sorprende: no gana la más nueva, gana la más cercana, y una transitiva antigua puede tumbar el proyecto.
3. Tres soluciones.
A. Declarar la dependencia directamente (profundidad 1: gana siempre):
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- sin version: la del BOM de Spring Boot (2.17.2) -->
</dependency>
...
</dependencies>A favor: simple, explícita, y dependency:analyze deja de quejarse. En contra: hay que recordar que está ahí por un conflicto; conviene un comentario.
B. dependencyManagement (gana sobre cualquier profundidad):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>A favor: es la herramienta pensada para esto, deja clara la intención y es la forma estándar de parchear una transitiva vulnerable. En contra: fija la versión a mano en vez de dejarla al BOM (mejor: sobrescribir la propiedad <jackson.version>).
C. exclusions en las dependencias problemáticas:
<dependency>
<groupId>com.ejemplo</groupId>
<artifactId>cliente-informes</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- y lo mismo en utilidades-nexus -->A favor: explícito sobre quién causaba el problema. En contra: verboso, hay que repetirlo en cada dependencia culpable, y si mañana aparece una tercera hay que acordarse.
4. Cuál elegir.
La B, con un matiz: en lugar de fijar 2.17.2 a mano, sobrescribir la propiedad del BOM.
<properties>
<!-- Forzamos la versión de Jackson: cliente-informes:4.2.0 arrastra la 2.9.8,
que carece de JsonMapper.Builder.build() (NoSuchMethodError, BIB-207) -->
<jackson.version>2.17.2</jackson.version>
</properties>Razones: dependencyManagement (que es lo que hay detrás de esa propiedad) es el mecanismo diseñado para esto, gana a cualquier profundidad, funciona aunque mañana aparezca una cuarta dependencia que traiga otra versión, y el comentario documenta el porqué para quien lo lea dentro de dos años.
Precaución obligatoria: subir a cliente-informes de Jackson 2.9.8 a 2.17.2 es un salto de ocho versiones menores. Podría usar API que cambió. Por eso este cambio debe ir acompañado de las pruebas de 11-04 ejecutándose en verde — que es exactamente el argumento con el que se cerró aquella lección.
5. Comprobación:
[INFO] +- com.ejemplo:cliente-informes:jar:4.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
- omitted for conflict with 2.17.2)Y después, mvn verify para confirmar que las 41 pruebas siguen en verde.
Solución 3
<build>
<plugins>
<!-- (1) SUREFIRE: solo pruebas UNITARIAS -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Doble seguridad: por nombre y por etiqueta -->
<excludes>
<exclude>**/*IT.java</exclude>
</excludes>
<excludedGroups>integracion</excludedGroups>
</configuration>
</plugin>
<!-- (2)(3) FAILSAFE: pruebas de INTEGRACIÓN en verify -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<includes>
<include>**/*IT.java</include>
</includes>
<groups>integracion</groups>
<skipITs>${skipITs}</skipITs> <!-- (5) controlado por perfil -->
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipITs>false</skipITs> <!-- (5) por defecto SÍ se ejecutan -->
<jacoco.version>0.8.12</jacoco.version>
</properties>
<profiles>
<!-- (4) PERFIL ci: cobertura con umbral -->
<profile>
<id>ci</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<!-- Instrumenta antes de las pruebas -->
<execution>
<id>preparar-agente</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Genera el informe HTML tras las pruebas -->
<execution>
<id>informe</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
<!-- Comprueba el umbral y FALLA si no se cumple -->
<execution>
<id>umbral-cobertura</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- (5) PERFIL rapido: sin pruebas de integración -->
<profile>
<id>rapido</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>Y la prueba de integración correspondiente:
package com.nexussoftware.bibliotech.persistencia;
import org.junit.jupiter.api.Tag;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest // (11-04) solo la capa de persistencia
@Tag("integracion") // (3) etiqueta
class PrestamoRepositoryIT { // (3) sufijo IT: la recoge failsafe, no surefire
@Autowired private PrestamoRepository repositorio;
// ...
}(6) Comandos por escenario:
| Escenario | Comando | Qué ejecuta | Tiempo |
|---|---|---|---|
| Desarrollo, tras cada cambio | ./mvnw test |
Solo las 41 unitarias | ~6 s |
| Antes de subir cambios | ./mvnw verify |
Unitarias + integración | ~45 s |
| Integración continua | ./mvnw clean verify -Pci |
Todo + cobertura con umbral | ~60 s |
| Empaquetado urgente | ./mvnw package -Prapido |
Solo unitarias, genera el jar | ~10 s |
| Emergencia real | ./mvnw package -DskipTests |
Ninguna prueba. Solo para depurar en local | ~5 s |
Detalles de la solución que conviene subrayar:
- Doble filtro en surefire (por nombre
*ITy por etiqueta). Es redundante a propósito: si alguien olvida el sufijoITpero pone la etiqueta —o al revés—, la prueba sigue quedando fuera del ciclo rápido. Los filtros redundantes en configuración de construcción son baratos y evitan sorpresas. ${skipITs}como propiedad en vez de duplicar la configuración del plugin en el perfil. El perfil solo cambia un valor.- JaCoCo solo en el perfil
ci. Instrumentar el bytecode ralentiza las pruebas; en desarrollo no aporta nada. checken la faseverify. Así la compilación falla por cobertura insuficiente después de haber ejecutado todas las pruebas, no antes.- El umbral del 70 % es una decisión de equipo, no una verdad universal. Y recuerda el aviso de 11-04: la cobertura no es calidad, y un umbral demasiado alto genera pruebas malas. Se discute a fondo en 12-05.
Conclusión
BiblioTech es ahora un proyecto Maven completo, reproducible y ejecutable en cualquier máquina del mundo que tenga Java 17.
Entiendes qué problema resuelve una herramienta de construcción: no solo compilar, sino resolver el infierno de las dependencias —los treinta artefactos transitivos que trae un solo starter, con versiones que deben ser compatibles entre sí—, ejecutar pruebas, empaquetar y hacerlo todo de forma reproducible. Y sabes por qué el javac con classpath a mano del módulo 1 dejó de ser viable en cuanto entró Spring Boot.
Conoces el principio de convención sobre configuración, que explica por qué en 11-04 pusiste las pruebas en src/test/java y mvn test las encontró sin que configuraras nada, y por qué cualquier proyecto Maven del mundo se compila igual sin leer documentación. Con su compromiso reconocido: si tu proyecto no encaja en las convenciones, Maven se vuelve incómodo.
Sabes leer y escribir un POM: las coordenadas groupId:artifactId:version que ya viste en 11-01 y su correspondencia con la ruta en ~/.m2, el packaging, la diferencia entre SNAPSHOT mutable y versión liberada inmutable —con su regla profesional: una versión liberada nunca se sobrescribe y nunca depende de un SNAPSHOT—, y las properties para centralizar versiones y fijar la codificación.
Dominas las dependencias: los tres repositorios donde se buscan, los ámbitos —incluidos los dos que ya usabas sin saberlo, runtime para H2 y test para JUnit, que es lo que evita que un framework de pruebas acabe dentro del jar de producción—, la transitividad que trae treinta jars por una declaración, y la regla de la definición más cercana con su consecuencia que sorprende a todo el mundo: no gana la más nueva, gana la más cercana, y una transitiva antigua puede provocar un NoSuchMethodError en ejecución. Sabes diagnosticarlo con dependency:tree —que además responde a la pregunta de seguridad de 11-01: "¿tengo esta librería y quién la ha metido?"— y con dependency:analyze, que detecta el riesgo real de usar directamente algo que solo tienes de forma transitiva.
Distingues dependencyManagement de dependencies y conoces sus tres usos, incluido el que importa para la seguridad: forzar la versión corregida de una transitiva vulnerable sin esperar a que el intermediario se actualice. Y entiendes por fin del todo el spring-boot-starter-parent como BOM que gestiona más de 400 artefactos, con la advertencia que arrastrabas desde 11-02 ahora justificada: poner <version> a lo que gestiona el BOM es anular versiones probadas juntas.
Conoces el ciclo de vida con sus tres cadenas y la idea clave de que ejecutar una fase ejecuta todas las anteriores —que es la respuesta a por qué mvn test compilaba tu código sin pedírselo—, y la segunda idea clave: Maven no hace nada por sí mismo, todo lo hacen los plugins enganchados a fases, invocables también directamente como plugin:goal. Sabes qué hacen los de BiblioTech, por qué <release>17</release> es mejor que source/target, y —fundamental— la diferencia entre surefire y failsafe, con la trampa en la que cae todo el mundo: mvn test no ejecuta las *IT, y no avisa de ello.
Sabes empaquetar de tres formas y por qué el jar en capas de Spring Boot es superior al uber-jar de shade, con su consecuencia para 12-06: reconstruir solo la capa que cambió convierte un despliegue de 50 MB en uno de 200 KB. Manejas perfiles de construcción —distintos de los de Spring— con el aviso de no construir artefactos diferentes por entorno. Y conoces los proyectos multimódulo, cuyo beneficio real no es organizativo sino arquitectónico: bibliotech-dominio no puede usar Spring porque no lo tiene en su classpath, y eso convierte la separación de capas en un límite verificado por la construcción.
Sabes que las credenciales van en settings.xml, nunca en el POM, y que un secreto que ha estado en git se considera comprometido para siempre. Usas el Maven Wrapper, que elimina toda una clase de problemas de "en mi máquina funciona" y permite compilar el proyecto sin instalar Maven. Tienes los comandos del día a día y las prácticas de reproducibilidad: nunca rangos, nunca LATEST, nunca SNAPSHOT en producción, versiones de plugins fijadas y codificación explícita.
Y tienes criterio para la comparación con Gradle: más conciso, con caché e incremental, bastante más rápido en proyectos grandes; pero menos predecible, porque su fichero de construcción es código que puede hacer cualquier cosa. Con la recomendación práctica: los conceptos se transfieren; aprende Maven primero.
BiblioTech se compila con ./mvnw clean verify en veinticuatro segundos: cuarenta y una pruebas unitarias, tres de integración, y un jar ejecutable de cincuenta megas que arranca en cualquier máquina con Java 17. Y cambiar la versión de una dependencia vulnerable es ahora una línea y un comando. La lección de Log4Shell está saldada.
Pero vuelve a mirar esas cuarenta y una pruebas.
Prueban CalculadoraMultas, que solo necesita un Clock. Prueban las reglas de Prestamo, que son Java puro. Prueban EscrituraAtomica con un directorio temporal. Y en cuanto apareció un colaborador de verdad —un repositorio, un servicio de avisos—, tuviste que escribir a mano una implementación en memoria y otra que registraba llamadas: veinte líneas de clases internas por prueba, con dos colaboradores sencillos. GestorPrestamos tiene cinco. PrestamoRepository es una interfaz de Spring Data con veinte métodos heredados: implementarla a mano es inviable. Y el ClienteMetadatos de 09-06 necesita una API HTTP real al otro lado.
Hay además preguntas que las aserciones sobre el resultado no responden. ¿Se envió el aviso al empleado correcto, con el texto correcto? ¿Se llamó al repositorio una sola vez o cuarenta? ¿Qué ocurre cuando la base de datos lanza una excepción —un escenario dificilísimo de provocar con una implementación real y que en producción pasará un martes cualquiera?
En la próxima lección aparecen los dobles de prueba con nombre y apellidos —dummy, stub, spy, mock y fake— y Mockito 5, que los genera por ti con una anotación, usando exactamente la generación de bytecode del módulo 10. Definirás comportamiento con when(...).thenReturn(...), provocarás fallos con thenThrow, verificarás interacciones con verify —y aprenderás el criterio de cuándo verificar y cuándo no, porque el exceso de verificación produce justo esas pruebas frágiles que se rompen al refactorizar—, capturarás con ArgumentCaptor lo que realmente se pasó al servicio de avisos, y probarás el ClienteMetadatos sin tocar la red.
Y verás algo más importante que cualquier API: que a veces la mejor respuesta no es un simulado, sino un diseño que hace innecesario el simulado. Justo como hizo el Clock fijo de 11-04.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
