Modularidad en Java 9 (2/2)

En esta segunda parte exploramos el JDK modular, la resolución de módulos, carga de recursos, y cómo migrar una aplicación a módulos. Si no leíste la primera parte, te recomiendo que lo hagas ahora.

Contenido

1. El JDK Modular

En el año 2000 el JDK tenía múltiples dependencias entre paquetes no relacionados:

source: Mark Reinhold

Esto ocurre porque en un proyecto en el que no haya límites claros –como una división en librerías– es fácil tomar atajos que creen dependencias sin que nadie lo note. Incluso en un proyecto con desarrolladores disciplinados como el JDK, hicieron falta años para refactorizar hacia un sistema modular.

En 2017, el JDK modularizado tiene este aspecto (versión simplificada):

source: Mark Reinhold

El JDK 9 tiene 94 módulos. Puedes listarlos ejecutando:


Verás que los módulos tienen los siguientes prefijos:

java

Módulos del núcleo de la plataforma Java. Estos son los módulos del lenguaje, módulos enterprise, o módulos agregadores que agrupan los dos anteriores.

javafx

Módulos de Java FX.

jdk

APIs y herramientas del JDK como el compilador, javadoc, y consola. No son parte de la especificación del lenguaje.

jdk.incubator

Módulos experimentales sujetos a cambios. Por ejemplo, jdk.incubator.httpclient.

jdk.unsupported

Tipos no soportados y que pueden ser eliminadas en cualquier momento. Por ejemplo, sun.misc.Unsafe .

oracle

Módulos específicos de la implementación de Oracle del JDK.


Los módulos agregadores son módulos sin código propio que agrupan a otros módulos mediante sentencias requires transitive. En el JDK hay dos:

  • java.se: Java Standard Edition
  • java.se.ee: Java Enterprise Edition

Un proyecto bien diseñado no debería requerir los módulos agregadores, a menos que necesite la plataforma entera, lo cual es improbable.

1.1. La estructura del JDK

El JDK previo a Java 9 tenía este aspecto:

Pero ahora tiene este otro:

Los cambios en la estructura de directorios están documentados en JEP 220: Modular Run-Time Images.

2. Herramientas

El JDK tiene varias herramientas para operar con módulos:

jdeps

Muestra las dependencias de código con el JDK y módulos de terceros.

jdeprscan

Muestra código obsoleto (deprecated).

jlink

Crea imágenes que contienen la aplicación y un runtime de tamaño mínimo para ejecutarla.

jmod

Trabaja con ficheros jmod. Este formato es un JAR con código nativo, ficheros de configuración, y otros datos.


Para describir un módulo, por ejemplo java.logging:

Esto muestra los paquetes que contiene, exporta, servicios provistos, usados, y paquetes requeridos.

Para listar que paquetes dependen de otro, por ejemplo de java.logging, hace falta un poco de bash:

Ojo Mac users, el script de arriba solo funciona si tienes un bash moderno. MacOS tiene un bash de hace diez años por temas de licencias. Escribir scripts compatibles con POSIX requiere más trabajo, así que el mío lo actualizo con homebrew. Hay otros scripts en este tutorial que requieren esta actualización.

2.1. jlink

jlink construye una imagen de runtime con solo lo necesario para ejecutar la aplicación. La imagen es nativa del sistema operativo donde la creas.

¿Para qué sirve crear una imagen?

  • Menor consumo de disco y memoria. Esto es especialmente útil al usar microservicios e instalar en dispositivos móviles.
  • Aplicaciones autocontenidas.
  • Optimizaciones en tiempo de enlazado. Eliminación de código inalcanzable, compresión de la imagen, constant folding, inlining de funciones, etc.
  • Mayor seguridad, porque a menor código, menor probabilidad de que nos afecten los defectos de una clase concreta.

jlink está estructurado en plugins. Puedes ejecutar jlink --list-plugins para verlos todos. Estos son dos plugins interesantes que están desactivados por defecto:

compress

