Saltar a contenido

ADR-018 — Upgrade a Spring Boot 4 (framework 7, security 7, hibernate 7, jackson 3) + consola boot-ui (solo dev)

  • Status: accepted (operador aprobó el upgrade y la consola de desarrollo, 2026-08-21)
  • Context: Spring Boot 3.4 salió de soporte OSS en 2025-12; el lanzamiento a producción es 2026-09-01, así que seguir en 3.4 sin parches de seguridad hasta esa fecha no es aceptable. Además se pidió sumar boot-ui (consola de desarrollo de Julien Dubois: introspección de salud/métricas/JPA/seguridad en el navegador), que requiere Spring Boot 4.
  • Alternatives:
  • Quedarse en Spring Boot 3.5 (última minor de la línea 3.x, soportada más tiempo que 3.4): evita el trabajo de migración de Jackson 3 / Spring Framework 7, pero boot-ui no corre ahí — quedaría afuera del alcance pedido.
  • Spring Boot 4.x (elegida): versión GA más nueva resoluble en Maven Central al momento del upgrade, 4.1.1 (verificado contra maven-metadata.xml de spring-boot-starter-parent — 4.1.1 es la última release, 4.2.0-M1 es milestone, no GA). Es la única línea que soporta boot-ui.
  • Decision: backend/pom.xml parent → spring-boot-starter-parent:4.1.1. Se agrega com.julien-dubois.bootui:bootui-spring-boot-starter:1.14.1 (última 1.x en Maven Central al momento del upgrade) con su comportamiento AUTO por defecto: activa solo con perfil dev/local (o con spring-boot-devtools presente), se auto-apaga en prod/production, y rechaza requests no-loopback aunque esté activa (bootui.allow-non-localhost=false por defecto). El perfil e2e (application-e2e.properties, nuevo — reservado por ADR-017 pero sin contenido hasta ahora) no fija bootui.enabled a mano: AUTO ya resuelve sola los tres casos que importan (ver Consequences).
  • Consequences:
  • Jackson 3: Spring Boot 4 trae Jackson 3, que renombra los paquetes de jackson-core y jackson-databind de com.fasterxml.jackson.* a tools.jackson.* (las anotaciones, com.fasterxml.jackson.annotation.*, se quedan donde estaban — no cambian). ObjectMapper, TypeReference y la excepción de (de)serialización se mudan; JsonProcessingException (checked, extendía IOException) desaparece a favor de JacksonException (unchecked). Afecta SorteoService, SeedMinimo, GestorApiTest, SeedMinimoTest. spring.jackson.property-naming-strategy: SNAKE_CASE sigue siendo la property correcta y el contrato snake_case de toda la API sigue intacto — confirmado con la suite completa y a mano contra el jar real (POST /api/gestor/campanias con body snake_case).
  • Flyway modularizado: Boot 4 separó la autoconfiguración de Flyway a un módulo propio (spring-boot-flyway / spring-boot-starter-flyway / spring-boot-starter-flyway-test). FlywayMigrationStrategy se muda de org.springframework.boot.autoconfigure.flyway a org.springframework.boot.flyway.autoconfigure. backend/pom.xml reemplaza la dependencia directa flyway-core por spring-boot-starter-flyway (que la trae transitiva, versión 12.4.0 gestionada por Boot); flyway-sqlserver se queda como estaba (Boot no la gestiona).
  • MockMvc modularizado: Boot 4 separó el soporte de test de MockMvc a spring-boot-starter-webmvc-test / spring-boot-webmvc-test. AutoConfigureMockMvc se muda de org.springframework.boot.test.autoconfigure.web.servlet a org.springframework.boot.webmvc.test.autoconfigure. MockMvc, MockMvcRequestBuilders, MockMvcResultMatchers (paquete org.springframework.test.web.servlet, de Spring Framework, no de Boot) no cambiaron. No hizo falta spring-boot-starter-data-jpa-test: ningún test de este repo usa @DataJpaTest.
  • Regresión real de Spring Framework 7 en resolución de @ControllerAdvice (HIGH, detectada por el smoke manual, no por la suite): sin @Order explícito, Spring elige entre @ControllerAdvice candidatos por orden de registro del bean, no por especificidad del basePackages. Bajo Spring Framework 7 ese orden cambió lo suficiente como para que el catch-all de Exception de GlobalExceptionHandler ganara la carrera antes que el DataIntegrityViolationException específico de GestorExceptionHandler — el alta duplicada de "campaña vigente" volvía 500 en vez de 409. La suite completa (123 tests, incluido EsquemaInvariantesTest que prueba la traducción de excepción a nivel repositorio) pasaba igual, porque nadie ejercitaba el camino completo controller→advice con una inserción real duplicada — GestorExceptionHandlerTest sólo invoca el handler aislado. Se agregó @Order(HIGHEST_PRECEDENCE) a GestorExceptionHandler y @Order(LOWEST_PRECEDENCE) explícito a GlobalExceptionHandler, más un test de regresión end-to-end (GestorApiTest.crearCampania_duplicadaVigente_409) que pega contra el controller real, no contra el handler aislado.
  • boot-ui no ensancha la seguridad existente: detecta Spring Security y se auto-registra permitAll SOLO en /bootui, /bootui/**, /bootui/api, /bootui/api/** (log propio de BootUiSpringSecurityAutoConfiguration lo confirma); /api/gestor/** sigue en 401 sin auth, /api/salud sigue público, verificado a mano.
  • Trae actuator + micrometer + opentelemetry + archunit transitivos (bootui-spring-boot-starter depende de spring-boot-starter-actuator, spring-boot-micrometer-tracing-opentelemetry, micrometer-tracing-bridge-otel, archunit — todos en scope compile, siempre en el jar, no solo en un perfil dev). Es necesario para que la introspección de boot-ui funcione; los endpoints de actuator expuestos por web siguen limitados al default de Boot (health, info) — no se cambió management.endpoints.web.exposure.include.
  • Los assets estáticos de la consola (HTML/JS/CSS) quedan descargables incluso con bootui.enabled en AUTO-apagado o en prod (confirmado a mano: GET /bootui/index.html → 200 con prod solo, sin dev). bootui-ui empaqueta su SPA bajo classpath:/META-INF/resources/bootui/ (patrón webjar), que Spring sirve como recurso estático genérico apenas el jar está en el classpath — independiente del enabled de la librería. La API funcional (/bootui/api/**, la única que expone datos vivos de la app) y el filtro loopback-only SÍ están correctamente gateados y devuelven 404/403 fuera de dev/local — el shell estático es HTML/JS inerte sin backend que lo alimente, severidad baja (revela que la herramienta está en el classpath, no expone datos), pero se documenta en vez de ocultarlo.
  • application-e2e.properties no fija bootui.enabled=OFF: se evaluó hacerlo (como pide un enfoque ingenuo de "doble capa de defensa") pero se descartó porque con --spring.profiles.active=dev,e2e el último perfil activo pisa al anterior — un OFF fijo en e2e ganaría sobre el dev explícito y dejaría la consola inalcanzable incluso cuando alguien la pide a propósito para depurar una corrida E2E, rompiendo ese caso de uso. bootui.enabled=AUTO (default de la librería, sin pisar nada) ya resuelve los tres casos reales sin este problema: e2e sola (CI) apagada, prod,e2e apagada, dev,e2e prendida — los tres confirmados a mano contra el jar empaquetado real.

Implementation Plan

  • backend/pom.xml: parent spring-boot-starter-parent:4.1.1; flyway-core reemplazado por spring-boot-starter-flyway; spring-boot-starter-webmvc-test agregado (scope test); com.julien-dubois.bootui:bootui-spring-boot-starter:1.14.1 agregado; jacoco-maven-plugin a 0.8.15 (soporte de class files más nuevos).
  • Imports migrados a los paquetes nuevos de Jackson 3 (tools.jackson.*) y del Flyway/MockMvc modularizados de Boot 4, en SorteoService, SeedMinimo y los tests que los usan.
  • @Order explícito en GestorExceptionHandler (HIGHEST_PRECEDENCE) y GlobalExceptionHandler (LOWEST_PRECEDENCE) — fix forzado por la regresión de Spring Framework 7 descripta arriba.
  • backend/src/main/resources/application-e2e.properties (nuevo, sin contenido de bootui.enabled — ver Consequences).
  • docs/SMOKE-M6.md §0: nota de cómo abrir la consola en dev y por qué prod queda apagada sola.
  • Tests: GestorApiTest.crearCampania_duplicadaVigente_409 (regresión del fix de @Order), BootUiConsoleTest (apagada fuera de dev/local vía /bootui/api/health; prendida + loopback-only con perfil dev vía MockMvc con remoteAddr no-loopback).

Verification

  • [x] mvn -q test (SP_TEST_DB_URLSistemaPremios_test3) verde: 127 tests, 0 failures, 0 errors, 22 clases de test.
  • [x] mvn -q package -DskipTests empaqueta backend + landing + gestor en un solo jar (perfil full).
  • [x] Smoke contra el jar en :8093 (perfil e2e, DB SistemaPremios_test3): /api/salud 200, /gestor/ 200, /p/x 200, /api/gestor/panel sin auth 401, alta duplicada de campaña vigente 409 (no 500 — regresión encontrada y corregida).
  • [x] Smoke --spring.profiles.active=dev,e2e: GET /bootui 200 desde localhost; rechaza (403) un caller no-loopback (X-Forwarded-For spoofeado, mismo mecanismo de confianza de proxy que ya documentaba JD-B1 para el rate limit); /api/gestor/** sigue protegido.
  • [x] Smoke --spring.profiles.active=prod,e2e: GET /bootui 404 (capa API/loopback correctamente apagada; el shell estático queda servible como recurso inerte, documentado arriba).
  • [ ] Compresión gzip en /gestor/no verificable: server.compression nunca estuvo configurado en este repo (ni antes ni después del upgrade); hallazgo pre-existente, fuera de alcance de este ADR, no es una regresión del upgrade.