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

  1. El problema: javac a mano
  2. Qué resuelve una herramienta de construcción
  3. Convención sobre configuración
  4. La estructura estándar de directorios
  5. Instalación y comprobación
  6. El POM: coordenadas
  7. SNAPSHOT frente a versión liberada
  8. properties: variables del POM
  9. El pom.xml completo de BiblioTech
  10. Dependencias: declaración y repositorios
  11. Ámbitos de dependencia
  12. Dependencias transitivas
  13. Resolución de conflictos: la definición más cercana
  14. mvn dependency:tree en la práctica
  15. exclusions
  16. dependencyManagement frente a dependencies
  17. El BOM y el spring-boot-starter-parent
  18. El ciclo de vida: tres cadenas
  19. Las fases de la cadena por defecto
  20. Plugins y goals
  21. Los plugins que usa BiblioTech
  22. surefire frente a failsafe
  23. Empaquetar: maven-jar-plugin, shade y spring-boot-maven-plugin
  24. Perfiles
  25. Proyectos multimódulo
  26. settings.xml y las credenciales
  27. El Maven Wrapper
  28. Comandos del día a día
  29. Reproducibilidad y fijación de versiones
  30. Maven frente a Gradle
  31. BiblioTech como proyecto Maven completo
  32. Errores Comunes y Consejos
  33. Ejercicios

  1. El problema: javac a mano

Volvamos al módulo 1. Así se compilaba y ejecutaba BiblioTech:

javac -d clases src/com/nexussoftware/bibliotech/*.java
java -cp clases com.nexussoftware.bibliotech.Main

Funciona 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:

  1. Copiar los recursos de src/main/resources a target/classes.
  2. Compilar las pruebas con otro classpath (el de producción más JUnit, AssertJ y Mockito).
  3. Ejecutar las pruebas invocando el lanzador de JUnit a mano.
  4. Empaquetar en un jar con un MANIFEST.MF correcto.
  5. 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.

  1. 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.

  1. 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.java es 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.

  1. 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.jar
graph 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:

  1. target/ nunca se sube a git. Es todo regenerable. Tu .gitignore debe tener target/.
  2. 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.

  1. Instalación y comprobación

mvn -version
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=false

En 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.

  1. 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:

~/.m2/repository/com/nexussoftware/bibliotech/1.0.0-SNAPSHOT/bibliotech-1.0.0-SNAPSHOT.jar

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)

  1. SNAPSHOT frente a versión liberada

La 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:

mvn -U clean install     # -U = update-snapshots

  1. 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}.

  1. El pom.xml completo de BiblioTech

Este 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.

  1. 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.

  1. Á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:

mvn dependency:build-classpath -Dmdep.outputFile=classpath.txt

  1. 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-aspects

Casi 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.

  1. Resolución de conflictos: la definición más cercana

Con treinta dependencias transitivas, es inevitable que dos pidan versiones distintas de lo mismo:

tu-proyecto
├── libreria-a  -> jackson-databind:2.15.0
└── libreria-b  -> jackson-databind:2.17.2

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:

mvn dependency:tree -Dverbose
[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.

  1. mvn dependency:tree en la práctica

Es la herramienta de diagnóstico que más veces te va a sacar de un apuro. Cuatro usos concretos:

1. Ver todo el árbol:

mvn dependency:tree

2. Buscar quién arrastra un artefacto (la pregunta de seguridad de 11-01):

mvn dependency:tree -Dincludes=org.apache.logging.log4j:log4j-core
[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:compile

Ahí 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:

mvn dependency:tree -Dverbose

4. Detectar dependencias declaradas y no usadas, o usadas y no declaradas:

mvn dependency:analyze
[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:compile

Ese 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.

  1. exclusions

A 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:

<exclusion>
    <groupId>*</groupId>
    <artifactId>*</artifactId>
</exclusion>

Úsalo con cuidado: es fácil quitar algo que sí hacía falta y descubrirlo en ejecución.

  1. dependencyManagement frente a dependencies

Dos 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:

  1. Centralizar versiones en un proyecto multimódulo: el padre las declara, los módulos las usan sin versión.
  2. 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.
  3. 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.

  1. El BOM y el spring-boot-starter-parent

Un 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>

  1. 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 orden

mvn site genera un sitio web con informes del proyecto. Se usa poco hoy; la mayoría de los equipos usan otras herramientas de informes.

  1. 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.

  1. 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 herencia

Ese ú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í.

  1. 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.

  1. 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.

  1. Empaquetar: maven-jar-plugin, shade y spring-boot-maven-plugin

Tres 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:

java -cp "target/bibliotech.jar:lib/*" com.nexussoftware.bibliotech.BiblioTechApplication

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:

mvn exec:java -Dexec.mainClass="com.nexussoftware.bibliotech.HerramientaImportacion"

Útil para utilidades y scripts de mantenimiento dentro del proyecto.

  1. 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:

mvn help:active-profiles

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).

  1. 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 todos

El 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 depende

El 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.

  1. settings.xml y las credenciales

Mientras 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 en settings.xml, y preferiblemente leídas de variables de entorno como en el ejemplo. Maven ofrece mvn --encrypt-password para 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.

  1. 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.

mvn wrapper:wrapper -Dmaven=3.9.8

Genera:

mvnw                                     <- script Unix
mvnw.cmd                                 <- script Windows
.mvn/wrapper/maven-wrapper.properties    <- la versión declarada

Y a partir de ahí:

./mvnw clean verify        # en vez de mvn clean verify
./mvnw spring-boot:run

Ventajas:

  1. Todo el mundo usa la misma versión, sin excepción.
  2. No hace falta instalar Maven para compilar el proyecto. Solo Java.
  3. La versión está en git, así que cambiarla es un commit revisable.
  4. 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.

  1. 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:

  • -DskipTests frente 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 1C puede 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:

mvn versions:display-dependency-updates
[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.3

Ejecutarlo 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.

  1. 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.

mvn versions:display-plugin-updates   # detecta plugins sin versión fijada

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.

  1. 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 (Kotlin DSL)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

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:

  1. Es lo mayoritario en back-end empresarial Java: lo que te vas a encontrar.
  2. Es más fácil de aprender: no hay que aprender un lenguaje además de la herramienta.
  3. Es predecible: el ciclo de vida es siempre el mismo, y eso es exactamente lo que quieres cuando estás aprendiendo lo demás.
  4. 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.

  1. 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.yml

El .gitignore mínimo:

target/
!.mvn/wrapper/maven-wrapper.jar
*.iml
.idea/
.classpath
.project
.settings/

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-updates

Salida 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 s

En 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ó.

  1. 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.

  1. 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:compile

Se pide:

  1. Explica por qué el error aparece en ejecución y no al compilar.
  2. Aplica la regla de resolución: ¿qué versión gana y por qué?
  3. Da tres soluciones distintas con su XML, indicando ventajas e inconvenientes.
  4. Elige una y justifícala.
  5. 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:

  1. mvn test ejecute solo las unitarias (rápido, para desarrollo).
  2. mvn verify ejecute unitarias e integración.
  3. Las de integración se llamen *IT.java y estén etiquetadas con @Tag("integracion").
  4. Exista un perfil ci que además genere el informe de cobertura con JaCoCo y falle si la cobertura de instrucciones baja del 70 %.
  5. Exista un perfil rapido que salte las pruebas de integración incluso en mvn verify.
  6. 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:

mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind -Dverbose
[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 *IT y por etiqueta). Es redundante a propósito: si alguien olvida el sufijo IT pero 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.
  • check en la fase verify. 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

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados