¿Qué es JPMS? Presentación del sistema de módulos de plataforma Java

Hasta Java 9, el elemento de organización de código de nivel superior de Java había sido el paquete. Comenzando con Java 9 que cambió: encima del paquete ahora está el módulo. El módulo recopila paquetes relacionados juntos.

El Java Platform Module System (JPMS) es una estructura a nivel de código, por lo que no cambia el hecho de que empaquetamos Java en archivos JAR. En última instancia, todo sigue empaquetado en archivos JAR. El sistema de módulos agrega un nuevo descriptor de nivel superior que los JAR pueden usar al incorporar el module-info.javaarchivo.

Las aplicaciones y organizaciones a gran escala aprovecharán los módulos para organizar mejor el código. Pero todos consumirán módulos, ya que el JDK y sus clases ahora están modularizados.

Por qué Java necesita módulos

JPMS es el resultado del proyecto Jigsaw, que se llevó a cabo con los siguientes objetivos declarados: 

  • Facilite a los desarrolladores la organización de grandes aplicaciones y bibliotecas
  • Mejorar la estructura y seguridad de la plataforma y del propio JDK
  • Mejorar el rendimiento de la aplicación
  • Maneja mejor la descomposición de la plataforma para dispositivos más pequeños

Vale la pena señalar que JPMS es una característica de SE (Edición estándar) y, por lo tanto, afecta todos los aspectos de Java desde cero. Dicho esto, el cambio está diseñado para permitir que la mayoría del código funcione sin modificaciones al pasar de Java 8 a Java 9. Hay algunas excepciones a esto, y las notaremos más adelante en esta descripción general.

La idea principal detrás de un módulo es permitir la recopilación de paquetes relacionados que son visibles para el módulo, mientras se ocultan elementos de los consumidores externos del módulo. En otras palabras, un módulo permite otro nivel de encapsulación.

Ruta de clases frente a ruta del módulo

En Java, hasta ahora, la ruta de clases ha sido el resultado final de lo que está disponible para una aplicación en ejecución. Aunque la ruta de clases sirve para este propósito y se comprende bien, termina siendo un cubo grande e indiferenciado en el que se colocan todas las dependencias.

La ruta del módulo agrega un nivel por encima de la ruta de clases. Sirve como contenedor de paquetes y determina qué paquetes están disponibles para la aplicación.

Módulos en el JDK

El propio JDK ahora está compuesto por módulos. Comencemos mirando las tuercas y tornillos de JPMS allí.

Si tiene un JDK en su sistema, también tiene la fuente. Si no está familiarizado con el JDK y cómo obtenerlo, consulte este artículo.

Dentro de su directorio de instalación de JDK hay un /libdirectorio. Dentro de ese directorio hay un src.ziparchivo. Descomprímelo en un /srcdirectorio.

Mire dentro del /srcdirectorio y navegue hasta el /java.basedirectorio. Allí encontrará el module-info.javaarchivo. Abrelo.

Después de los comentarios de Javadoc en la cabecera, encontrará una sección nombrada  module java.base seguida de una serie de exportslíneas. No nos detendremos en el formato aquí, ya que se vuelve bastante esotérico. Los detalles se pueden encontrar aquí.

Puede ver que muchos de los paquetes familiares de Java, como java.io, se exportan desde el java.basemódulo. Esta es la esencia de un módulo que reúne los paquetes.

La otra cara de  exportsla requiresmoneda es la instrucción. Esto permite que un módulo sea requerido por el módulo que se está definiendo.

Al ejecutar el compilador de Java contra módulos, especifica la ruta del módulo de manera similar a la ruta de clases. Esto permite resolver las dependencias.

Crear un proyecto Java modular

Echemos un vistazo a cómo está estructurado un proyecto Java modulado.

Vamos a hacer un pequeño programa que tiene dos módulos, uno que proporciona una dependencia y el otro que usa esa dependencia y exporta una clase principal ejecutable.

Cree un nuevo directorio en algún lugar conveniente de su sistema de archivos. Llámalo /com.javaworld.mod1. Por convención, los módulos de Java viven en un directorio que tiene el mismo nombre que el módulo.

Ahora, dentro de este directorio, cree un module-info.javaarchivo. En el interior, agregue el contenido del Listado 1.

Listado 1: com.javaworld.mod1 / module-info.java

module com.javaworld.mod1 { exports com.javaworld.package1; }

Observe que el módulo y el paquete que exporta tienen nombres diferentes. Estamos definiendo un módulo que exporta un paquete.

Ahora crea un archivo en este camino, dentro del directorio que contiene el module-info.javaarchivo: /com.javaworld.mod1/com/javaworld/package1. Nombra el archivo  Name.java. Coloque el contenido del Listado 2 en su interior.

Listado 2: Name.java

 package com.javaworld.package1; public class Name { public String getIt() { return "Java World"; } } 

El Listado 2 se convertirá en una clase, paquete y módulo del que dependemos.

Ahora creemos otro directorio paralelo /com.javaworld.mod1 y llamémoslo /com.javaworld.mod2. En este directorio, module-info.javacreemos una definición de módulo que importe el módulo que ya creamos, como en el Listado 3.

Listado 3: com.javaworld.mod2 / module-info.java

 module com.javaworld.mod2 { requires com.javaworld.mod1; } 

El Listado 3 se explica por sí mismo. Define el com.javaworld.mod2módulo y requiere com.javaworld.mod1.

Dentro del /com.javaworld.mod2directorio, crear una ruta de clase de esta manera: /com.javaworld.mod2/com/javaworld/package2.

Ahora agregue un archivo dentro llamado Hello.java, con el código provisto en el Listado 4.

Listado 4: Hello.java

 package com.javaworld.package2; import com.javaworld.package1.Name; public class Hello { public static void main(String[] args) { Name name = new Name(); System.out.println("Hello " + name.getIt()); } } 

En el Listado 4, comenzamos definiendo el paquete y luego importando la com.javawolrd.package1.Nameclase. Tenga en cuenta que estos elementos funcionan como siempre. Los módulos han cambiado la forma en que los paquetes están disponibles a nivel de estructura de archivos, no a nivel de código.

Similarly, the code itself should be familiar to you. It simply creates a class and calls a method on it to create a classic “hello world” example.

Running the modular Java example

The first step is to create directories to receive the output of the compiler. Create a directory called /target at the root of the project. Inside, create a directory for each module: /target/com.javaworld.mod1 and /target/com.javaworld.mod2.

Step 2 is to compile the dependency module, outputting it to the /target directory. At the root of the project, enter the command in Listing 5. (This assumes the JDK is installed.)

Listing 5: Building Module 1

 javac -d target/com.javaworld.mod1 com.javaworld.mod1/module-info.java com.javaworld.mod1/com/javaworld/package1/Name.java 

This will cause the source to be built along with its module information.

Step 3 is to generate the dependent module. Enter the command shown in Listing 6.

Listing 6: Building Module 2

 javac --module-path target -d target/com.javaworld.mod2 com.javaworld.mod2/module-info.java com.javaworld.mod2/com/javaworld/package2/Hello.java 

Let’s take a look at Listing 6 in detail. It introduces the module-path argument to javac. This allows us to define the module path in similar fashion to the --class-path switch. In this example, we are passing in the target directory, because that is where Listing 5 outputs Module 1.

Next, Listing 6 defines (via the -d switch) the output directory for Module 2. Finally, the actual subjects of compilation are given, as the module-info.java file and class contained in Module 2.

To run, use the command shown in Listing 7.

Listing 7: Executing the module main class

 java --module-path target -m com.javaworld.mod2/com.javaworld.package2.Hello 

The --module-path switch tells Java to use /target directory as the module root, i.e., where to search for the modules. The -m switch is where we tell Java what our main class is. Notice that we preface the fully qualified class name with its module.

You will be greeted with the output Hello Java World.

Backward compatibility 

You may well be wondering how you can run Java programs written in pre-module versions in the post Java 9 world, given that the previous codebase knows nothing of the module path. The answer is that Java 9 is designed to be backwards compatible. However, the new module system is such a big change that you may run into issues, especially in large codebases.

When running a pre-9 codebase against Java 9, you may run into two kinds of errors: those that stem from your codebase, and those that stem from your dependencies.

For errors that stem from your codebase, the following command can be helpful: jdeps. This command when pointed at a class or directory will scan for what dependencies are there, and what modules those dependencies rely on.

For errors that stem from your dependencies, you can hope that the package you are depending on will have an updated Java 9 compatible build. If not you may have to search for alternatives.

One common error is this one:

How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

This is Java complaining that a class is not found, because it has migrated to a module without visibility to the consuming code. There are a couple of solutions of varying complexity and permanency, described here.

Nuevamente, si descubre tales errores con una dependencia, consulte con el proyecto. Es posible que tengan una compilación de Java 9 para su uso.

JPMS es un cambio bastante radical y llevará tiempo adoptarlo. Afortunadamente, no hay prisa urgente, ya que Java 8 es una versión de soporte a largo plazo.

Dicho esto, a largo plazo, los proyectos más antiguos deberán migrar y los nuevos deberán utilizar los módulos de forma inteligente, con suerte aprovechando algunos de los beneficios prometidos.

Esta historia, "¿Qué es JPMS? Introducción al sistema de módulos de plataforma Java" fue publicada originalmente por JavaWorld.