Día 1 de 30 Fase 0: Setup Profesional, JDK y Dominio de Maven

Instalación de JDK, Dominio de Apache Maven y Anatomía de Dependencias

Gestión de JDK 17 y 21, Maven Wrapper (mvnw), Anatomía del pom.xml, Scopes y Plugins

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 3h de estudio
  • 6 conceptos
  • 3 preguntas
  • 2 katas
JDK 17JDK 21Mavenpom.xmlScopesDependenciasBuild LifecycleGradle
Al terminar este día sabrás 4
  • Tener JDK 17 y JDK 21 instalados y saber cambiar entre ellos sin romper Maven.
  • Leer un pom.xml de Spring Boot y explicar qué hace cada bloque.
  • Predecir qué ejecuta Maven al lanzar mvn verify y por qué.
  • Elegir el scope correcto de una dependencia y diagnosticar conflictos de versiones con dependency:tree.

Gestión de Múltiples JDKs (17 y 21): JAVA_HOME y SDKMAN!

Una máquina de desarrollo a menudo necesita alternar entre Java 17 LTS (para proyectos corporativos vigentes) y Java 21 LTS (para nuevos desarrollos con Virtual Threads). SDKMAN! en Linux/macOS/WSL o gestores de variables de entorno en Windows gestionan el valor de la variable JAVA_HOME y el PATH.

En una línea sdk install java 21.0.2-tem && sdk use java 17.0.10-tem

Instalar dos JDK y fijar uno por proyecto bash
# Linux / macOS / WSL con SDKMAN!
sdk list java | grep tem          # distribuciones Eclipse Temurin disponibles
sdk install java 21.0.4-tem
sdk install java 17.0.12-tem
sdk default java 21.0.4-tem       # versión global

# Fijar la versión dentro de un proyecto concreto
echo "java=17.0.12-tem" > .sdkmanrc
sdk env                           # activa la versión del .sdkmanrc

# Comprobar QUÉ JDK usa Maven (no basta con java -version)
mvn -v
# Apache Maven 3.9.9
# Java version: 17.0.12, vendor: Eclipse Adoptium, runtime: /home/ana/.sdkman/candidates/java/17.0.12-tem

# Windows (PowerShell), solo para la sesión actual
# $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.4.7-hotspot"
# $env:Path = "$env:JAVA_HOME\bin;$env:Path"
Cómo funciona por dentro

Cuando escribes java o javac en la terminal, el sistema operativo busca el primer ejecutable con ese nombre en el PATH. Maven, en cambio, no mira el PATH: usa la variable JAVA_HOME para localizar el compilador. Por eso es posible que java -version diga 21 y Maven compile con 17. El primer diagnóstico siempre es mvn -v, que imprime la versión de Maven y, debajo, la ruta y versión del JDK que realmente está usando.

Cada versión del JDK genera bytecode con un número de versión de clase (61 para Java 17, 65 para Java 21). Una JVM solo ejecuta clases de su versión o anteriores. Si compilas con 21 y despliegas en un servidor con Java 17, el arranque falla con UnsupportedClassVersionError. La opción <release> del compilador evita el problema: además de fijar la versión del bytecode, restringe las APIs del JDK a las que existían en esa versión.

SDKMAN! (Linux, macOS, WSL) instala varios JDK en paralelo y cambia JAVA_HOME por ti. Con un archivo .sdkmanrc en la raíz del proyecto, sdk env activa automáticamente la versión correcta al entrar en la carpeta. En Windows nativo el equivalente es fijar JAVA_HOME por terminal o usar un gestor como Scoop.

Si JAVA_HOME no apunta al JDK correcto, Maven compilará con una versión inesperada, provocando errores de versión de bytecode (UnsupportedClassVersionError).

Se conecta con Compilación de BytecodeVariables de EntornoEclipse Temurin / Corretto

Compruébalo: java -version muestra 21, pero mvn -v muestra 17. ¿Qué está pasando?

El PATH apunta a un JDK 21, pero JAVA_HOME sigue apuntando a un JDK 17. Maven solo mira JAVA_HOME. Corrige la variable (o usa sdk use) y vuelve a comprobar con mvn -v.

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Configuración de JDK y Maven Wrapper (0 - 25 min)

    Qué estudiar. Verificación de java -version, javac -version, estructura del script mvnw y archivo .mvn/wrapper/maven-wrapper.properties.

    Pregunta a responder. ¿Cómo asegurar que cualquier persona del equipo use Java 17 o 21 sin configurar Maven manual?

    Resultado esperado. Proyecto base generado con ./mvnw --version reportando compatibilidad con el JDK instalado.

  2. Bloque 2: Anatomía del pom.xml y Fases del Ciclo de Vida (25 - 50 min)

    Qué estudiar. Estructura de src/main/java, src/test/java, etiquetas groupId, artifactId, version, y ejecución de mvn clean compile, mvn test, mvn package.

    Pregunta a responder. ¿Qué archivos se generan en la carpeta target/ y cómo limpiarlos?

    Resultado esperado. Compilación limpia con ./mvnw clean package generando el artefacto .jar en target/.

  3. Bloque 3: Scopes y Diagnóstico con dependency:tree (50 - 80 min)

    Qué estudiar. Diferencias entre scopes compile, test, runtime, provided. Uso de ./mvnw dependency:tree para auditar librerías transitivas.

    Pregunta a responder. ¿Qué librería introduce dependencias no deseadas o vulnerables en el proyecto?

    Resultado esperado. Salida de dependency:tree analizada con dependencias de test correctamente aisladas.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Repaso de las 5 reglas de oro de Maven y configuración del plugin de compilador con <release>17</release> o <release>21</release>.

    Pregunta a responder. ¿Por qué <release> es superior a <source> y <target> antiguos en Java moderno?

    Resultado esperado. Pom.xml optimizado sin dependencias con versiones quemadas a mano.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué Maven utiliza un repositorio local (~/.m2) y cómo resuelve dependencias

Lo que debes entender

Cuando declaras una dependencia, Maven no la descarga una y otra vez para cada proyecto. La busca primero en tu caché local (~/.m2/repository). Si no está, la descarga desde Maven Central (o el Nexus corporativo) y la almacena allí. Si 10 proyectos usan Spring Boot 3.2.0, el binario solo se almacena una vez en el disco de tu máquina.

Analogía para contarlo

Como una biblioteca municipal: si ya tienes el libro prestado en tu estantería local (~/.m2), lo lees de inmediato; si no, vas a la biblioteca central a pedir una copia.

Lo que suele explicarse mal

Creer que pom.xml "instala" cosas como si fuera un gestor de paquetes de sistema operativo (apt o brew). En realidad solo define un grafo de dependencias para el classpath del proyecto.

Señal de que aún no lo dominas

Si borras el directorio de tu proyecto pensando que eso elimina las dependencias descargadas de tu ordenador, o si no sabes dónde inspeccionar el archivo .m2 cuando hay un jar corrupto.

Diferencia entre <dependencies> y <dependencyManagement> (BOMs)

Lo que debes entender

<dependencies> añade librerías reales al classpath. <dependencyManagement> NO añade nada; solo define las versiones recomendadas y compatibles de las dependencias. Spring Boot utiliza una BOM (Bill of Materials) dentro de dependencyManagement para que tú puedas declarar spring-boot-starter-web sin especificar <version>, garantizando que todas las librerías de Spring sean 100% compatibles entre sí.

Analogía para contarlo

<dependencyManagement> es el menú de precios del restaurante; <dependencies> es el plato que realmente pides a la mesa.

Lo que suele explicarse mal

Poner números de versión manuales a cada dependencia de Spring Boot, anulando la resolución automática del starter y provocando colisiones de versiones (jar hell).

Señal de que aún no lo dominas

Ver un pom.xml de Spring Boot lleno de etiquetas <version>3.2.1</version> en dependencias estándar oficiales.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Qué diferencia práctica hay entre una dependencia con <scope>test</scope> y una con <scope>runtime</scope>? Ver respuesta

Respuesta razonada

Las dos dependencias se excluyen de algún sitio, pero de sitios distintos. Con test, la dependencia está disponible para compilar y ejecutar src/test/java y no se empaqueta. Con runtime, no está disponible para compilar src/main/java (no puedes escribir import de sus clases), pero sí se empaqueta y está en el classpath al arrancar la aplicación.

Ejemplo: JUnit va en test porque producción nunca lo necesita. El driver de PostgreSQL va en runtime porque producción lo necesita para conectarse, pero tu código solo debe depender de las interfaces de java.sql o de JPA.

Qué está evaluando la pregunta

Evalúa si entiendes el Classpath de Java. test permite importar las clases en src/test/java pero las excluye del empaquetado final. runtime impide importar las clases directamente en el código Java durante la compilación (evita acoplamiento a una implementación concreta), pero incluye el JAR en la ejecución final (típico del driver JDBC de PostgreSQL o MySQL).

Nivel 2 (Intermedio) Si dos librerías en tu pom.xml dependen de versiones distintas de jackson-databind (una de 2.15.0 y otra de 2.16.0), ¿cómo decide Maven cuál usar y cómo inspeccionas el conflicto? Ver respuesta

Respuesta razonada

Maven aplica la regla de mediación por cercanía: gana la versión más cercana a tu proyecto en el árbol de dependencias. Si están a la misma profundidad, gana la que se declaró primero en el pom.xml. No elige la más moderna, lo que puede provocar NoSuchMethodError en ejecución si una librería necesitaba una API de la versión que perdió.

Para inspeccionarlo: ./mvnw dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core muestra qué versión se omitió y por qué (omitted for conflict with). Para resolverlo, fija la versión en <dependencyManagement> (o confía en el BOM de Spring Boot) y, en proyectos grandes, activa la regla dependencyConvergence del maven-enforcer-plugin para que el build falle ante conflictos.

Qué está evaluando la pregunta

Evalúa el conocimiento del algoritmo de resolución de dependencias de Maven: "Nearest definition wins" (la definición más cercana en el árbol de dependencias gana; si están al mismo nivel, gana la primera declarada). Se inspecciona ejecutando ./mvnw dependency:tree.

Nivel 3 (Entrevista Senior) ¿Para qué sirve el plugin spring-boot-maven-plugin:repackage y por qué un JAR normal generado por Maven no arranca directamente con java -jar? Ver respuesta

Respuesta razonada

Un JAR normal de Maven solo contiene tus clases compiladas: ni las dependencias ni un Main-Class en el MANIFEST.MF. Al hacer java -jar la JVM no sabe qué clase arrancar y, aunque lo supiera, faltarían Spring, Tomcat, Jackson, etc.

El goal repackage del spring-boot-maven-plugin se ejecuta después de package y reescribe el JAR: mete tus clases en BOOT-INF/classes, las dependencias en BOOT-INF/lib y pone como Main-Class el JarLauncher de Spring Boot, que crea un classloader capaz de leer JARs anidados y luego invoca tu clase @SpringBootApplication (indicada como Start-Class). El JAR original se conserva como *.jar.original.

Qué está evaluando la pregunta

Evalúa la arquitectura de los Fat JARs / Uber JARs. Un JAR estándar de Maven solo contiene las clases compiladas de tu proyecto (src/main/java), sin incluir los JARs de dependencias externas. El plugin de Spring Boot reempaqueta el JAR creando una estructura especial (BOOT-INF/classes, BOOT-INF/lib) e inyectando un cargador de clases (org.springframework.boot.loader.JarLauncher) capaz de leer JARs anidados dentro de un JAR.

Reto del tutor

Un desarrollador junior añade una dependencia en su pom.xml para conectar a Oracle Database, pero escribe <scope>provided</scope> porque "el servidor de la empresa ya tiene instalada la base de datos Oracle". ¿Qué ocurrirá cuando compile y cuando ejecute la aplicación con java -jar localmente? Corrige su razonamiento como un líder técnico.

Lo que una respuesta suele tener bien

Identifica que provided significa que el entorno de ejecución (JVM) debe proporcionar el JAR, y que una base de datos externa no es lo mismo que el driver JDBC en la JVM.

Lo que suele quedarse en la superficie

Decir que "dará error de compilación".

El concepto de fondo que suele faltar

No comprender que el código compilará sin ningún error (si solo usa interfaces estándar de java.sql), pero al arrancar en local fallará inmediatamente con ClassNotFoundException: oracle.jdbc.OracleDriver porque el driver no fue empaquetado en el JAR.

Cómo lo respondería alguien senior en una entrevista

Confundió el motor de base de datos con el conector de software (driver JDBC). El servidor Oracle puede estar corriendo en la red, pero la JVM de la aplicación necesita el archivo JAR del driver (ojdbc11) en su classpath para poder comunicarse por TCP/IP. Al usar provided, Maven no mete el driver dentro del archivo JAR ejecutable, esperando que un servidor de aplicaciones antiguo (como Tomcat o WebLogic) lo inyecte en su carpeta /lib. En una aplicación moderna autocontenida de Spring Boot 3, el driver debe tener scope runtime para que viaje empaquetado dentro de BOOT-INF/lib/.

Proyecto «Pedidos» · paso 1 de 30

Crear el proyecto

Tener un proyecto Maven que cualquiera pueda clonar y compilar con el mismo comando.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Genera el proyecto pedidos con el Maven Wrapper y maven.compiler.release en 21.
  2. Crea los paquetes com.tienda.pedidos.dominio y com.tienda.pedidos.aplicacion.
  3. Añade JUnit 5 y AssertJ con scope test y un test vacío que pase.
  4. Sube el proyecto a un repositorio Git con un .gitignore de Maven.

Está listo cuando

  • ./mvnw verify termina en verde en un ordenador sin Maven instalado.
  • ./mvnw dependency:tree no muestra dependencias de test en el scope compile.

Katas: ejercicios cortos

Reglas de oro

  • Usa SIEMPRE el Maven Wrapper (./mvnw) en tus repositorios; jamás dependas del mvn que el desarrollador tenga instalado globalmente.
  • Las librerías de prueba (JUnit, Mockito, AssertJ, Testcontainers) deben tener SIEMPRE <scope>test</scope>.
  • Los drivers JDBC (PostgreSQL, H2, MySQL) se declaran con <scope>runtime</scope> para no acoplar el código de compilación a la implementación del vendor.
  • Jamás pongas números de versión fijos en librerías gestionadas por el starter de Spring Boot (spring-boot-starter-parent); deja que la BOM resuelva la versión compatible.
  • Ejecuta periódicamente ./mvnw dependency:tree para detectar conflictos de versiones transitivas (JAR Hell) antes de subir a producción.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Configuración Compilador <release>17</release> <release>21</release>
Versión Bytecode (.class) Clase versión 61 Clase versión 65
Herramienta de Construcción Recomendada Maven 3.8.6+ o Gradle 7.3+ Maven 3.9.5+ o Gradle 8.5+
Soporte Spring Boot 3.x Línea base mínima requerida Soporte nativo completo para Virtual Threads
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown