En qué orden aparecen
Los errores de un cambio de JDK no llegan todos a la vez, y saber en qué fase aparece cada uno ahorra tiempo de diagnóstico.
Primero salen los de compilación, que son los baratos: el compilador señala la línea y la causa. Después los de arranque, que aparecen cuando el contexto se levanta y ya cuestan más porque la traza apunta al síntoma y no al origen. Y al final los que solo se manifiestan bajo carga o con datos reales, que son los caros. Compilar con el JDK nuevo manteniendo el entorno de ejecución antiguo, como se explica en la actualización de versiones de Java, sirve precisamente para separar los dos primeros grupos.
Errores de compilación
Aparecen en cuanto se cambia el compilador. Son los más fáciles de localizar porque el mensaje señala el fichero y la línea.
package javax.xml.bind does not exist
Causa: JAXB salió del JDK en Java 11. Las clases ya no están en el classpath de compilación salvo que alguna dependencia las arrastre.
Qué se hace: Declarar jakarta.xml.bind-api junto con una implementación. Si el proyecto aún no puede cambiar el espacio de nombres, existen las versiones 2.3.x que mantienen javax.xml.bind como dependencia externa.
package javax.annotation does not exist
Causa: Las anotaciones comunes, entre ellas @PostConstruct y @PreDestroy, salieron del JDK con el mismo movimiento.
Qué se hace: Añadir jakarta.annotation-api como dependencia explícita. Es de las que más se olvidan porque solo afecta a unas pocas clases del proyecto.
error: source option 8 is no longer supported
Causa: Los JDK recientes han retirado el soporte para compilar hacia versiones muy antiguas.
Qué se hace: Subir el destino y, de paso, cambiar maven.compiler.source y maven.compiler.target por maven.compiler.release, que además comprueba que las APIs utilizadas existan en la versión de destino.
cannot find symbol: class Unsafe
Causa: sun.misc.Unsafe y el resto de clases internas del JDK dejaron de ser accesibles desde código externo.
Qué se hace: Rara vez está en el código propio: casi siempre viene de una librería antigua de serialización, de caché o de instrumentación. Lo que se actualiza es la librería, no el código.
Errores al arrancar la aplicación
El proyecto compila y falla al levantar el contexto. Aquí la traza suele apuntar al síntoma, no a la causa.
NoClassDefFoundError: javax/xml/bind/JAXBContext
Causa: La API está en el classpath de compilación, arrastrada por alguna dependencia, pero no hay implementación en ejecución.
Qué se hace: Declarar la implementación además de la API. Es el caso que mejor ilustra por qué compilar no demuestra nada sobre la ejecución.
InaccessibleObjectException: module java.base does not opens java.lang
Causa: Una librería llama a setAccessible() sobre clases del JDK. Hasta Java 15 era un aviso; desde Java 16 la encapsulación es estricta y es un error.
Qué se hace: Actualizar la librería a una versión que no lo necesite. Abrir el módulo con --add-opens java.base/java.lang=ALL-UNNAMED funciona como medida temporal, pero deja el arranque dependiendo de una bandera que se pierde en el siguiente cambio de despliegue.
UnsupportedClassVersionError: class file version 61.0
Causa: Un artefacto compilado con Java 17 se está ejecutando sobre un JRE anterior. Suele indicar que el entorno de construcción y el de producción no van sincronizados.
Qué se hace: Comparar la versión del compilador con la del entorno de ejecución. La correspondencia es directa: 52 es Java 8, 55 es Java 11, 61 es Java 17 y 65 es Java 21. Fijar maven.compiler.release evita generar bytecode más moderno que el destino real.
NoSuchMethodError en una clase que no se ha tocado
Causa: Hay dos versiones de la misma dependencia en el classpath. Al subir de JDK cambian las versiones de medio árbol y una transitiva gana donde antes perdía.
Qué se hace: Mirar el árbol real con mvn dependency:tree y fijar la versión en la gestión de dependencias del proyecto. El procedimiento completo está en la guía del inventario de dependencias.
ClassNotFoundException: javax.servlet.http.HttpServlet
Causa: El contenedor ya solo expone el espacio de nombres jakarta y el artefacto sigue pidiendo el antiguo.
Qué se hace: No es un problema del JDK sino del servidor: es el cambio de Jakarta EE, que se explica en la migración de Java EE a Jakarta EE.
Los que no se ven hasta que hay carga o datos reales
Estos son los que hacen que una migración parezca terminada cuando no lo está.
Cambios de orden en colecciones y en resultados
Causa: Varias implementaciones internas cambiaron entre versiones. El código que dependía sin saberlo de un orden concreto empieza a comportarse distinto.
Qué se hace: Ordenar de forma explícita donde el orden importe. Si una prueba falla de forma intermitente tras la migración, este suele ser el motivo.
Diferencias de formato en fechas y números
Causa: El proveedor de datos regionales por defecto cambió a partir de Java 9, y algunos formatos y separadores no coinciden con los de Java 8.
Qué se hace: Fijar el formato de forma explícita donde el texto se envíe a otro sistema o se guarde. Arrancar con -Djava.locale.providers=COMPAT devuelve el comportamiento anterior, pero es una medida de transición, no una solución.
Consumo de memoria o latencia distintos sin cambios de código
Causa: El recolector de basura por defecto y su configuración cambiaron entre versiones, y las banderas antiguas pueden ignorarse en silencio.
Qué se hace: Revisar las opciones de arranque una por una y comprobar cuáles siguen reconocidas. Medir antes y después con carga comparable, no en un entorno de pruebas vacío.
Cómo abordarlos sin acumular riesgo
- Compilar con el JDK nuevo antes de cambiar el entorno de ejecución, para separar los dos tipos de fallo.
- Resolver primero las dependencias que bloquean al resto, y solo después el resto.
- Una tanda de actualizaciones por commit, con las pruebas pasando en cada punto.
- Anotar cada bandera de arranque que se añada, y por qué, para poder retirarla después.
- Comparar el comportamiento antes y después en los flujos críticos, no solo que arranque.
Si la aplicación no tiene pruebas suficientes para hacer esa comparación, construirlas es la primera fase y no un extra: es lo que se explica en qué implica modernizar una aplicación legacy.