La lección anterior recogió las prácticas de CicloUrbana escritas en positivo: lo que conviene hacer y por qué. Esta recorre el reverso, que es como casi todos aprendemos de verdad. Cada práctica de 10-01 es la cicatriz de un error concreto que alguien cometió antes, y conocer el error —su síntoma exacto, su mensaje en el log, la forma que tiene— vale más que conocer la regla, porque el día que aparezca lo reconocerás en lugar de investigarlo durante tres horas.
El catálogo está ordenado por dónde aparecen, no por gravedad, porque así es como se encuentran: primero los que impiden arrancar, luego los que arrancan pero no hacen nada, y por último los que funcionan hasta que llega el volumen. Todos siguen el mismo esquema —síntoma → causa → diagnóstico → solución— y todos llevan el código que falla junto al corregido. Y hay una sección destacada para la trampa que hemos pisado tres veces a lo largo del curso y que merece por fin un tratamiento único: la del proxy.
Contenido
- Cómo leer este catálogo
- Arranque y contexto
- La trampa del proxy
- Configuración
- JPA y persistencia
- REST y el contrato
- Seguridad
- Concurrencia
- Rendimiento
- Pruebas
- Tabla de diagnóstico rápido
- Errores Comunes y Consejos
- Ejercicios
- Cómo leer este catálogo
Los errores de este catálogo se dividen en tres familias que exigen actitudes distintas:
| Familia | Cómo se manifiesta | Coste real |
|---|---|---|
| Ruidosos | La aplicación no arranca o lanza una excepción clara | Bajo: molestan una tarde y se arreglan |
| Silenciosos | Todo funciona, pero no hace lo que crees | Alto: se descubren en producción, semanas después |
| Diferidos | Funcionan hoy y fallan con el volumen o la concurrencia | Muy alto: aparecen en el peor momento |
La lista está sesgada a propósito hacia las dos últimas. Un NoSuchBeanDefinitionException cuesta veinte minutos; un @Transactional que se ignora en silencio cuesta una reconciliación de datos.
- Arranque y contexto
2.1. La clase principal fuera del paquete raíz
Síntoma. NoSuchBeanDefinitionException sobre beans que existen y están correctamente anotados. O peor: la aplicación arranca, pero ningún endpoint responde y no hay error alguno.
Causa. @SpringBootApplication incluye @ComponentScan sin argumentos, lo que escanea el paquete de la clase anotada y todos sus subpaquetes. Si CicloUrbanaApplication vive en com.ciclourbana.arranque, los paquetes com.ciclourbana.estaciones y com.ciclourbana.alquileres quedan fuera del escaneo.
Diagnóstico. Compara el paquete de la clase principal con el de los beans que no aparecen, y confirma con /actuator/beans, que no los lista.
Solución. Mover CicloUrbanaApplication a com/ciclourbana/, raíz de todos los paquetes de negocio. Añadir @ComponentScan("com.ciclourbana") funciona, pero es un parche que enmascara una estructura mal puesta.
2.2. NoSuchBeanDefinitionException con la clase delante
Síntoma. Parameter 0 of constructor in com.ciclourbana.alquileres.AlquilerService required a bean of type 'CalculadoraTarifa' that could not be found.
Causas posibles, en orden de frecuencia. La clase no tiene estereotipo (@Component, @Service, @Repository); está fuera del paquete escaneado (2.1); está condicionada por un @Profile o @ConditionalOnProperty que no se cumple; o se define con @Bean en una clase que no es @Configuration.
Diagnóstico. Arrancar con --debug imprime el informe de autoconfiguración, que lista las condiciones evaluadas positivas y negativas con su motivo. Es la herramienta más infrautilizada de Spring Boot.
2.3. Dependencias circulares
Síntoma. El recuadro ┌─────┐ ... └─────┘ con la lista de beans del ciclo, y APPLICATION FAILED TO START.
Causa. A necesita B y B necesita A. Con inyección por constructor es lógicamente imposible.
Diagnóstico. El propio mensaje dice qué beans forman el ciclo y en qué fichero están definidos.
Solución. Las tres de Inyección de Dependencias: extraer la responsabilidad compartida a un tercer componente, invertir la dirección con un evento, o replantear quién debe llamar a quién.
Lo que no es solución. spring.main.allow-circular-references=true ni @Lazy en uno de los dos puntos: hacen desaparecer el mensaje sin arreglar nada, y el ciclo sigue produciendo un orden de inicialización imprevisible.
2.4. Dos candidatos sin @Primary
Síntoma. required a single bean, but 2 were found: tarifaEstandar, tarifaEstudiante.
Solución. @Primary sobre la implementación dominante cuando existe una, @Qualifier en el punto de inyección cuando hace falta una concreta, o —lo más robusto en proyectos grandes— un cualificador propio como @Universitaria, que el compilador verifica.
El error asociado, más peligroso. Confiar en la coincidencia de nombres: llamar al parámetro tarifaEstudiante para que Spring elija ese bean. Funciona, y se rompe en silencio el día que un IDE renombra el parámetro.
- La trampa del proxy
Esta sección unifica lo que hasta ahora hemos visto tres veces por separado, en Transacciones, en Seguridad a Nivel de Método y en Tareas Programadas y Asincronía. Es el error más frecuente de todo el curso y el más silencioso.
3.1. Por qué ocurre
Ninguna de estas anotaciones modifica tu código. Spring crea un proxy —una subclase generada por CGLIB— que envuelve tu bean, y lo que se inyecta en el resto de la aplicación no es tu objeto: es el proxy.
flowchart LR
C["AlquilerController"] -->|"llamada externa:<br/>SÍ atraviesa el proxy"| P["Proxy de AlquilerService"]
P -->|"abre transacción,<br/>comprueba permisos,<br/>consulta la caché"| S["AlquilerService (objeto real)"]
S -->|"this.metodo():<br/>NO atraviesa el proxy"| S
De ahí la regla única que explica los cuatro casos: solo las llamadas que entran desde fuera pasan por el proxy. Una llamada de un método de la clase a otro de la misma clase va por this y la anotación se ignora por completo y sin ningún aviso.
3.2. Los cuatro casos, con su síntoma característico
| Anotación | Qué deja de ocurrir | Síntoma real |
|---|---|---|
@Transactional |
No hay transacción | Cada operación se autoconfirma; un fallo a medias deja datos inconsistentes sin error |
@Async |
No hay asincronía | El método se ejecuta síncrono; la latencia no baja por muchos hilos que añadas |
@Cacheable |
No hay caché | La tasa de aciertos es 0 % y nadie entiende por qué |
@PreAuthorize |
No hay comprobación de permisos | Ninguno. Y ese es exactamente el problema |
El último es el más grave del curso: una regla de seguridad que no se ejecuta y no avisa.
// ❌ Las tres anotaciones internas se ignoran
@Service
public class AlquilerService {
@Transactional
public void finalizarLote(List<Long> ids) {
for (Long id : ids) {
finalizar(id, peticion); // this.finalizar(...): sin proxy
}
}
@PreAuthorize("@seguridadAlquileres.esPropietario(#idAlquiler, principal)")
@Transactional
public AlquilerResponse finalizar(Long idAlquiler, FinalizarAlquilerRequest p) { ... }
}3.3. Los otros dos disparadores: visibilidad y final
Con proxies CGLIB, Spring genera una subclase que sobrescribe los métodos. De ahí:
| Situación | Resultado |
|---|---|
Método private |
No se puede sobrescribir: la anotación se ignora en silencio |
Método protected o de paquete |
No se intercepta de forma fiable |
Método final |
No se puede sobrescribir: se ignora |
Clase final |
No se puede crear el proxy: fallo de arranque o bean sin interceptar |
Objeto creado con new |
No es un bean: no hay proxy ni ninguna anotación funciona |
Llamada desde el constructor o @PostConstruct |
El proxy aún no está montado |
Desde Spring Framework 6.0 el arranque registra un aviso al detectar @Transactional en un método no público. Es una ayuda, no una garantía.
3.4. Las tres formas de resolverlo
A. Extraer a otro bean (la recomendada). Además de funcionar, casi siempre mejora el diseño: orquestar un lote y ejecutar la operación individual son responsabilidades distintas.
// ✅ La llamada sale de un bean y entra en otro: atraviesa el proxy
@Service
public class ProcesadorDevoluciones {
private final AlquilerService alquilerService; // el PROXY, no el objeto real
public void procesarLote(List<Long> ids) {
ids.forEach(id -> alquilerService.finalizar(id, peticion));
}
}B. Autoinyección del proxy de sí mismo. Funciona y es fea: un campo @Lazy private final AlquilerService self y llamar a self.finalizar(...); el @Lazy rompe el ciclo. Se usa cuando extraer no compensa, y conviene comentar por qué. C. Gestión programática. Para @Transactional, TransactionTemplate no depende de proxies y da control exacto del alcance; es la salida cuando hace falta una transacción por iteración de un bucle.
Cómo confirmar que el proxy actúa. Para transacciones, logging.level.org.springframework.transaction: DEBUG debe imprimir Creating new transaction donde lo esperas. Para caché, /actuator/metrics/cache.gets. Para tareas, /actuator/scheduledtasks. Si no aparece nada, es esta trampa.
- Configuración
4.1. El perfil que nunca se activó
Síntoma. En producción: los datos desaparecen al reiniciar, Swagger es accesible desde Internet, el log crece sin control y /actuator/env devuelve 200 con la contraseña visible.
Causa. Un solo fallo: java -jar ciclourbana.jar sin --spring.profiles.active y sin SPRING_PROFILES_ACTIVE en el entorno. application-prod.yml está dentro del JAR pero nunca se lee.
Diagnóstico. El log de arranque contiene la prueba literal:
Solución. Fijar el perfil en el entorno del contenedor o del servicio, y —esto es lo que impide que vuelva a ocurrir— una comprobación en el arranque que falle si el perfil activo está vacío en un entorno que no sea local.
4.2. La propiedad mal escrita que se ignora en silencio
Síntoma. Se cambia un valor en el YAML y no pasa nada.
Causa. Spring Boot no valida que las propiedades de un fichero se correspondan con algo. ciclourbana.tarifa.precio-minuo: 0.15 simplemente no se enlaza a nada, y el valor por defecto sigue vigente.
Diagnóstico. /actuator/configprops muestra los valores efectivos de cada @ConfigurationProperties; /actuator/env muestra de qué fuente sale cada propiedad. Si el valor efectivo no es el que escribiste, la clave está mal.
Solución. Dos, complementarias. El procesador de metadatos (spring-boot-configuration-processor), que hace que el IDE autocomplete y subraye en rojo lo que no existe. Y @ConfigurationProperties en lugar de @Value, porque el enlace de un record con @Validated falla al arrancar si un campo obligatorio queda a nulo.
4.3. YAML mal indentado y la precedencia inesperada
Síntoma A. Una rama entera de configuración se ignora. Causa: un nivel de indentación de más o de menos convierte hikari en hijo de la clave equivocada. YAML es sensible a los espacios y no admite tabuladores.
Síntoma B. El valor del fichero no gana. Causa: la precedencia, de mayor a menor: argumentos de línea de comandos → variables de entorno → ficheros externos → ficheros dentro del JAR. Una SPRING_DATASOURCE_URL heredada del entorno gana siempre al fichero, y es una fuente clásica de desconcierto en contenedores.
Síntoma C. Una lista tiene menos elementos de los esperados. Causa: al combinar perfiles, las listas y los mapas se sustituyen enteros, no se fusionan.
4.4. Secretos versionados
Síntoma. Ninguno. Ese es el problema.
Causa. Una contraseña «temporal» en un application-prod.yml, un .env sin .gitignore, o un COPY . . en el Dockerfile sin .dockerignore que mete el directorio .git completo dentro de la imagen.
Diagnóstico y solución. Una herramienta de detección de secretos sobre el historial completo (--log-opts="--all"), no sobre la copia de trabajo. Y después, rotar el secreto: borrarlo del código no lo desactiva, porque sigue en el historial de Git, en cada clon y en cada imagen construida desde entonces.
- JPA y persistencia
5.1. LazyInitializationException
Síntoma. failed to lazily initialize a collection of role: ... could not initialize proxy - no Session.
Causa. Se accede a una relación perezosa fuera de la transacción, típicamente durante la serialización JSON, porque open-in-view: false cierra el contexto de persistencia al terminar el método del servicio.
Solución. No es cargar más cosas: es mapear a DTO dentro de la transacción. La entidad no debe salir del servicio.
// ❌ La entidad sale con relaciones sin cargar
@Transactional(readOnly = true)
public Estacion obtener(Long id) { return estacionRepositorio.findById(id).orElseThrow(); }
// ✅ El mapeo ocurre con la transacción abierta
@Transactional(readOnly = true)
public EstacionDetalleResponse obtenerDetalle(Long id) {
Estacion estacion = estacionRepositorio.buscarConBicicletas(id)
.orElseThrow(() -> new RecursoNoEncontradoException("Estación", id));
return mapper.aDetalle(estacion);
}Lo que no es solución. open-in-view: true ni marcar la relación como EAGER. La primera oculta el problema detrás de la serialización; la segunda lo convierte en el 5.2.
5.2. El N+1 silencioso y el EAGER por defecto
Síntoma. El endpoint funciona y la latencia crece proporcionalmente al número de resultados. Con 20 estaciones tarda 200 ms; con 200, dos segundos.
Causa. Una consulta para la lista y una más por cada elemento. Y una causa estructural que mucha gente desconoce: @ManyToOne y @OneToOne son EAGER por defecto en JPA, así que una relación que nadie declaró perezosa dispara una consulta por fila sin que aparezca en el código.
Diagnóstico.
201 sentencias para 200 estaciones no necesita más análisis. Y en las trazas de 09-06 se ve directamente: veinte spans select idénticos y consecutivos.
Solución. Declarar todas las asociaciones FetchType.LAZY, y cargar lo que se necesite con @EntityGraph, JOIN FETCH o —lo mejor cuando solo se va a mostrar— una proyección que haga contar a la base de datos.
Cómo evitar que vuelva. Una prueba con @DataJpaTest que afirme el número de consultas: assertThat(contador.getTotal()).isLessThanOrEqualTo(2). Sin esa prueba, el N+1 regresa dentro de tres meses.
5.3. equals y hashCode mal implementados en entidades
Síntoma. Una entidad añadida a un HashSet no se encuentra después de guardarla, o un Set con duplicados aparentes.
Causa. El equals generado por el IDE incluye el id, que es null antes de persistir y deja de serlo después: el hashCode cambia mientras el objeto está dentro del HashSet, y el elemento queda inaccesible en un cubo equivocado.
Solución. equals basado en el id, tolerante a proxies, y un hashCode constante por clase:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Estacion otra)) return false;
return id != null && id.equals(otra.getId()); // sin id, solo igual a sí misma
}
@Override
public int hashCode() { return getClass().hashCode(); } // estable siempreY la regla práctica que evita el problema entero: no metas entidades sin persistir en un HashSet.
5.4. save() innecesario y las cascadas que borran de más
Síntoma A. Nada visible; solo ruido. Causa: llamar a save() sobre una entidad ya gestionada. El dirty checking genera el UPDATE en el commit sin que nadie lo pida; el save() es redundante y hace creer al lector que sin él no se guardaría.
Síntoma B. Al borrar una estación desaparecen sus bicicletas, y con ellas el histórico. Causa: cascade = CascadeType.ALL con orphanRemoval = true puesto «para que funcione el guardado», sin reparar en que ALL incluye REMOVE.
// ❌ Borrar la estación borra su flota
@OneToMany(mappedBy = "estacion", cascade = CascadeType.ALL, orphanRemoval = true)
// ✅ Solo lo que tiene sentido: una bicicleta sobrevive a su estación
@OneToMany(mappedBy = "estacion", cascade = {CascadeType.PERSIST, CascadeType.MERGE})El criterio: la cascada de borrado solo es correcta cuando el hijo no tiene sentido sin el padre —las líneas de una factura—, y una bicicleta de Ribalta existe con independencia de dónde esté anclada.
5.5. La transacción que no revierte
Síntoma. El alquiler queda creado aunque el pago fallara. O bien, UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only.
Causa. Se capturó la excepción y no se relanzó. Sin excepción que llegue al proxy, el proxy hace commit. Y si la excepción se lanzó en un método interno también transaccional, este ya marcó la transacción para rollback, de modo que el commit final estalla.
// ❌ El alquiler se registra sin cobrar
try { pasarelaPago.cobrar(importe); }
catch (PagoRechazadoException e) { log.error("Pago rechazado", e); }
// ✅ Relanzar como excepción del dominio
catch (PagoRechazadoException e) {
log.error("Pago rechazado para el alquiler {}", alquiler.getId(), e);
throw new ReglaNegocioException("PAGO_RECHAZADO", "El pago ha sido rechazado");
}La variante hermana: esperar rollback con una excepción comprobada. Por defecto Spring solo revierte ante RuntimeException y Error. En CicloUrbana no da problemas porque toda la jerarquía de CicloUrbanaException hereda de RuntimeException; donde no sea así, hace falta rollbackFor = Exception.class. Y la más sutil: @Transactional sobre un método void cuyo cuerpo captura y registra todos los fallos: no lanza nunca, la transacción siempre confirma y el llamante no tiene forma de saber que no se hizo nada.
5.6. El OFFSET grande
Síntoma. La página 1 vuela; la página 10 000 tarda segundos. Causa: LIMIT 20 OFFSET 200000 obliga a PostgreSQL a producir y descartar 200 000 filas antes de devolver 20.
Solución. Paginación por keyset: el cliente envía la última fila vista y la consulta se posiciona en el índice en lugar de contar. Y para listados donde el total no se necesita, Slice en lugar de Page, porque el count(*) de cada Page suele ser más caro que la consulta de datos.
- REST y el contrato
6.1. Exponer entidades y sus tres consecuencias
Síntoma A: fuga de datos. GET /api/v1/usuarios/1 devuelve contrasenaHash y el DNI de un ciudadano. Nadie lo decidió: el mecanismo por defecto es publicar.
Síntoma B: referencias circulares. StackOverflowError o una respuesta de varios megabytes al serializar Estacion → bicicletas → Bicicleta → estacion → ....
Síntoma C: la app móvil deja de mostrar un campo. Alguien renombró una propiedad del dominio y el JSON cambió con ella.
Solución. DTOs, sin la excepción «solo en este endpoint». Y para el caso C, una prueba de contrato que afirme exactamente qué claves devuelve la respuesta pública: si alguien añade un campo interno, falla antes del despliegue.
6.2. DTOs sin validar y el mass assignment
Síntoma. Llega un null donde no debería, o un usuario se registra con {"roles":["ADMIN"]}.
Causa. Falta @Valid en el @RequestBody —las restricciones del DTO están, pero nadie las evalúa—, o el DTO de entrada tiene campos que el cliente no debería poder fijar.
La regla que lo resuelve entero: lo que no está en el DTO de petición no se puede modificar. ActualizarUsuarioRequest contiene nombre y tipoTarifa; ni roles, ni contraseña, ni activo, ni id.
6.3. Devolver 200 para todo y tragarse las excepciones
Síntoma. El cliente recibe 200 con un cuerpo vacío o {"error": "algo ha ido mal"}, y las métricas de 09-03 muestran cero errores mientras los ciudadanos se quejan.
Causa. Un try { ... } catch (Exception e) { return ResponseEntity.ok().build(); } en el controlador, o un @ExceptionHandler que devuelve siempre el mismo código.
Por qué importa más de lo que parece. El código de estado gobierna los reintentos de los clientes, el comportamiento de cachés y proxies, y todas las alertas de disponibilidad. Un error devuelto como 200 es invisible para la monitorización.
Solución. Un @RestControllerAdvice único que traduzca cada excepción del dominio a su código: 404 para RecursoNoEncontradoException, 409 para ConflictoRecursoException, 422 para ReglaNegocioException, 400 para la validación.
6.4. Serializar Page directamente
Síntoma. Un aviso en el arranque —Serializing PageImpl instances as-is is not supported— y un JSON con pageable, sort.sorted y otros campos internos de Spring Data convertidos, sin que nadie lo decida, en parte del contrato público.
Solución. Un envoltorio propio, que es lo que hace PaginaResponse<T>: contenido, página, tamaño, total de elementos y total de páginas, y nada más. El contrato deja de depender de la estructura interna de una librería.
- Seguridad
7.1. El orden de las reglas deja un endpoint abierto
Síntoma. Un endpoint que debería requerir rol responde 200 sin autenticación.
Causa. authorizeHttpRequests evalúa en orden y gana la primera coincidencia. Una regla general colocada antes que una específica hace que la específica nunca se evalúe.
Diagnóstico. Una prueba con spring-security-test que recorra una lista de rutas sin token y afirme 401/403 en todas. Es la única forma fiable: leer el orden a ojo funciona mal en cuanto hay más de seis reglas.
Y la regla estructural: terminar con anyRequest().denyAll(). Con denegación por defecto, olvidar una regla produce un 403 visible; con permiso por defecto, produce un agujero.
7.2. CSRF desactivado sin entender por qué
Síntoma. Ninguno inmediato. El riesgo aparece si cambia el mecanismo de autenticación.
El razonamiento correcto. CSRF protege contra que el navegador adjunte automáticamente una credencial a una petición originada en otro sitio, y eso ocurre con cookies de sesión. Con un token Bearer que el cliente adjunta explícitamente, el ataque no aplica y desactivarlo es correcto.
Dónde está la trampa: el día que alguien decida guardar el JWT en una cookie —una decisión razonable por otros motivos—, CSRF vuelve a ser necesario y nadie lo recordará. Por eso la línea que lo desactiva debe llevar escrita su condición: // sin CSRF: la credencial viaja en Authorization, no en cookie.
7.3. Contraseñas sin codificar y JWT mal construidos
| Error | Síntoma | Solución |
|---|---|---|
| Guardar la contraseña en claro o con MD5/SHA-1 | Ninguno, hasta la filtración | BCrypt con coste revisado, o Argon2 |
Comparar contraseñas con equals |
Vulnerable a ataques de tiempo | Siempre passwordEncoder.matches(...) |
| JWT sin caducidad | Un token robado sirve para siempre | exp corto (≤15 min) y refresco rotatorio |
| Datos sensibles en el payload | El JWT es legible, solo va firmado | Solo identificador y roles |
| Secreto de firma corto o en el repositorio | Se puede falsificar cualquier token | ≥256 bits, aleatorio, por variable de entorno y rotable |
El segundo punto de la tabla merece énfasis porque es contraintuitivo: un JWT no está cifrado. Cualquiera puede decodificar su carga útil en un navegador. La firma garantiza que no se ha manipulado, no que nadie pueda leerlo.
7.4. Filtrar el mensaje de error interno
Síntoma. La respuesta contiene ERROR: relation "usuarios" does not exist, una traza de pila o una ruta del sistema de ficheros. Causa: devolver e.getMessage() de una excepción de origen desconocido, o dejar server.error.include-stacktrace en su valor por defecto.
Solución. include-stacktrace: never, include-message: never, y un ProblemDetail con mensaje controlado más el identificador de traza. El detalle real vive en el log del servidor, donde el ciudadano no llega y el soporte sí.
- Concurrencia
8.1. Estado mutable en un singleton
Síntoma. Un contador que pierde incrementos, un dato que aparece en la respuesta de otro usuario, un fallo irreproducible en local que solo ocurre bajo carga.
Causa. Un campo mutable en un bean singleton, compartido por todos los hilos: private Usuario usuarioActual es literalmente el usuario de otra petición, y private int alquileresProcesados pierde incrementos.
Solución. El estado por petición viaja en los argumentos o en el MDC; el estado compartido vive en la base de datos, en la caché o en un Counter de Micrometer, que sí es seguro entre hilos.
8.2. La tarea programada que se ejecuta N veces al escalar
Síntoma. Con tres réplicas, tres correos de resumen a cada ciudadano y tres cobros de recargo.
Causa. Cada instancia tiene su propio planificador. @Scheduled no coordina nada entre procesos.
Solución. ShedLock sobre la PostgreSQL que ya existe, con @SchedulerLock(name = ..., lockAtLeastFor = ..., lockAtMostFor = ...) y usingDbTime() para que la referencia temporal sea el reloj de la base de datos y no el de cada JVM. Y la defensa de fondo, que sirve incluso si el bloqueo falla: hacer las tareas idempotentes. Marcar una bicicleta como MANTENIMIENTO dos veces no cambia nada; sumar un importe a un acumulador dos veces, sí.
8.3. El contexto que no viaja al hilo asíncrono
Síntoma. Un @PreAuthorize que falla dentro de un método asíncrono por falta de autenticación, y líneas de log del trabajo en segundo plano sin traceId, imposibles de relacionar con la petición que las originó.
Causa. El SecurityContextHolder y el MDC viven en ThreadLocal y no cruzan solos al hilo del ejecutor.
Solución. Envolver el ejecutor en un DelegatingSecurityContextAsyncTaskExecutor y registrar un TaskDecorator que copie el MDC, con la restauración en un finally —igual de importante que el MDC.remove() del FiltroTraza, porque el hilo del pool se reutiliza y el identificador se quedaría pegado contaminando las tareas siguientes—.
El error conceptual asociado: poner @Async y @Transactional en el mismo método. La transacción tampoco viaja: el hilo del ejecutor abre una nueva e independiente, no ve las escrituras sin confirmar del llamante, y una entidad gestionada pasada como argumento pertenece a un EntityManager de otro hilo. La regla es pasar identificadores, nunca entidades, y disparar el trabajo en AFTER_COMMIT.
- Rendimiento
9.1. Optimizar sin medir
Síntoma. Una tarde de trabajo y ninguna mejora perceptible, o una mejora que nadie puede demostrar.
Causa. La intuición sobre dónde se va el tiempo es notoriamente mala: casi todo el mundo apuesta por su propio código Java cuando el reparto real está dominado por la base de datos. Y la ley de Amdahl lo remata: hacer diez veces más rápida la serialización JSON —que es el 5 % del tiempo— mejora el conjunto un 5 %.
Solución. Línea base con k6 guardada en el repositorio, un cambio cada vez, y volver a medir. Y el SLO en p95 y p99, nunca en la media: con 1 000 peticiones, una media de 120 ms puede esconder un p99 de 3 segundos que afecta a 20 ciudadanos de cada mil.
9.2. La caché que esconde una consulta mal escrita
Síntoma. El p95 mejora, pero la carga de la base de datos sigue alta y cualquier fallo de caché produce un pico enorme.
Causa. Se añadió @Cacheable sobre un método que hacía 43 consultas. La caché no arregla la consulta: la oculta el 95 % del tiempo y la deja intacta para el 5 % restante, que ahora es el peor.
La regla de oro: primero se elimina el trabajo innecesario, después se cachea lo que queda. Un endpoint que tras corregir el N+1 y añadir un índice pasa de 2 410 ms a 91 ms puede no necesitar caché en absoluto.
9.3. El pool desproporcionado y el DEBUG en producción
Síntoma A. Se sube maximum-pool-size de 10 a 100 y el rendimiento baja. Causa: una conexión activa es un núcleo de CPU y un disco de la base de datos trabajando; con más conexiones que capacidad real, el servidor gasta el tiempo cambiando de contexto y compitiendo por bloqueos.
Diagnóstico. hikaricp.connections.pending distingue dos situaciones que se confunden siempre: si hay espera y la base de datos va sobrada, la concurrencia es real y el pool se queda corto; si hay espera y la CPU de la base de datos está tranquila, algo retiene conexiones —una llamada HTTP dentro de una transacción es el sospechoso número uno— y subir el pool solo alarga la agonía.
Síntoma B. El disco se llena y el coste de la agregación de logs se dispara. Causa: logging.level.com.ciclourbana: DEBUG o org.hibernate.SQL: DEBUG heredados del perfil de desarrollo. Solución: INFO como nivel base y, cuando haga falta detalle, subirlo en caliente con /actuator/loggers y bajarlo al terminar. Con una advertencia de seguridad: algunos clientes HTTP registran las cabeceras completas en DEBUG, incluida Authorization.
- Pruebas
10.1. @SpringBootTest para todo
Síntoma. La suite tarda veinticinco minutos y nadie la ejecuta en local.
Causa. El cono de helado: levantar el contexto entero para probar una fórmula de tarifa. Y una causa secundaria que sorprende: Spring cachea los contextos entre clases de prueba, pero cada combinación distinta de propiedades, perfiles o @MockitoBean crea uno nuevo, así que una suite con quince variantes levanta quince contextos.
Solución. Unificar la configuración de las pruebas de integración en una clase base común —PruebaIntegracionBase— y bajar a la pirámide todo lo que no necesite el contexto.
10.2. Pruebas intermitentes
| Causa | Síntoma | Solución |
|---|---|---|
LocalDateTime.now() en el código probado |
Falla a medianoche o en el cambio de hora | Clock inyectado y Clock.fixed en la prueba |
| Dependencia del orden de ejecución | Falla al añadir una prueba nueva | Cada prueba prepara lo suyo y no deja rastro |
| Datos compartidos entre pruebas | Falla al ejecutar en paralelo | @Transactional en la prueba, o limpieza explícita |
Thread.sleep esperando algo asíncrono |
Falla en una máquina más lenta | Awaitility con condición y tiempo máximo |
| Dependencia de un servicio externo real | Falla cuando el servicio está caído | Testcontainers o un doble |
Y la peor consecuencia, que es cultural: una sola prueba intermitente entrena al equipo a reintentar en lugar de investigar, y esa costumbre acaba ignorando también los fallos reales.
10.3. La cobertura como objetivo
Síntoma. 85 % de cobertura y errores en producción en el código cubierto.
Causa. La cobertura mide qué código se ejecutó, no qué comportamiento se verificó. Una prueba sin un solo assert da cobertura del 100 %.
// Cobertura total, cero verificación
@Test
void noVerificaAbsolutamenteNada() {
new TarifaEstandar().calcular(Duration.ofMinutes(30));
}Solución. Leer la cobertura como un mapa del rojo —un if de negocio entero sin cubrir es una pregunta legítima— y no como una puntuación. Y una regla mecánica de revisión: toda prueba tiene al menos un assertThat o un assertThatThrownBy.
- Tabla de diagnóstico rápido
| Síntoma | Dónde mirar primero | Lección |
|---|---|---|
NoSuchBeanDefinitionException |
Paquete de la clase principal, estereotipo, @Profile; arrancar con --debug |
02-01 |
| Recuadro de dependencias en el arranque | El propio mensaje: dice qué beans forman el ciclo | 02-02 |
| «Esta anotación no hace nada» | Trampa del proxy: autoinvocación, método private/final, new |
Apartado 3 |
| La transacción no revierte | Excepción capturada sin relanzar, o comprobada | 04-07 |
UnexpectedRollbackException |
Un método interno marcó rollback y el externo capturó | 04-07 |
LazyInitializationException |
Entidad fuera del servicio; mapear a DTO dentro de la transacción | 04-07 |
| Latencia proporcional al número de resultados | N+1: generate_statistics, contador de consultas, la cascada de spans |
09-01 |
| Mediana buena y p99 pésimo | Espera de recurso: hikaricp.connections.pending, bloqueos, GC |
09-01 |
| El valor del YAML se ignora | /actuator/configprops y /actuator/env; indentación y precedencia |
02-04 |
| En producción se comporta como en desarrollo | El perfil no se activó: buscar No active profile set en el log |
07-02 |
| Un endpoint responde sin autenticación | Orden de las reglas; falta denyAll() al final |
05-02 |
| Un usuario accede a datos de otro | Falta @PreAuthorize por dato, o está en un método no interceptado |
05-05 |
| La respuesta filtra SQL o trazas | include-stacktrace, include-message, e.getMessage() ajeno |
03-06 |
| Una tarea «dejó de ejecutarse» | Excepción que escapó al planificador; /actuator/scheduledtasks |
07-03 |
| Trabajo duplicado al escalar | Sin ShedLock; y tareas no idempotentes | 07-03 |
Logs asíncronos sin traceId |
Falta el TaskDecorator que copia el MDC |
07-03 |
| Tasa de aciertos de caché en 0 % | Autoinvocación, o clave que cambia en cada llamada | 09-02 |
| Métricas que hacen caer Prometheus | Cardinalidad: una etiqueta con muchos valores distintos | 09-03 |
OOMKilled (código 137) sin excepción |
MaxRAMPercentage demasiado alto o fuga; -Xlog:gc |
09-01 |
| La suite tarda veinte minutos | @SpringBootTest por todas partes y contextos no reutilizados |
06-04 |
Errores Comunes y Consejos
Buscar la causa donde está el síntoma. El planificador de un solo hilo hace que una tarea lenta bloquee a otra, y el síntoma aparece en la tarea equivocada. Un pool retenido por llamadas HTTP produce un p99 alto en endpoints que no tienen nada que ver. Antes de mirar el código del síntoma, pregúntate qué recurso comparte con el resto.
Arreglar el síntoma en lugar de la causa. @Lazy para un ciclo, open-in-view: true para una LazyInitializationException, subir el pool para una espera de conexiones, flyway:repair para un checksum que no cuadra. Los cuatro hacen desaparecer el mensaje y dejan el problema intacto.
Cambiar varias cosas a la vez al diagnosticar. Si tocas el índice, el pool y la caché en el mismo despliegue y mejora, no sabes cuál sirvió ni cuál está empeorando otra cosa.
Ignorar los avisos del arranque. Spring Boot avisa de @Transactional en métodos no públicos, de la serialización de PageImpl y de open-in-view activo. Un arranque lleno de avisos que «siempre han estado ahí» es un sitio donde el aviso nuevo pasa desapercibido.
Consejo: cuando algo «no hace nada», sospecha del proxy antes que de nada. Cuatro anotaciones distintas, un solo mecanismo, un solo diagnóstico: ¿la llamada viene de fuera del bean? ¿el método es public y no final? ¿el objeto lo creó Spring?
Consejo: convierte cada error de producción en una prueba. Antes de arreglarlo, escribe la prueba que lo reproduce y compruébala en rojo. Así sabes que arreglaste lo que creías y garantizas que no vuelve.
Consejo: aprende a leer los tres informes que Spring Boot ya te da. El de autoconfiguración con --debug, /actuator/env con las fuentes de cada propiedad y /actuator/configprops con los valores efectivos. Entre los tres explican la mayoría de los «pero si yo lo puse».
Ejercicios
Ejercicio 1: seis fallos en un servicio de incidencias
Encuentra los seis errores de esta clase, indica para cada uno el síntoma que producirá y en qué momento aparecerá, y reescríbela.
@Service
public class IncidenciaService {
@Autowired private IncidenciaRepositorio repositorio;
private final Map<Long, Incidencia> ultimasVistas = new HashMap<>();
@Transactional
public void procesarPendientes() {
for (Incidencia i : repositorio.findAll()) {
cerrarSiProcede(i);
}
}
@Async
@Transactional
private void cerrarSiProcede(Incidencia incidencia) {
try {
incidencia.setEstado(EstadoIncidencia.CERRADA);
ultimasVistas.put(incidencia.getId(), incidencia);
notificador.avisarTaller(incidencia.getBicicleta().getMatricula());
} catch (Exception e) {
log.error("Error", e);
}
}
}Ejercicio 2: del síntoma a la causa
Para cada uno de estos cinco informes de producción, formula la hipótesis más probable, di qué comprobarías para confirmarla y qué no harías.
- «Desde el despliegue del martes, el p99 de
POST /api/v1/alquilerespasó de 300 ms a 4 s. La mediana sigue en 45 ms. CPU de la aplicación al 20 %, CPU de la base de datos al 25 %. No hayFull GC.» - «El operario Ramón dice que marcó una bicicleta como averiada y sigue apareciendo disponible. No hay ningún error en el log.»
- «Los ciudadanos reciben tres correos de resumen mensual desde que el ayuntamiento pidió más capacidad.»
- «
/api/v1/estacionestardaba 200 ms con 20 estaciones. Ahora que hay 200, tarda 2 s.» - «Cambiamos
ciclourbana.tarifa.precio-minutoa 0,15 enapplication-prod.yml, desplegamos, y las facturas siguen calculándose a 0,12.»
Ejercicio 3: la migración peligrosa
El equipo va a desplegar la versión 2.5.0 de CicloUrbana con despliegue progresivo —las instancias de la versión anterior conviven unos minutos con las nuevas— e incluye esta migración:
-- V10__ajustes_alquileres.sql
ALTER TABLE alquileres DROP COLUMN estacion_origen_id;
ALTER TABLE alquileres ADD COLUMN estacion_inicio_id BIGINT NOT NULL;
ALTER TABLE alquileres ADD CONSTRAINT fk_alquiler_estacion_inicio
FOREIGN KEY (estacion_inicio_id) REFERENCES estaciones(id);
CREATE INDEX idx_alquileres_estacion_inicio ON alquileres (estacion_inicio_id);Enumera todos los problemas que provocará y reescribe el plan de migración completo.
Soluciones
Solución 1
Error 1 — @Autowired en campo. No permite final, obliga a reflexión para probar la clase y oculta el crecimiento de dependencias. Aparece: al escribir la primera prueba unitaria.
Error 2 — HashMap mutable en un singleton. Estado compartido entre hilos sin sincronización: HashMap puede corromperse en escrituras concurrentes —llegando a bucles infinitos en la estructura interna— y además crece sin límite, así que es también una fuga de memoria. Aparece: bajo carga, de forma irreproducible.
Error 3 — autoinvocación. cerrarSiProcede(i) se llama con this: ni @Async ni @Transactional tienen efecto. Todo se ejecuta síncrono dentro de la transacción externa. Aparece: nunca como error; simplemente la asincronía no existe.
Error 4 — anotaciones sobre un método private. Aunque se corrigiera la autoinvocación, un método privado no es interceptable. Aparece: nunca; se ignora en silencio.
Error 5 — excepción capturada y no relanzada. Si algo falla, se registra y el bucle continúa; la transacción externa confirma y se dan por cerradas incidencias que no lo están. Y si el fallo marcó la transacción para rollback, el commit final da UnexpectedRollbackException. Aparece: el día del primer fallo parcial.
Error 6 — llamada externa dentro de la transacción. notificador.avisarTaller(...) retiene una conexión del pool durante toda la llamada de red. Con findAll() sobre miles de incidencias, la transacción puede durar minutos con una conexión bloqueada. Aparece: cuando crece el volumen, como espera de conexiones y p99 alto.
Y un séptimo problema de diseño que el enunciado no numera: findAll() sin filtro ni paginación para descartar la mayoría en memoria.
@Service
public class IncidenciaService { // constructor omitido: repositorio y cerrador, final
/** Sin @Transactional: solo orquesta. Cada incidencia va en su propia transacción. */
public int procesarPendientes() {
List<Long> ids = repositorio.buscarIdsPendientesDeCierre(); // filtra en la BD
int cerradas = 0;
for (Long id : ids) {
try {
cerrador.cerrar(id); // otro bean: la llamada atraviesa el proxy
cerradas++;
} catch (DataAccessException e) {
log.error("No se pudo cerrar la incidencia {}", id, e);
}
}
return cerradas;
}
}
@Service
public class CerradorIncidencias {
@Transactional(timeout = 5) // público, en otro bean: sí se intercepta
public void cerrar(Long idIncidencia) {
Incidencia incidencia = repositorio.findById(idIncidencia)
.orElseThrow(() -> new RecursoNoEncontradoException("Incidencia", idIncidencia));
incidencia.setEstado(EstadoIncidencia.CERRADA); // dirty checking, sin save()
eventos.publishEvent(new IncidenciaCerrada(idIncidencia,
incidencia.getBicicleta().getMatricula()));
}
}
@Component
public class NotificadorIncidencias {
/** Fuera de la transacción y fuera del hilo que orquesta. */
@Async("ejecutorCorreo")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void alCerrarse(IncidenciaCerrada evento) {
notificador.avisarTaller(evento.matricula());
}
}Nótese que la corrección del error 3 arregla también el 6: al salir la notificación al AFTER_COMMIT, la conexión ya está devuelta al pool cuando empieza la llamada de red. Y el estado mutable del error 2 simplemente desaparece: no hacía falta.
Solución 2
1. Espera de conexión del pool. Las señales encajan una a una: mediana intacta (el camino rápido no cambió), p99 disparado con CPU baja en ambos lados (no es saturación de cálculo), sin Full GC (no son pausas de memoria). Comprobaría: hikaricp.connections.pending y hikaricp.connections.usage; /actuator/threaddump durante un pico buscando hilos que ya tienen conexión y están bloqueados en un socket; y leak-detection-threshold: 20000, cuya traza señala el método culpable. La causa más probable es una llamada HTTP dentro de una transacción introducida el martes. Lo que no haría: subir maximum-pool-size ni los hilos de Tomcat; con la base de datos al 25 % no falta capacidad, y más conexiones retenidas por llamadas remotas solo alargan el problema.
2. La trampa del proxy sobre @Transactional. «Sin ningún error en el log» es la firma. Comprobaría: si marcarAveriada se invoca desde otro método de la misma clase, si es public, y si logging.level.org.springframework.transaction: DEBUG imprime Creating new transaction en esa llamada. Segunda hipótesis, si la transacción sí existe: falta el save() y la entidad no está gestionada porque se construyó fuera del contexto. Lo que no haría: añadir save() a ciegas; si el problema es el proxy, el save() tampoco se ejecutará en transacción.
3. Tarea programada multiplicada al escalar. «Desde que el ayuntamiento pidió más capacidad» significa más réplicas, y cada una tiene su propio planificador. Comprobaría: el número de réplicas y si la tarea lleva @SchedulerLock. Lo que no haría: mover la tarea a un perfil activo en una sola instancia como solución definitiva —crea un punto único de fallo—; ShedLock es la respuesta, y además conviene revisar si la tarea es idempotente.
4. N+1. La latencia crece proporcionalmente al número de resultados: es la definición. Comprobaría: generate_statistics en preproducción para ver el recuento de sentencias, o directamente la cascada de spans buscando select repetidos. Lo que no haría: añadir una caché. Escondería el problema el 95 % del tiempo y dejaría el 5 % restante peor que antes.
5. Propiedad que no se enlaza. Tres hipótesis por orden: la clave está mal escrita; una variable de entorno CICLOURBANA_TARIFA_PRECIO_MINUTO gana al fichero por precedencia; o el perfil prod no se activó y el fichero no se lee. Comprobaría: /actuator/configprops para el valor efectivo de TarifasProperties y /actuator/env para saber de qué fuente sale. Ambos responden en treinta segundos. Lo que no haría: recompilar «por si acaso» ni cambiar el valor en application.yml sin saber cuál de las tres causas es.
Solución 3
Los problemas, por orden de gravedad.
1. Se pierden los datos y son irrecuperables. DROP COLUMN estacion_origen_id borra la estación de origen de todos los alquileres históricos. No hay UPDATE que los rellene después: se pierde el dato de dónde empezó cada alquiler de la red.
2. ADD COLUMN ... NOT NULL sin valor por defecto falla. Si la tabla tiene filas, PostgreSQL no puede rellenar la columna nueva y la migración aborta a mitad. Y aunque no fallara, no habría de dónde sacar el valor: ya se borró en la línea anterior.
3. Rompe las instancias de la versión anterior. Durante el despliegue progresivo, las instancias 2.4.0 siguen ejecutando SELECT ... estacion_origen_id .... En cuanto la migración se aplica, todas ellas empiezan a fallar con column does not exist, y como la migración se ejecuta antes de arrancar las nuevas, el servicio queda caído para todos los ciudadanos de Ribalta.
4. CREATE INDEX sin CONCURRENTLY bloquea las escrituras de la tabla alquileres durante toda la construcción del índice. Con millones de filas, son minutos sin poder iniciar ni finalizar alquileres.
5. Es un renombrado disfrazado. El cambio real es estacion_origen_id → estacion_inicio_id, y renombrar una columna nunca es compatible hacia atrás. Además, el beneficio es cosmético: conviene preguntarse si merece el riesgo.
El plan correcto: expand/contract en tres despliegues.
-- V10__expand_estacion_inicio.sql (despliegue 1, con la versión 2.5.0)
ALTER TABLE alquileres ADD COLUMN estacion_inicio_id BIGINT; -- admite nulos
UPDATE alquileres SET estacion_inicio_id = estacion_origen_id; -- por lotes si es grande
ALTER TABLE alquileres ADD CONSTRAINT fk_alquiler_estacion_inicio
FOREIGN KEY (estacion_inicio_id) REFERENCES estaciones(id);
CREATE INDEX CONCURRENTLY idx_alquileres_estacion_inicio ON alquileres (estacion_inicio_id);
-- V11__endurecer_estacion_inicio.sql (despliegue 2, con la versión 2.6.0)
UPDATE alquileres SET estacion_inicio_id = estacion_origen_id WHERE estacion_inicio_id IS NULL;
ALTER TABLE alquileres ALTER COLUMN estacion_inicio_id SET NOT NULL;
-- V12__contract_eliminar_estacion_origen.sql (despliegue 3, con la versión 2.7.0)
DROP INDEX IF EXISTS idx_alquileres_estacion_origen;
ALTER TABLE alquileres DROP COLUMN estacion_origen_id;En 2.5.0 la entidad mapea la columna nueva y escribe en ambas, para que las instancias 2.4.0 que aún viven sigan viendo datos correctos; la columna nueva admite nulos porque durante unos minutos hay instancias antiguas insertando filas sin rellenarla. En 2.6.0 la entidad deja de escribir en la vieja, y solo entonces —en 2.7.0— se borra.
Las cuatro reglas que resume el ejercicio. Una migración debe funcionar con la versión que corre ahora y con la que se va a desplegar. Añadir es compatible; renombrar y borrar no lo son nunca. Un NOT NULL se alcanza en tres pasos, jamás en uno. Y CONCURRENTLY es obligatorio en cualquier índice sobre una tabla en producción.
Y una comprobación práctica que cuesta poco y detecta casi todo esto: ejecutar la migración contra una copia reciente de producción con la versión anterior de la aplicación corriendo encima. Si la aplicación antigua sigue funcionando después de migrar, el despliegue progresivo es seguro.
Conclusión
Tienes ahora el reverso del catálogo anterior: los errores que las prácticas de 10-01 previenen, cada uno con su síntoma real, su causa, cómo se diagnostica y el código corregido. Y con una clasificación que orienta la atención: los ruidosos cuestan una tarde, los silenciosos cuestan una reconciliación de datos y los diferidos aparecen justo cuando peor viene.
Sabes reconocer los fallos de arranque —la clase principal fuera del paquete raíz, el bean que no existe, el ciclo con su recuadro, los dos candidatos sin desempate— y usar las tres herramientas que Spring Boot ya te da para diagnosticarlos: el informe de --debug, /actuator/env y /actuator/configprops. Tienes por fin la trampa del proxy tratada en un solo sitio, con su mecanismo —solo las llamadas que entran desde fuera atraviesan el proxy—, sus cuatro caras (@Transactional, @Async, @Cacheable y la más peligrosa, @PreAuthorize), sus disparadores de visibilidad y final, y sus tres soluciones, con la de extraer a otro bean como la que además mejora el diseño.
Reconoces el perfil que nunca se activó y su prueba literal en el log, la propiedad mal escrita que se ignora sin protestar, las listas que se sustituyen en lugar de fusionarse y el secreto versionado que no se arregla borrándolo sino rotándolo. En JPA tienes la LazyInitializationException con su única solución correcta, el N+1 y su EAGER oculto en @ManyToOne, el equals que rompe los HashSet, las cascadas que borran de más, la transacción que confirma porque alguien capturó la excepción y el OFFSET grande. En REST, las tres consecuencias de exponer entidades, el mass assignment, el 200 que hace invisibles los errores para la monitorización y el Page que convierte la estructura interna de una librería en contrato público. En seguridad, el orden de las reglas, el CSRF desactivado sin razón escrita, el JWT que no está cifrado y el error que regala información interna. En concurrencia, el estado mutable compartido, la tarea que se multiplica al escalar y el contexto que no viaja al hilo asíncrono. En rendimiento, optimizar sin medir, la caché que esconde una consulta mal escrita y el pool desproporcionado. Y en pruebas, el cono de helado, las cinco causas de intermitencia y la cobertura que sube sin verificar nada. Cierra el catálogo la tabla de diagnóstico rápido: veintiún síntomas con el sitio por donde empezar y la lección donde está la respuesta completa.
Queda una tercera dimensión de la pregunta «¿está bien hecha?». Hemos visto qué conviene hacer y qué conviene evitar, pero ninguna de las dos cosas dice nada sobre cómo se lee el código. Un servicio puede cumplir las treinta y ocho comprobaciones y no tener ni un solo error del catálogo, y aun así ser un método de doscientas líneas con cuatro banderas booleanas, nombres que no dicen nada, comentarios que repiten el código y un modelo de dominio que es una bolsa de setters. Eso no rompe nada hoy: rompe la capacidad del equipo de cambiarlo mañana. La lección siguiente, Consejos para Escribir Código Limpio, se ocupa de esa dimensión: nombres que revelan intención, funciones con un solo nivel de abstracción, SOLID con ejemplos reales de Ribalta, comentarios que explican el porqué, inmutabilidad y Optional bien usados, el modelo de dominio anémico frente a Alquiler.finalizar(...), las reglas de dependencia verificadas automáticamente con ArchUnit, el estilo que no se discute porque lo aplica una herramienta, y cómo se reconoce, se registra y se paga la deuda técnica.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
