Modularidad en Java 9 (2/2)

0
4991

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:

$ java --list-modules

java.activation@9
java.base@9
java.compiler@9
java.corba@9
...

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:

├──bin
│  └── java
│  └── javac
├──jre
│  └── bin
│  │   └── java
│  └── lib
│      └── rt.jar
└──lib

Pero ahora tiene este otro:

├──bin
│  └── java
│  └── javac
├──conf
├──include
├──jmods
│  └── java.base.jmod
│  └── …
├──legal
└──lib

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:

$ java --describe-module java.logging

java.logging@9
exports java.util.logging
requires java.base mandated
provides jdk.internal.logger.DefaultLoggerFinder with sun.util.logging.internal.LoggingProviderImpl
contains sun.net.www.protocol.http.logging
contains sun.util.logging.internal
contains sun.util.logging.resources
  

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:

    for file in $(/usr/libexec/java_home -v 9)/jmods/*; do mod=`basename $file .jmod`; java --describe-module $mod | grep "requires java.logging" | ifne echo $mod | sort; done
  

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.

brew install bash
chsh -s /usr/local/bin/bash
brew install coreutils moreutils

# esto no tiene que ver, pero son otras cosas útiles que suelo instalar
brew install gnu-sed --with-default-names
brew install git openssl htop pstree

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

jlink --module-path Algorithms/target/classes:Hello/target/classes:$JAVA_HOME/jmods 
      --add-modules Algorithms,Hello 
      --launcher start=Hello/com.example.app.Hello 
      --output Hello-image

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:

image
├── bin
├── conf
├── include
├── legal
└── lib

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

$ jlink --add-modules Algorithms,Hello,HelloTest 
        --suggest-providers algorithms.sort.Sortable
        --module-path Algorithms/target/classes:Hello/target/classes:HelloTest/target/classes:$JAVA_HOME/jmods

Suggested providers:
  Algorithms provides algorithms.sort.Sortable used by Hello

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.

jlink --module-path Algorithms/target/classes:Hello/target/classes:$JAVA_HOME/jmods 
      --add-modules Algorithms,Hello 
      --launcher start=Hello/com.example.app.Hello 
      --output Hello-image
      --bind-services

Para mostrar todos los módulos de una imagen ejecuta

imagen/bin/java --list-modules

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:

$ jlink --module-path jdk-9_windows-x64/jmods

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:

$ jdeps -verbose:class --module-path Algorithms/target/classes 

Algorithms/target/classes/Algorithms/algorithms/sort/Sortable.class 
Sortable.class -> java.base
   algorithms.sort.Sortable  ->  java.lang.Comparable  java.base
   algorithms.sort.Sortable  ->  java.lang.Object      java.base
  

Para mostrar las dependencias de un JAR:

$ jdeps -summary --module-path Algorithms/target/classes:$JAVA_HOME/jmods ./Hello/target/Hello-1.0-SNAPSHOT.jar

Hello -> Algorithms
Hello -> java.base
Hello -> java.logging

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

jdeps -jdkinternals --module-path Algorithms/target/classes
jdeps -jdkinternals -cp Algorithms/target/classes/Algorithms
  

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

cd SimpleHello
jdeps --dotoutput . --module-path Algorithms/target/classes:$JAVA_HOME/jmods Algorithms
brew install graphviz
dot Algorithms.dot -Tpng -o Algorithms.png
open Algorithms.png

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

java --module-path directory --list-modules

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:

      java --show-module-resolution --module-path out -m packt.addressbook/packt.addressbook.Main
    

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

$ jar xvf antlr.jar META-INF/MANIFEST.MF
 inflated: META-INF/MANIFEST.MF
$ echo Automatic-Module-Name: Paquito >> META-INF/MANIFEST.MF 
$ jar umf META-INF/MANIFEST.MF antlr.jar 

Para nombrar un módulo automático con Maven

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Automatic-Module-Name>Paquito</Automatic-Module-Name>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

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

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

jdeps --generate-module-info . commons-logging-api-1.1.jar

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.

$ jdeps --generate-module-info . antlr.jar 
Missing dependence: ./Paquito/module-info.java not generated
Error: missing dependencies
   antlr.CSharpCodeGenerator   -> antlr.actions.csharp.ActionLexer   not found

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

// para cargar un recurso leyendolo del modulepath
ClassLoader.getSystemResources(resourceName)

// para cargarlo si conoces una clase del módulo donde está el recurso
Class.forName(className).getResourceAsStream(resourceName)

// para cargarlo si conoces el módulo que contiene el recurso
ModuleLayer.boot().findModule(moduleName).getResourceAsStream(resourceName)

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.
$ findclass() { for file in $(/usr/libexec/java_home -v 9)/jmods/*; do jar -tf $file | grep $1 | ifne sh -c "echo '\n`basename $file .jmod`' ; cat - " ; done }

$ findclass /DataSource.class

java.activation
classes/javax/activation/DataSource.class

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:

$ java --describe-module java.logging

java.logging@9
exports java.util.logging
requires java.base mandated
provides jdk.internal.logger.DefaultLoggerFinder with sun.util.logging.internal.LoggingProviderImpl
contains sun.net.www.protocol.http.logging
contains sun.util.logging.internal
contains sun.util.logging.resources

Para ver las dependencias hacia java.logging:

for file in $(/usr/libexec/java_home -v 9)/jmods/*; do mod=`basename $file .jmod`; java --describe-module $mod | grep "requires java.logging" | ifne echo $mod | sort; done

Para que estos comandos bash funcionen necesitas
brew install moreutils.

Según el gráfico las dependencias parecen:

java.sql
java.activation
java.xml.crypto
java.rmi
java.security.sasl

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

java.activation
java.corba
java.rmi
java.se
java.security.sasl
java.sql
java.sql.rowset
java.xml.bind
java.xml.crypto
java.xml.ws
javafx.fxml
javafx.web
jdk.deploy
jdk.dynalink
jdk.javaws
jdk.jshell
jdk.packager
jdk.plugin
jdk.policytool
jdk.scripting.nashorn
jdk.security.jgss
jdk.snmp
jdk.xml.bind
jdk.xml.ws

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:

$ java --describe-module java.xml.bind | grep java.logging
requires java.logging

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

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad