Transición de Java 8 a Java 11

No existe una solución única para realizar la transición del código de Java 8 a Java 11. En el caso de aplicaciones no triviales, pasar de Java 8 a Java 11 puede suponer una gran cantidad de trabajo. Entre los posibles problemas se incluyen API quitadas, paquetes en desuso, uso de la API interna, cambios en los cargadores de clases y cambios en la recolección de elementos no utilizados.

Por lo general, el enfoque es intentar ejecutar en Java 11 sin volver a compilar ni compilar primero con JDK 11. Si el objetivo es poner en marcha una aplicación lo más rápido posible, lo mejor suele ser intentar ejecutar en Java 11. En el caso de una biblioteca, el objetivo será publicar un artefacto compilado y probado con JDK 11.

El cambio a Java 11 merece la pena el esfuerzo. Desde Java 8, se han agregado nuevas características y se han realizado mejoras. Estas características mejoran el inicio, el rendimiento y el uso de la memoria, y proporcionan una mejor integración con los contenedores. Existen también otras adiciones y modificaciones a la API que mejoran la productividad del desarrollador.

En este documento se tratan las herramientas para inspeccionar el código. También se tratan los problemas que puede encontrarse y las recomendaciones para resolverlos. También debe consultar otras guías, como la guía de migración de Oracle JDK. Aquí no se explica cómo hacer que el código existente sea modular.

Cuadro de herramientas

Java 11 tiene dos herramientas, jdeprscan y jdeps, que son útiles para detectar posibles problemas. Estas herramientas se pueden ejecutar en archivos JAR o class existentes. Puede evaluar el esfuerzo que requiere la transición sin tener que volver a compilar.

jdeprscan comprueba si se están usando API en desuso o eliminadas. La utilización de API en desuso no es un problema que produzca un bloqueo, pero es algo que se debe examinar. ¿Hay archivos jar actualizados? ¿Necesita registrar una incidencia para resolver el uso de API en desuso? El uso de API quitadas es un problema que produce un bloqueo y que debe solucionarse antes de intentar ejecutar en Java 11.

jdeps, que es un analizador de dependencias de clases de Java. Cuando se usa con la opción --jdk-internals, jdeps indica qué clase depende de cada API interna. Puede seguir usando la API interna en Java 11, pero sustituir su uso cuando antes. En la página de wiki de OpenJDK acerca de la herramienta de análisis de dependencias de Java, encontrará los reemplazos recomendados para algunas API internas de JDK que se usan habitualmente.

Hay complementos jdeps y jdeprscan para Gradle y Maven. Se recomienda agregar estas herramientas a los scripts de compilación.

Herramienta Complemento de Gradle Complemento de Maven
jdeps jdeps-gradle-plugin Complemento JDeps de Apache Maven
jdeprscan jdeprscan-gradle-plugin Complemento JDeprScan de Apache Maven

El propio compilador de Java, javac, es otra herramienta del cuadro de herramientas. Las advertencias y los errores que obtiene de jdeprscan y jdeps proceden del compilador. La ventaja de usar jdeprscan y jdeps es que puede ejecutar estas herramientas con archivos jar y class existentes, incluidas bibliotecas de terceros.

Lo que jdeprscan y jdeps no pueden hacer es advertir sobre el uso de la reflexión para acceder a la API encapsulada. El acceso reflectante se comprueba en tiempo de ejecución. En última instancia, debe ejecutar el código en Java 11 para saberlo con certeza.

Uso de jdeprscan

La manera más sencilla de utilizar jdeprscan es proporcionarle un archivo jar de una compilación existente. También puede darle un directorio, como el directorio de salida del compilador, o un nombre de clase individual. Use la opción --release 11 para obtener la lista más completa de las API en desuso. Si desea establecer la prioridad de las API en desuso que deben ir después, vuelva a marcar la configuración con --release 8. Es probable que la API que quedó en desuso en Java 8 se quite antes que la API que ha quedado en desuso más recientemente.

jdeprscan --release 11 my-application.jar

La herramienta jdeprscan genera un mensaje de error si tiene problemas para resolver una clase dependiente. Por ejemplo, error: cannot find class org/apache/logging/log4j/Logger. Se recomienda agregar clases dependientes a --class-path o utilizar el argumento class-path, pero la herramienta continuará el examen sin él. El argumento es --class-path. No funcionarán otras variantes del argumento class-path.

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

Esta salida nos indica que la clase com.company.Util está llamando a un constructor en desuso de la clase java.lang.Double. El archivo javadoc recomendará la API que se usará en lugar de la API en desuso. No se puede hacer nada para resolver el error error: cannot find class sun/misc/BASE64Encoder porque se trata de una API que se ha quitado. Desde Java 8, debe usarse java.util.Base64.

Ejecute jdeprscan --release 11 --list para hacerse una idea de qué API han quedado en desuso desde Java 8. Para obtener una lista de las API que se han quitado, ejecute jdeprscan --release 11 --list --for-removal.

Uso de jdeps

Use jdeps con la opción --jdk-internals para buscar dependencias de la API interna de JDK. La opción de línea de comandos --multi-release 11 es necesaria en este ejemplo porque log4j-core-2.13.0.jar es un archivo jar multiversión. Sin esta opción, jdeps notificará si encuentra un archivo jar multiversión. La opción especifica qué versión de los archivos de clase se va a inspeccionar.

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

La salida proporciona algunos consejos útiles para eliminar el uso de la API interna de JDK. Siempre que sea posible, se sugiere una API de reemplazo. El nombre del módulo donde está encapsulado el paquete se indica entre paréntesis. El nombre del módulo se puede usar con --add-exports o --add-opens si es necesario para romper la encapsulación explícitamente.

El uso de sun.misc.BASE64Encoder o sun.misc.BASE64Decoder producirá un error java.lang.NoClassDefFoundError en Java 11. El código que usa estas API debe modificarse para usar java.util.Base64.

Intente eliminar el uso de cualquier API que proceda del módulo jdk.unsupported. La API de este módulo hará referencia a la propuesta de mejora de JDK (JEP) 260 como sugerencia de reemplazo. En pocas palabras, la propuesta JEP 260 indica que se admitirá el uso de la API interna hasta que la API de reemplazo esté disponible. Aunque el código puede usar la API interna de JDK, seguirá funcionando, al menos por un tiempo. Eche un vistazo a la propuesta JEP 260, ya que indica los reemplazos de algunas API internas. Se pueden usar identificadores de variable en lugar de algunas API sun.misc.Unsafe, por ejemplo.

jdeps puede hacer algo más que examinar el uso de los elementos internos de JDK. Es una herramienta útil para analizar las dependencias y para generar archivos module-info. Eche un vistazo a la documentación para más información.

Uso de javac

Para compilar con JDK 11 se necesitan actualizaciones en los scripts de compilación, las herramientas, los marcos de pruebas y las bibliotecas incluidas. Use la opción -Xlint:unchecked de javac para obtener más información sobre el uso de la API interna de JDK y otras advertencias. También puede ser necesario usar --add-opens o --add-reads para exponer los paquetes encapsulados al compilador (consulte la propuesta JEP 261).

Las bibliotecas pueden considerar la posibilidad de empaquetar como un archivo jar multiversión. Los archivos jar multiversión permiten admitir los entornos de ejecución de Java 8 y Java 11 con el mismo archivo jar. Agregan complejidad a la compilación. La forma de crear archivos jar multiversión queda fuera del ámbito de este documento.

Ejecución en Java 11

La mayoría de las aplicaciones se ejecutarán en Java 11 sin necesidad de modificaciones. Lo primero que debe intentar es la ejecución en Java 11 sin volver a compilar el código. El motivo es ver qué advertencias y errores produce la ejecución. Con este enfoque se obtiene una
aplicación que se ejecutará en Java 11 más rápidamente porque solo se harán los cambios mínimos.

La mayoría de los problemas que pueda encontrar se podrán resolver sin tener que volver a compilar el código. Si hay que corregir un problema en el código, hágalo pero siga compilando con JDK 8. Si es posible, trabaje para que la aplicación se ejecute con java versión 11 antes de compilarla con JDK 11.

Consulta de las opciones de la línea de comandos

Antes de ejecutar Java 11, revise rápidamente las opciones de la línea de comandos. Las opciones que se han quitado harán que la máquina virtual Java (JVM) se cierre. Esta comprobación es especialmente importante si usa opciones de registro de GC, ya que han cambiado drásticamente desde Java 8. La herramienta JaCoLine es una buena opción para detectar problemas con las opciones de la línea de comandos.

Comprobación de las bibliotecas de terceros

Una posible fuente de problemas son las bibliotecas de terceros que no controla. Puede actualizar activamente las bibliotecas de terceros a las versiones más recientes. También puede ver qué queda fuera de la ejecución de la aplicación y actualizar solo las bibliotecas necesarias. El problema de actualizar todas las bibliotecas a una versión reciente es que dificulta la búsqueda de la causa principal en caso de errores en la aplicación. ¿Se ha producido el error debido a alguna biblioteca actualizada? ¿O el error se debió a algún cambio en el entorno de ejecución? El problema de actualizar solo lo que se necesita es que puede tardar varias iteraciones en resolverse.

Aquí, la recomendación es realizar el menor número posible de cambios y actualizar las bibliotecas de terceros como un trabajo independiente. Si actualiza una biblioteca de terceros, lo más probable es que quiera la versión más reciente y actualizada que sea compatible con Java 11. Según lo antigua que sea la versión actual, es posible que quiera ser más prudente y actualizar a la primera versión compatible con Java 9+.

Además de consultar las notas de la versión, puede usar jdeps y jdeprscan para evaluar el archivo jar. Además, el grupo de calidad de OpenJDK mantiene una página wiki sobre calidad que muestra el estado de las pruebas de muchos proyectos de software de código abierto (FOSS) gratuitos en las versiones de OpenJDK.

Establecimiento explícito de la recolección de elementos no utilizados

El recolector paralelo de elementos (GC paralelo) no utilizados es el GC predeterminado en Java 8. Si la aplicación usa el valor predeterminado, el GC debe establecerse explícitamente con la opción de la línea de comandos -XX:+UseParallelGC. El valor predeterminado ha cambiado en Java 9 al primer recolector de elementos no utilizados (G1GC). Para realizar una comparación justa de una aplicación que se ejecuta en Java 8 frente a Java 11, la configuración del GC debe ser la misma. La experimentación con la configuración de GC se debe posponer hasta que la aplicación se haya validado en Java 11.

Establecimiento explícito de las opciones predeterminadas

Si utiliza la máquina virtual de zona activa, al establecer la opción de línea de comandos -XX:+PrintCommandLineFlags, se volcarán los valores de las opciones establecidas por la máquina virtual, especialmente los valores predeterminados establecidos por el GC. Ejecute con esta marca en Java 8 y use las opciones impresas cuando ejecute en Java 11. En su mayor parte, los valores predeterminados son los mismos en las versiones 8 y 11. Sin embargo, la configuración de la versión 8 garantiza la paridad.

Se recomienda establecer la opción de línea de comandos --illegal-access=warn. En Java 11, el uso de la reflexión para acceder a la API interna de JDK producirá una advertencia de acceso reflectante no válido. De forma predeterminada, la advertencia solo se emite para el primer acceso no válido. La configuración de --illegal-access=warn producirá una advertencia en todos los accesos reflectantes no válidos. Encontrará más casos de acceso no es válido con la opción establecida warn. Sin embargo, también recibirá muchas advertencias redundantes.
Una vez que la aplicación se ejecute en Java 11, establezca --illegal-access=deny para imitar el comportamiento futuro del entorno de ejecución de Java. A partir de Java 16, el valor predeterminado será --illegal-access=deny.

Precauciones de ClassLoader

En Java 8, puede convertir el cargador de clases del sistema en URLClassLoader. Normalmente, lo hacen las aplicaciones y bibliotecas que desean insertar clases en la ruta de acceso a clases en tiempo de ejecución. La jerarquía del cargador de clases ha cambiado en Java 11. El cargador de clases del sistema (también conocido como cargador de clases de aplicación) ahora es una clase interna. La conversión a URLClassLoader producirá una excepción ClassCastException en tiempo de ejecución. Java 11 no tiene una API para aumentar dinámicamente la ruta de acceso a clases en tiempo de ejecución, pero se puede hacer mediante reflexión, con las advertencias obvias sobre el uso de la API interna.

En Java 11, el cargador de clases de arranque solo carga los módulos principales. Si crea un cargador de clases con un elemento primario NULL, es posible que no encuentre todas las clases de la plataforma. En Java 11, debe pasar ClassLoader.getPlatformClassLoader() en lugar de null como el cargador de clases primario en estos casos.

Cambios de los datos de configuración regional

El origen predeterminado de los datos de configuración regional en Java 11 ha cambiado con la propuesta JEP 252 al repositorio de datos de configuración regional común del consorcio Unicode. Esto puede tener un impacto en el formato localizado. Establezca la propiedad del sistema java.locale.providers=COMPAT,SPI para revertir al comportamiento de la configuración regional de Java 8, si es necesario.

Posibles problemas

Estos son algunos de los problemas que podría encontrar. Siga los vínculos para obtener más información sobre estos problemas.

Opciones no reconocidas

Si se ha quitado una opción de línea de comandos, la aplicación imprimirá Unrecognized option: o Unrecognized VM option, seguido del nombre de la opción incorrecta. Una opción desconocida hará que la máquina virtual se cierre. Las opciones que han quedado en desuso, pero no se han quitado, generarán una advertencia de máquina virtual.

En general, las opciones que se quitaron no tienen ningún reemplazo y el único recurso es quitar la opción de la línea de comandos. La excepción son las opciones del registro de la recolección de elementos no utilizados. El registro de GC se ha vuelto a implementar en Java 9 para usar la plataforma de registro de JVM unificada. Consulte la correspondencia entre las marcas de registro de recolección de elementos no utilizados heredadas y la configuración de Xlog en la tabla 2-2 de la sección sobre cómo habilitar el registro con la plataforma de registro unificada de JVM, en la referencia de las herramientas de Java SE 11.

Advertencias de máquina virtual

La utilización de opciones en desuso producirá una advertencia. Una opción está en desuso cuando se ha reemplazado o ya no es útil. Al igual que con las opciones quitadas, estas opciones se deben quitar de la línea de comandos. La advertencia VM Warning: Option <option> was deprecated significa que todavía se admite la opción, pero que es posible que se quite en el futuro. Una opción que ya no se admite y que generará la advertencia VM Warning: Ignoring option. Las opciones que ya no se admiten no tienen ningún efecto en el entorno de ejecución.

En la página web Explorador de opciones de máquina virtual se proporciona una lista exhaustiva de las opciones que se han agregado o quitado de Java desde JDK 7.

Error: No se pudo crear la máquina virtual Java

Este mensaje de error se imprime cuando la máquina virtual Java encuentra una opción no reconocida.

ADVERTENCIA: Se ha producido una operación de acceso reflectante no válida

Cuando el código Java usa la reflexión para tener acceso a la API interna de JDK, el entorno de ejecución emitirá una advertencia de acceso reflectante no válido.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Esto significa que un módulo no ha exportado el paquete al que se está accediendo mediante reflexión. El paquete está encapsulado en el módulo y es básicamente una API interna. La advertencia puede omitirse como primer esfuerzo para ponerse en marcha en Java 11. El entorno de ejecución de Java 11 permite el acceso reflectante para que el código heredado pueda continuar funcionando.

Para solucionar esta advertencia, busque código actualizado que no use la API interna. Si el problema no se puede resolver con el código actualizado, se puede usar --add-exports o la opción de línea de comandos --add-opens para abrir el acceso al paquete. Estas opciones permiten el acceso a los tipos no exportados de un módulo desde otro módulo.

La opción --add-exports permite que el módulo de destino tenga acceso a los tipos públicos del paquete con nombre del módulo de origen. A veces, el código usará setAccessible(true) para tener acceso a los miembros no públicos y a la API. Esto se conoce como reflexión profunda. En este caso, use --add-opens para dar acceso al código a los miembros no públicos de un paquete. Si no está seguro de si desea usar --add-exports o --add-opens, empiece por --add-exports.

Las opciones --add-exports o --add-opens deben considerarse una solución alternativa, no una solución a largo plazo. El uso de estas opciones rompe la encapsulación del sistema del módulo, que está pensada para evitar que se use la API interna de JDK. Si la API interna se quita o cambia, se producirá un error en la aplicación. Se denegará el acceso reflectante en Java 16, excepto si cuando el acceso se habilite mediante opciones de la línea de comandos, como --add-opens. Para imitar el comportamiento futuro, establezca --illegal-access=deny en la línea de comandos.

La advertencia del ejemplo anterior se emite porque el módulo java.base no exporta el paquete sun.nio.ch. En otras palabras, no hay ningún exports sun.nio.ch; en el archivo module-info.java del módulo java.base. Esto puede resolverse con --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. Las clases que no están definidas en un módulo pertenecen implícitamente al módulo sin nombre, literalmente llamado ALL-UNNAMED.

java.lang.reflect.InaccessibleObjectException

Esta excepción indica que está intentando llamar a setAccessible(true) en un campo o método de una clase encapsulada. También puede obtener una advertencia de acceso reflectante no válido. Use la opción --add-opens para dar acceso al código a los miembros no públicos de un paquete. El mensaje de excepción le indicará que el módulo "no abre" el paquete al módulo que está intentando llamar a setAccessible. Si el módulo es un "módulo sin nombre", use UNNAMED-MODULE como módulo de destino en la opción --add-opens.

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

El error NoClassDefFoundError probablemente se debe a paquete dividido o a una referencia a módulos quitados.

Error NoClassDefFoundError causado por paquetes divididos

Un paquete se llama dividido cuando se encuentra en más de una biblioteca. El síntoma de un problema de paquete dividido es que no se encuentra una clase que se sabe que está en la ruta de acceso a clases.

Este problema solo se produce cuando se usa la ruta de acceso a módulos. El sistema de módulos de Java optimiza la búsqueda de clases mediante la restricción de un paquete a un módulo con nombre. El entorno de ejecución da preferencia a module-path frente a class-path al buscar una clase. Si un paquete se divide entre un módulo y la ruta de acceso a clases, solo se usa el módulo para buscar la clase. Esto puede dar lugar a errores de NoClassDefFound.

Una manera fácil de comprobar un paquete dividido es conectar la ruta de acceso a módulos y clases en jdeps, y usar la ruta de acceso a los archivos de clase de la aplicación como valor de <path>. Si hay un paquete dividido, jdeps imprimirá una advertencia: Warning: split package: <package-name> <module-path> <split-path>.

Para resolver este problema, se puede usar --patch-module <module-name>=<path>[,<path>] para agregar el paquete dividido al módulo con nombre.

Error NoClassDefFoundError causado por el uso de módulos de Java EE o CORBA

Si la aplicación se ejecuta en Java 8, pero produce una excepción java.lang.NoClassDefFoundError o un java.lang.ClassNotFoundException, es probable que la aplicación use un paquete de los módulos de Java EE o CORBA. Estos módulos quedaron en desuso en Java 9 y se quitaron en Java 11.

Para resolver el problema, agregue una dependencia en tiempo de ejecución al proyecto.

Módulo quitado Paquete afectado Dependencia sugerida
Java API for XML Web Services (JAX-WS) java.xml.ws JAX WS RI Runtime
Java Architecture for XML Binding (JAXB) java.xml.bind JAXB Runtime
JavaBeans Activation Framework (JAV) java.activation JavaBeans (TM) Activation Framework
Common Annotations java.xml.ws.annotation Javax Annotation API
Common Object Request Broker Architecture (CORBA) java.corba GlassFish CORBA ORB
Java Transaction API (JTA) java.transaction Java Transaction API

-Xbootclasspath/p ya no es una opción admitida

Se ha quitado la compatibilidad con -Xbootclasspath/p. En su lugar, use --patch-module. La opción --patch-module se describe en la propuesta JEP 261. Busque la sección sobre el contenido del módulo de revisión. --patch-module se puede usar con javac y con java para reemplazar o aumentar las clases de un módulo.

Lo que hace --patch-module es insertar el módulo de revisiones en la búsqueda de clases del sistema del módulo. El sistema del módulo tomará primero la clase del módulo de revisiones. Este es el mismo efecto que bootclasspath en Java 8.

UnsupportedClassVersionError

Esta excepción significa que está intentando ejecutar en una versión de Java código que se compiló con una versión posterior de Java. Por ejemplo, ejecuta en Java 11 con un archivo jar compilado con JDK 13.

Versión de Java Versión del formato de archivo de clase
8 52
9 53
10 54
11 55
12 56
13 57

Pasos siguientes

Una vez que la aplicación se ejecute en Java 11, considere la posibilidad de trasladar las bibliotecas de la ruta de acceso a clases a la ruta de acceso de módulos. Busque versiones actualizadas de las bibliotecas de las que dependa la aplicación. Elija bibliotecas modulares, si están disponibles. Use la ruta de acceso a módulos siempre que sea posible, aunque no tenga previsto usar módulos en su aplicación. El uso de module-path ofrece un mejor rendimiento para la carga de clases que class-path.