Compresión de la imagen. Puedes no comprimir (0), crear una tabla global de cadenas (1), o comprimir en zip (2).

include-locales

Para añadir localización española pasa --include-locales=es. Si quieres varios idiomas, sepáralos por comas.


Para crear una imagen

Este comando está en el script createHelloImagen.sh del código de ejemplo que acompaña a este artículo. La estructura de la imagen resultante es esta:

En caso de que el módulo realice reflexión de otro y no lo declare, no se incluirá en la imagen. Aun así, puedes ejecutar la imagen y añadir el módulo con el flag --add-modules. jlink solo funciona si tu proyecto está formado por módulos explícitos (no módulos anónimos ni automáticos).

Para mostrar los proveedores de un servicio

Pruebalo ejecutando ./showProviders.sh en el código de ejemplo.

Para añadir por adelantado todas las implementaciones de un servicio en los módulos observables, añade el flag --bind-services al comando de creación de imagen. El efecto será que todos los módulos que contengan una implementación de un servicio serán añadidos a la imagen.

Para mostrar todos los módulos de una imagen ejecuta

Al hacer esto para la Hello-image que había creado, me encontré 23 módulos del jdk y 17 java. No entiendo porqué empaqueta módulos como java.smartcardio. En total sale una imagen de 146Mb en disco, frente a los 517Mb del JDK. Si además la comprimimos la imagen se queda en 75Mb.

Para comprimir la imagen añade los flags --strip-debug and --compress=n, donde n puede ser 0, 1, o 2.

Para escoger la máquina virtual Hotspot usa --vm={client|server|minimal|all}. Las opciones minimal y client no funcionan en MacOS. Al comprimir la imagen solo cambia un fichero: imagen/lib/modules, que pasa de 114Mb a 41Mb.

Para añadir un idioma que no sea el inglés, añade el flag --add-module jdk.localedata. Este módulo contiene las traducciones para todos los idiomas soportados. El inglés ya está incluido en jdk.localedata.

Para crear una imagen para otro sistema operativo tienes que bajar el JDK de esa plataforma y apuntar al directorio que contiene sus jmods. ¿Porqué no valen los jmods de tu JDK? porque los jmods, además de código Java multiplataforma, también contienen código nativo. Suponiendo que hayamos descargado el JDK 9 de Windows, ejecutamos esto:

Un problema adicional es extraer los jmods de un JDK nativo. Si tienes MacOS, y quieres crear una imagen para Windows, Oracle te da un JDK en formato .exe (ejecutable de Windows). Necesitarás un sistema operativo Windows para poder descomprimirlo.

2.2. jdeps

jdeps realiza un análisis estático del bytecode de la aplicación y muestra los módulos JDK imprescindibles para ejecutar la clase o JAR que estemos analizando. Como con jlink, cuidado con usar reflexión, porque jdeps no la detecta.

Para mostrar las dependencias de una clase:

Para mostrar las dependencias de un JAR:

Si además quieres mostrar las dependencias transitivas en tiempo de ejecución añade el flag -recursive.

Para buscar usos de código privado en nuestro código

En JDK 9, el código que use código privado del JDK (por ejemplo, sun.*) producirá errores. Podemos exportar este código privado mediante el flag --add-exports, pero la solución correcta es usar las clases del JDK que reemplazan a sun.*.

Para mostrar un gráfico de dependencias

SimpleHello es el ejemplillo de la parte 1 del tutorial. Idea muestra un gráfico mucho mejor que este, pero si quieres generar un PNG para un javadoc por ejemplo, con graphviz queda algo así:

3. Resolución de módulos

Resolver un módulo es calcular el número mínimo de módulos a partir de un grafo de dependencias y un módulo raíz de ese grafo. Esto lo realiza el compilador y el entorno de ejecución. Ojo, los módulos implicados en este cálculo se leen del module path, no del classpath.

Llamamos módulos observables a aquellos a los que la plataforma tiene acceso para satisfacer las dependencias entre módulos. Están formados por los módulos de la plataforma y los módulos del directorio que pasemos por línea de comando.

Para ver los módulos observables en un directorio dado

Podemos pasar varios directorios separados por el carácter de separación de directorios del sistema operativo. En MacOS, el carácter es : (dos puntos), en Windows es ; (punto y coma).

Para resolver los módulos el compilador o entorno de ejecución

  • Empieza en el módulo que contiene el punto de entrada a la aplicación.
  • Lee la lista de módulos requeridos por el módulo raíz y hace recursión por las dependencias de esos módulos.

La resolución de errores terminará con error si

  • Falta un módulo requerido.
  • Hay dependencias cíclicas entre módulos.
  • Hay dos módulos con el mismo nombre.
  • Hay un paquete partido (split package). Los paquetes partidos son paquetes cuyo código existe en dos módulos observables. Esto es un error de compilación que podría evitarse en tiempo de ejecución usando un cargador de clases adicional, pero la solución correcta es simplemente poner el paquete en un único módulo.

Para ver la resolución de módulos lanza la aplicación con el flag --show-module-resolution:

3.1. El módulo anónimo

El módulo anónimo (unnamed module) es aquel módulo donde se añade el código para el que no hay definido explícitamente un módulo. El módulo anónimo tiene las características siguientes:

  • El código en el módulo anónimo puede leer todo el código del classpath sin restricciones. Sin embargo, no puede acceder el código no exportado de los módulos observables.
  • Durante la compilación, el módulo anónimo usa al módulo java.se como módulo raíz (por tanto, el código de java.se.ee no es legible por defecto).
  • El módulo anónimo sólo es legible desde otros módulos anónimos.
  • Si hay un paquete definido en un módulo anónimo y otro no anónimo, el paquete del módulo anónimo es ignorado.

Cada cargador de clases tiene un módulo anónimo, devuelto por el método ClassLoader::getUnnamedModule.

3.2. Módulos automáticos

Un módulo automático es un JAR normal cargado desde el modulepath. Se comporta como un módulo porque el sistema de módulos crea un descriptor automático. Los módulos automáticos tienen las características siguientes:

  • Tiene acceso a todos los módulos, incluyendo al módulo anónimo.
  • Requiere de forma transitiva todos los módulos en el modulepath.
  • Todos sus paquetes son legibles (están exportados por defecto).
  • Su nombre de módulo se decide a partir del nombre de fichero, pero también podemos escribirlo nosotros en el fichero META-INF/MANIFEST.MF.
  • No puede tener un paquete que también exista en otro módulo.
  • Requiere que las dependencias del propio jar estén presentes. Podemos usar jdeps -recursive -summary somefile.jar para mostrar sus dependencias a otros módulos, y jdeps --generate-module-info . somefile.jar para generar un descriptor automático de un módulo.

Con este ya hemos visto los tres tipos de módulos que existen:

Tipo

¿Qué es?

¿Qué lee?

anónimo

código en el classpath

módulos explícitos y automáticos

explícito

un JAR + descriptor de módulo

módulos explícitos y automáticos

automático

JAR en el modulepath

lee transitivamente 1 todos los módulos
(anónimo, automático, y explícito)

1 La transitividad solo aplica a los módulos explícitos porque son los únicos que declaran dependencias.

3.2.1. Cómo nombrar un módulo

  • Por defecto es el nombre del fichero sin el número de versión ni la extensión jar. Cualquier guión en el nombre se convierte en un punto.
  • También podemos darle valor añadiendo una entrada al manifiesto del JAR. Por ejemplo: Automatic-Module-Name: Paquito.

Para nombrar un módulo automático con el manifiesto

Para nombrar un módulo automático con Maven

Para nombrar un módulo automático con un descriptor

Esto lo podemos hacer a mano o automáticamente con jdeps:

Cuando falta código nativo

He puesto un ejemplo con commons-logging porque con antlr no me funcionó. Me salía un error que decía ActionLexer not found, que es una clase que está ahí, pero luego he visto que necesita código nativo.

3.3. Layering

Cuando el entorno de ejecución calcula el grafo de módulos accesibles, crea un layer, que mapea cada módulo en el grafo al cargador de clases responsable de cargar los tipos definidos en ese módulo.

Cuando la máquina virtual arranca, crea un “boot layer” que resuelve el módulo que contiene el punto de entrada a la aplicación contra los módulos observables. Normalmente este será el único layer creado, pero en aplicaciones sofisticadas es posible añadir layers adicionales que definan un conjunto diferente de módulos observables, o carguen más de una versión de un módulo.

Las aplicaciones que usan layers adicionales pueden ser IDEs, entornos de ejecución de pruebas, servidores de aplicaciones, o aplicaciones que extiendan su funcionalidad con plugins.

Si quieres saber más sobre el tema consulta

4. El API de Módulos

El API de módulos permite operar con módulos: carga, lectura, modificación, construcción, búsqueda.

Su código está en estos paquetes:

4.1. Carga de Recursos

Ten cuenta que cuando un fichero está en el directorio raíz o dentro del directorio META-INF, el recurso no está encapsulado, y por tanto es accesible mediante escaneo del modulepath usando ClassLoader.

Para ver un ejemplo completo puedes consultar esta respuesta de SO, o clonar este repositorio de GitHub.

5. Migración

Tienes que dar dos pasos:

  • Compila y ejecuta la aplicación en JDK 9 con classpath.
  • Mueve tu aplicación a módulos y sustituye el classpath por el modulepath.

5.1. JDK 9 con classpath

Cuando compilas y ejecutas tu aplicación con JDK 9 usando el classpath, tu código y librerías pasan a estar en el módulo anónimo. Todo módulo respecta las reglas de encapsulación de otros módulos, así que posiblemente encuentres infracciones de encapsulación que tendrás que arreglar. A continuación los comento.

5.1.1. Posibles problemas

  • Reflexión de elementos privados en elementos del JDK. O sea, tú, o una librería que usas ha hecho un setAccessible(true) a un elemento privado. Para facilitar la migración, esto es un warning (equivalente a pasar --illegal-access=permit en línea de comandos), pero en futuras releases lo cambiarán a ser un error (--illegal-access=deny). Por eso es buena idea lanzar la app con un deny para hacerte una idea del trabajo que podrías tener cuando lo cambien.
  • Uso de código interno de la plataforma. Por ejemplo, estás usando sun.*.
    • Si el código interno continúa siendo accesible no requiere acción alguna por tu parte, pero deberías pensar en usar alguna alternativa listada en Replace use of JDK’s internal APIs. Estas alternativas te las muestra jdeps si sacas un listado de accesos al código interno desde tu código.
    • Si el código interno es público pero no está exportado y quieres una solución temporal, puedes exportar el paquete desde línea de comandos, tanto en java como en javac, usando --add-exports módulo/paquete=móduloAlQueExportas. Para exportar al módulo anónimo usa ALL-UNNAMED como nombre.
    • Si el código interno ha sido eliminado o no hay alternativa clara tendrás que buscarte la vida.
  • Paquetes partidos. Este es el caso en el que tienes un mismo paquete en diferentes módulos. Muevelos al mismo módulo o renombralos.
  • Clases movidas a java.se.ee. Hay paquetes que ahora están en Java EE así que tienes que importar el módulo correspondiente.

Para saber en que módulo está una clase

  • Si estás en Idea puedes escribir el nombre de la clase en el código fuente y dejar que te sugiera un import. Por ejemplo, escribiendo Datasource sugiere importar javax.activation.DataSource. El import aparecerá inicialmente en rojo si pertenece a un módulo no requerido, y haciendo mouseover te sugerirá el nombre de ese módulo que falta.
  • Sin ayuda de un IDE, si sabes en qué paquete está puedes hacer por ejemplo: java --list-modules | grep activation. A mí esto alguna vez no me ha funcionado así que hice el siguiente script para buscar por nombre de clase. Desconozco si hay un modo más sencillo.

5.2. JDK 9 con módulos

En este punto deberías tener tu código y librerías en el classpath y funcionando. Si no vas a mantener la aplicación puedes parar aquí. En caso contrario te sugiero estos pasos:

  • Empaqueta y ejecuta tu código como módulo automático. Tienes un ejemplo en el holamundo del primer tutorial. No necesitas cambiar o añadir código, lo único que cambia es el comando javac y java.
  • Convierte tu código a un módulo no automático. Tras al paso anterior, solo necesitas añadir un descriptor de módulo.
  • Si consigues que todo funcione, intenta actualizar las librerías a versiones preparadas para JDK 9.
  • Divide tu código en múltiples módulos de acuerdo con tu arquitectura.

Antes de migrar a módulos deberías entender el ejemplo holamundo del que hablé antes, los tres tipos de módulo, y los tipos de acceso de cada uno.

5.2.1. Posibles problemas

Es posible que alguna de las librerías añadidas como módulo automático necesite jars adicionales. Si usas un gestor de dependencias como Maven, no debería ser un problema porque ya te descarga él todas las dependencias.

Los módulos automáticos exportan todos sus paquetes, pero si no son public y alguna librería quiere hacer reflexión, tendrás que darle permiso.

  • Si el código es tuyo, una manera sencilla es exponer el módulo entero para reflexión con open module {}. Luego puedes ir afinando.
  • Si el código está en una librería externa, tienes que usar los flags de javac y java para permitir la reflexión.

Problemas relacionados con las herramientas que utilizas.

  • El soporte de Jigsaw en los plugins de Maven debería irse actualizando aquí. Si tienes algún problema y entiendes lo que está pasando, probablemente puedas hacer un workaround.
  • Idea tiene un soporte completo para módulos. Es capaz de mostrar un diagrama con las dependencias de módulos, sugerencias, inspecciones, etc. En el momento de escribir esto, en la lista de bugs abiertos había cuatro abiertos que no eran críticos.

6. WTF java.logging

En Project Jigsaw: JDK modularization destacan java.util.logging como módulo problematico. Si miras el gráfico del año 2000, logging tenía quince flechas entrantes, solo superado por base (java.lang, java.io, java.net, java.nio, security), que tiene 20. A su vez logging tenía una dependencia circular con management (JMX), que dependía de JNDI, RMI, CORBA, que a su vez etc. etc. El resultado eran dependencias transitivas que abarcaban la plataforma entera.

Ahora logging tiene cinco flechas entrantes y una saliente. Las dependencias entre módulos se obtienen de sus descripciones. Para mostrar la descripción de un módulo por terminal puedes usar java --describe-module. Por ejemplo, para ver las dependencias de java.logging:

Para ver las dependencias hacia java.logging:

Para que estos comandos bash funcionen necesitas brew install moreutils.

Según el gráfico las dependencias parecen:

Pero según el comando de arriba, los siguientes módulos requieren java.logging:

La diferencia es que el gráfico de 2017 no muestra javafx, ni jdk, y lo que parece una dependencia transitiva en, por ejemplo, java.xml.bind, es en realidad una dependencia directa:

En realidad no es tan malo porque el logging es un caso en el que la información fluye en una única dirección, y java.util.logging unicamente expone un interfaz que podemos implementar. Ni siquiera es un logging configurable. El Platform Logging API sería buen material para otro artículo.

7. Conclusión

Esta es más o menos toda la teoría sobre el sistema de módulos.

Cosas que no vimos –y por las que preguntaré en mi lecho de muerte con mi último aliento–: internacionalización, layering API, el futuro de OSGi.

Cosas que sí hemos visto:

Enlaces donde puedes encontrar más información:

Spring 5 ya soporta Java 9, y los microservicios se benefician de un tamaño más reducido, así que pronto estaremos migrando a Java 9. Si usas Spring boot lee este workaround.

Como al montar en bici, la experiencia se consigue practicando. Cuando encuentres dificultades, espero que estas páginas te sirvan de guía. peace out bitches