Modularidad en Java 9 (1/2)

3
39214

Los módulos son la principal novedad de Java 9. Un módulo agrupa código y recursos como los JARs tradicionales, pero añade además un descriptor que restringe el acceso a sus paquetes, y describe sus dependencias.

El proyecto Jigsaw incluye la implementación del sistema de módulos (JSR 376 and JEP 261), y las mejoras (JEPs) relacionadas. Un puzzle Jigsaw es una imagen dividida en cientos de piezas. De igual modo, la plataforma Java y nuestras aplicaciones, pasan en Java 9 a estar compuestos por docenas de módulos.

En este tutorial veremos

  • Las bases de la encapsulación,
  • los beneficios de la modularidad,
  • cómo escribir un descriptor de módulo,
  • y un ejemplo con varios módulos.

Contenido

Encapsulación

¿Qué es la encapsulación?

Encapsular consiste en ocultar los detalles de implementación y proporcionar una interfaz más simple. Esta estrategia es una herramienta universal para reducir la complejidad. Tu coche por ejemplo, te abstrae los detalles mecánicos bajo su capó, y ofrece una interfaz más simple en forma de pedales y volante.

encapsulación = detalles ocultos + interfaz simplificada

En informática usamos la encapsulación para construir capas de software de complejidad progresiva. En el nivel más bajo, el ordenador almacena información en forma de ceros y unos. Es un sistema parecido al de las transmisiones Morse de hace siglos. La novedad es que los ordenadores son máquinas programables, donde la información representa no solo texto y números, sino programas que realizan cálculos complejos.

Las instrucciones más cercanas a la máquina son tan simples como multiplicar dos números, o concatenar cadenas de texto. Sin embargo, al combinarlas creamos macroinstrucciones, cada vez más parecidas al lenguaje natural. La diferencia es notable:

Scala Ensamblador
print("hola mundo")  
section .data 
 hello:     db 'Hello World',10
 helloLen:  equ $-hello
section .text
global _start
 _start: 
  mov eax,4 
  mov ebx,1 
  mov ecx,hello  
  mov edx,helloLen
  int 80h 
  mov eax,1 
  mov ebx,0 
  int 80h  

En lenguajes de alto nivel las instrucciones se organizan en elementos de complejidad creciente:

instrucciones > funciones > clases > librerías > frameworks > aplicaciones

Cada uno de estos elementos tiene un nombre y propósito específico. Esta es una exigencia de nuestra memoria. Cuando algo crece demasiado necesitamos dividirlo y etiquetarlo para poder recordarlo. Usamos nombres como «capa de persistencia», «cola de mensajes», etc. Estas abstracciones proporcionan una visión de alto nivel que facilita el razonamiento.
Además, los componentes resultantes son reemplazables, y sus posibles defectos están acotados. Es sentido común que el diseño modular es preferible al monolítico.

En resumen

  • Un programa es complejo porque contiene muchas instrucciones simples.
  • La encapsulación nos permite agruparlas en componentes significativos y operar a alto nivel con ellos.

Encapsulación en Java 8

¿Qué herramientas de encapsulación nos proporciona Java 8?

  • Paquetes
  • Clases, y clases anidadas.
  • Modificadores de acceso
  • JARs que agrupan paquetes relacionados
  • Patrones de diseño que exponen un interfaz y ocultan la implementación
  • Manejadores de dependencias de terceros, como
    • Maven, que gestiona dependencias en tiempo de compilación
    • OSGi, que gestiona dependencias en tiempo de ejecución

Estas herramientas planteaban problemas:

  • Classpath Hell: el classpath (conjunto de clases cargadas) puede contener clases duplicadas, clases no disponibles, dependencias con diferentes versiones de una misma librería, o cargadores de clases anidados con comportamientos complicados.
  • Las clases cargadas carecen de información sobre su origen.
  • Una vez cargadas, todas las clases están disponibles por reflexión, y carecen de información sobre su origen.
  • El entorno de ejecución contiene la plataforma entera. En Java 8 existen profiles, pero sigue siendo una granularidad muy grande.

Estos problemas están ligados a la implementación del compilador, el runtime, y la funcionalidad del lenguaje. Para solucionarlos era necesario cambiarlos, y eso ha hecho el proyecto Jigsaw.

Modularidad en Java 9

Los módulos de Java 9 mejoran así la plataforma:

  • Encapsulación fuerte. La encapsulación se cumple durante compilación y ejecución, incluso frente a intentos de reflexión.
  • Configuración fiable. El runtime comprueba la disponibilidad de las dependencias antes de lanzar la aplicación.
  • Creación de imágenes que empaqueta la aplicación con una plataforma Java hecha a medida. Esto implica

    • Menores requerimientos de memoria y disco (útil para microservicios y dispositivos pequeños)
    • Mayor seguridad, porque el código implicado es menor.
    • Optimización mejorada (dead-code elimination, constant folding, compression, etc.).
  • Servicios desacoplados sin escaneo del classpath (las implementaciones de un interfaz se indican explícitamente).
  • Carga rápida de tipos. El sistema sabe dónde está cada paquete sin tener que escanear el classpath.
  • Preserva las fronteras establecidas por la arquitectura.

La encapsulación fuerte implica otros beneficios, como la posibilidad de realizar pruebas aisladas de un módulo, evitar la decadencia del código al introducir dependencias accidentales, y la reducción de dependencias cuando varios equipos trabajan en paralelo.

¿Qué es un módulo?

En el diccionario, un módulo es una parte de algo más complejo. En Java, llamamos módulo a un artefacto que puede contener código, recursos, y metadatos. Los metadatos describen dependencias con otros módulos, y regulan el acceso a los paquetes del módulo.

El conjunto de ficheros que forman un módulo se agrupa en uno de estos tres formatos

  • Formato explotado. Un directorio que contiene el código fuente, datos, y descriptor de módulo.
  • JAR. Ídem pero empaquetado en un JAR.
  • JMOD. Lo mismo que un JAR, pero además puede contener código nativo.

Esto es un ejemplo de módulo en formato JAR:

  holamundo.jar
   ├── com
   │   └── ejemplo
   │       └── HolaMundo.class
   └── module-info.class
  

Descriptor

El
descriptor de módulo es la meta-información sobre el módulo. Contiene lo siguiente:

  • Nombre del módulo
  • Paquetes expuestos
  • Dependencias con otros módulos
  • Servicios consumidos e implementados

Los descriptores se escriben en un fichero module-info.java en la raíz del fichero JAR o directorio. Este fichero se compila junto al resto de ficheros Java. Observa que el nombre de fichero no es un identificador legal Java porque contiene un guión.
Esto es a propósito para evitar que las herramientas lo confundan con un fichero Java. Es el mismo truco que se usa con package-info.java.

Este es un fichero module-info.java de ejemplo:

  module ejemplo {
      requires java.util.logging;
      exports com.ejemplo;
  }
  
  • El módulo se llama
    ejemplo
  • Depende del paquete
    java.util.logging
  • Y expone el paquete com.ejemplo

Cheatsheet

Un módulo se define con las siguientes palabras clave:

exports… to

expone un paquete, opcionalmente a un módulo concreto

import

el típico import de Java. Lo normal es usar nombres completos de paquetes en vez de imports, pero si repites mucho un tipo, puede ser de utilidad.

module

Comienza la definición de un módulo.

open

Permite la reflexión en un módulo.

opens

Permite la reflexión en un paquete concreto, para alguno o todos los paquetes.

provides…with

Indica un servicio y su implementación.

requires, static, transitive

requires indica la dependencia con un módulo. Añade
static para que sea requerido durante compilación y opcional durante la ejecución. Añade
transitive para indicar dependencia con las dependencias del módulo requerido.

  // nombre del módulo. open permite la reflexión en todo el módulo
  open module com.ejemplo 
  {
      // exporta un paquete para que otros módulos accedan a sus paquetes públicos
      exports com.apple;
  
      // indica una dependencia con el módulo com.orange
      requires com.orange;
  
      // indica una dependencia con com.banana. el 'static' hace que la dependencia 
      // sea obligatoria durante compilación pero opcional durante ejecución
      requires static com.banana;
  
      // indica una dependencia al módulo com.berry y sus dependencias
      requires transitive com.berry;
  
      // permite reflexión en el módulo com.pear
      opens com.pear;
  
      // permite reflexión en el paquete com.lemon pero solo desde el módulo com.mango
      opens com.lemon to com.mango;
  
      // expone el tipo MyImplementation que implementa el servicio MyService
      provides com.service.MyService with com.consumer.MyImplementation
  
      // usa el servicio com.service.MyService
      uses com.service.MyService
  }
  

Hola Mundo

classpath

Este es un hola mundo normal, compilado y ejecutado en el classpath.

holamundo
├── Makefile
└── src
    └── com
        └── ejemplo
            └── HolaMundo.java

donde Makefile es

compile:
	javac `find src -name "*.java"` -d build
run:
	java -cp build com.ejemplo.HolaMundo
clean:
	rm -rf build

modules

Este es el mismo hola mundo compilado y ejecutado como módulo.

holamundo
├── Makefile
└── src
    └── holamundo
        ├── com
        │   └── ejemplo
        │       └── HolaMundo.java
        └── module-info.java

Hay tres cambios que convierten el código en un módulo:

  • Añadimos un descriptor module-info.java en el directorio raíz del código fuente. Puede ser tan simple como module holamundo {}.
  • Movemos todo el código fuente a un directorio con el mismo nombre que dimos al módulo en el descriptor.
  • Compilamos indicando el directorio del código fuente del módulo:
javac --module-source-path src `find src -name "*.java"` -d build

Ya solo falta lanzarlo como módulo:

java --module-path build --module holamundo/com.ejemplo.HolaMundo

¿Qué pasa si lo lanzamos una aplicación modular usando el classpath? se ejecuta igualmente. El fichero module-info.java es ignorado porque lleva un guión, y por tanto no cuenta como código java. Puedes probarlo con:

java -cp build/Holamundo com.ejemplo.HolaMundo

Para clonar el código de este ejemplo:

git clone https://github.com/j4n0/holamundo-modules.git

Descriptor de módulo

module

module define el nombre de un módulo.

  module ejemplo {}
  
  • El nombre de un módulo debe ser un identificador válido en Java o un nombre válido de paquete.
  • El nombre debe ser único. Si hay dos módulos con el mismo nombre en diferentes directorios, solo se usa uno de ellos. Es una buena idea usar nombres inversos de dominio para garantizar que el nombre es único.
  • El nombre no debe coincidir con el de una clase, interfaz, o paquete. No porque cause errores, sino porque sería confuso.

O al menos, ese es el consejo de Oracle. Si es un módulo privado y usas ejemplo, en vez de com.ejemplo, estará más claro cuál es el módulo y cuál el paquete. Pero si es un API pública, te evitaras colisiones usando el nombre de dominio.

requires

requires indica una dependencia a un módulo.

  module ejemplo {
      requires java.logging;
  }
  

Ten en cuenta que no están permitidas las dependencias cíclicas durante compilación por varios motivos:

  • Impediría la compilación. Un tipo solo puede compilarse si los tipos de los que dependen ya han sido compilados.
  • No sería un buen diseño. Dos módulos en un ciclo, son en la práctica equivalentes a un único módulo. Normalmente las dependencias de un sistema discurren en un único sentido, de componentes generales a más específicos y no al revés. Coloquialmente hablando, yo uso al martillo, pero el martillo no me usa a mí.

static

requires static indica una dependencia obligatoria durante compilación, pero opcional durante ejecución.

  module HelloTest {
      requires static HelloLogger;
  }
  

Si el módulo HelloLogger no es accesible en tiempo de ejecución, los intentos de cargarlo devolverán nulo.

Para modelar una dependencia opcional con reflexión

  try {
      Class clazz = Class.forName("com.example.logger.HelloLogger");
      System.out.println("Using reflection: HelloLogger is " + clazz.getConstructor().newInstance());
  } catch (ReflectiveOperationException e) {
      System.out.println("Using reflection: HelloLogger not loaded");
  }
  

Para modelar una dependencia opcional con ServiceLoader

  Optional logger = ServiceLoader.load(HelloLogger.class).findFirst();
  

Esto lleva más trabajo. Hay que poner una línea uses en el módulo que carga la instancia:

  module HelloTest
  {
      requires static HelloLogger;
      uses com.example.logger.HelloLogger;
  }
  

Y una línea provides en el módulo que proporciona el tipo:

  module HelloLogger {
      exports com.example.logger;
      provides com.example.logger.HelloLogger with com.example.logger.HelloLogger;
  }
  

transitive

requires transitive indica dependencias con las dependencias de un módulo. Es decir, si A→B y B→C, entonces A→C.

  module A {
      requires transitive B;
  }
  

Si quieres visualizar las dependencias que fueron requeridas transitivamente pero que no están disponibles, añade el flag -Xlint:exports a javac.

exports

exports indica que los tipos públicos de un paquete pueden usarse desde otros módulos.

  module Hello {
      exports com.example.app;
  }
  

Decimos que un paquete es legible si está exportado, y que un tipo es accesible si es legible y además es public.

exports to

exports to indica que los tipos públicos de un paquete están disponibles pero solo para cierto(s) paquete. En inglés lo llaman “qualified export”.

  module HelloLogger {
      exports com.example.logger to com.example.junit;
  }
  

Hay un problema con este enfoque. Si tengo el módulo independiente HelloLogger y añado el qualified export a com.example.junit, estoy acoplando ambos módulos. Los qualified exports están reservados para casos especiales. Por ejemplo, el paquete sun.* estaba pensado para uso privado, pero una vez en el classpath cualquiera podía usarlo. Para programas que dependen de él podemos hacerlo de nuevo accesible con un qualified export, aunque la mejor solución es sustituirlo por sus alternativas.

Ejemplo

Teniendo esto en cuenta, en JDK 9 el significado de public depende de si el tipo está exportado o no.

¿es en un paquete exportado?

public es accesible

sí pero solo para ciertos módulos

public es accesible para esos módulos

no

public es legible pero no accesible

Supongamos un módulo “ejemplo” con el siguiente contenido:

  // Clyde.java
  package com.ejemplo.bar;
  public class Clyde {}
  
  // Inky.java
  package com.ejemplo.foo;
  class Inky {}
  
  // Pinky.java
  package com.ejemplo.foo;
  public class Pinky {}
  
  // module-info.java
  module ejemplo {
      exports com.ejemplo.foo;
      exports com.ejemplo.bar to holamundo;
  }
  

Si un módulo requiere este módulo ejemplo

  • Clyde es accesible sólo para el módulo holamundo
  • Inky es legible pero no accesible
  • Pinky es accesible

Además, Pinky y Clyde pueden usarse con cualquier tipo del módulo al que pertenecen, de acuerdo con los modificadores de acceso.

open

open permite la reflexión en un módulo o en un paquete concreto.

Llamamos deep reflection a la reflexión de tipos privados en Java. Por defecto, en el sistema de módulos sólo es posible hacer reflexión de métodos públicos pertenecientes a módulos exportados. Si el elemento es privado salta una excepción de tipo InaccessibleObjectException, y si es público pero no exportado
salta una excepción de tipo IllegalAccessException.

Para permitir deep reflection de todos los miembros de un módulo usa open.

  open module HelloLogger {
      exports com.example.logger;
  }
  

Para permitir deep reflection de un paquete de un método usa opens.

    module HelloLogger {
      exports com.example.logger;
      opens com.example.logger;
      opens com.example.logger to junit;
  }
  

Para permitir deep reflection de un paquete de un módulo de terceros añade el flag
--add-opens al comando java. Por ejemplo, para que el paquete myframework realice deep reflection en el paquete java.lang del módulo java.base escribe:

--add-opens java.base/java.lang=myframework

Para realizar reflexión en módulos exportados

    try {
      Field f = HelloLogger.class.getDeclaredField("isEnabled");
      f.setAccessible(true);
  } catch (NoSuchFieldException e) {
      fail("Reflecting private field: " + e.toString());
  }
  
  try {
      Method m = HelloLogger.class.getDeclaredMethod("_debug", String.class);
      m.setAccessible(true);
      Object target = HelloLogger.class.getConstructor().newInstance();
      m.invoke(target, "");
  } catch (ReflectiveOperationException e) {
      fail("Reflecting private field: " + e.toString());
  }
  

Para realizar reflexión en módulos no exportados

Es lo mismo que con módulos exportados, pero tienes que añadir un --add-opens desde línea de comando y usar Class.forName("com.example.whatever") en vez de referirte al nombre del tipo.

Servicios

provides with

provides with indica que el módulo proporciona un tipo que implementa el servicio.

En Java 8, para implementar un servicio sin exponer la implementación podemos usar una factoría, o escanear el classpath para buscar el servicio. El sistema de módulos de Java 9 permite declarar los servicios explícitamente en los descriptores,
lo cual es más rápido que un escaneo del classpath. Un servicio de Java 9 se compone de tres elementos: servicio, proveedor, y consumidor.

Para declarar el tipo servicio puedes usar un interfaz, clase abstracta, o clase Java. Este tipo debería estar en un paquete accesible por consumidor y proveedor. En este ejemplo defino un interfaz público y expongo su paquete en el
descriptor:

  package algorithms.sort;
  public interface Sortable {
      <T extends Comparable> void sort(T[] values);
  }
  
  module algorithms.sort {
      exports algorithms.sort;
  }
  

Para registrar el proveedor hay que

  • Requerir el módulo que contiene la definición del servicio
  • Registrar la implementación del servicio usando la formula
    provides SERVICE with IMPLEMENTATION; Para los nombre de servicio e implementación tienes que usar el nombre completo, incluyendo el paquete.

El siguiente ejemplo requiere el módulo
algorithms.sort, donde está el interfaz que he definido antes, y registro la implementación
algorithms.sort.BubbleSort. Observa que la implementación del servicio no necesita ser expuesta.

  module sortProvider {
      requires algorithms.sort;
      provides algorithms.sort with algorithms.sort.BubbleSort;         
  }
  

Para implementar el proveedor el tipo debe tener un constructor sin argumentos, y/o un método
public static provider() que devuelva el tipo del servicio. Por ejemplo,

  class BubbleSort implements Sortable {
      public <T extends Comparable> void sort(T[] values) {
          Arrays.sort(values); // imagina que esto es un bubblesort
      }
      BubbleSort(){}
  }
  

uses

uses indica que el módulo usa un servicio dado.

Para registrar el consumidor del servicio usa la palabra clave
uses.

  module consumer {
      requires algorithms;
      uses algorithms.sort.Sortable;
  }
  

Para cargar el servicio usa el API ServiceLoader. Hay dos modos de hacerlo: lookup o stream.

  // lookup
  ServiceLoader loader = ServiceLoader.load(Sortable.class);
  Sortable sortable = loader.iterator().next();
  sortable.sort(values);
  
  // stream
  Stream<ServiceLoader.Provider> providers = ServiceLoader.load(Sortable.class).stream();
  final Optional<ServiceLoader.Provider> sortable2 = providers.findFirst();
  sortable2.get().get().sort(values2);
  

Cualquiera de las dos maneras lleva solo unas pocas líneas, pero serían demasiadas si tuvieras que usarlo a menudo. Otra manera más es devolver la lista de implementaciones desde el propio interfaz. Por ejemplo,

  public interface Sortable {
    <T extends Comparable> void sort(T[] values);
    static Iterable getSortables() {
        return ServiceLoader.load(Sortable.class);
    }
  }
  

Instanciarlo el
Provider del servicio es útil si queremos obtener información del tipo que lo implementa, antes de instanciar el servicio en sí.

¿Porqué deberíamos instanciar un servicio de JDK 9 en vez de usar una factoría? Los servicios son más rápidos de instanciar porque tienen un descriptor explícito con el que encontrarlos. Esto permite añadir implementaciones en forma de módulos sin escanear
el classpath cada vez. Además, el API de
ServiceLoader proporciona funciones de cacheo de servicios.

Para listar los proveedores de un servicio puedes usar jlink:

  jlink --module-path MODULEPATH --add-modules NOMBRESDEMODULOS --suggest-providers SERVICIO
  

En nuestro ejemplo:

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

JDK 9

Homebrew es una forma sencilla de instalar y actualizar Java:

  brew update
  brew tap caskroom/cask
  brew install brew-cask
  brew cask install java
  

Para añadir al path los comandos del JDK edita
.bashrc con el contenido siguiente:

  export JAVA_HOME=$(/usr/libexec/java_home)
  export PATH="$JAVA_HOME/bin:$PATH"
  

Si alguna vez quieres alternar entre Java 8 y 9:

  # instala Java 8
  brew cask install java8

  # cambia a Java 8
  export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
  export PATH="$JAVA_HOME/bin:$PATH"

  # o cambia de vuelta a Java 9
  export JAVA_HOME=$(/usr/libexec/java_home -v 9)
  export PATH="$JAVA_HOME/bin:$PATH"
  

javac

¿Qué nuevas opciones relativas a módulos hay para javac?

--add-exports Hace legible un paquete no exportado en el descriptor de su módulo.
--add-modules Módulos raíz a resolver.
--limit-modules Limita los módulos observables.
--module Compila solo el módulo indicado.
--module-path Directorio donde están los módulos dependientes.
--module-source-path Directorios de código fuente para los módulos.
--module-version Versión de los módulos que estamos compilando.
--processor-module-path Ruta a los módulos que contienen procesadores de anotaciones.
--upgrade-module-path Módulos actualizables.

java

¿Qué nuevas opciones relativas a módulos hay para java?

--add-exports Hace legible un paquete no exportado en el descriptor de su módulo.
--illegal-access Permite saltarse la encapsulación. Los valores posibles son permit, warn, debug, deny.
--add-modules Añade módulos al modulepath.
--add-opens Permite reflexión de un paquete a otro. El formato es
–add-opens módulo/paquete=módulo_que_realiza_el_acceso
--describe-module Describe el contenido del módulo.
--list-modules Lista los módulos observables.
--module-path Directorios conteniendo los módulos.
--upgrade-module-path Actualiza módulos en la imagen.
--validate-modules Valida los módulos del module path y termina.

Al usar –add-module-path podemos hay nombres con significados especiales como ALL-DEFAULT, ALL-SYSTEM, ALL-MODULE-PATH. Curiosamente cuando hacemos java –help no muestra las opciones illegal-access, y add-opens.

Si los comandos java o javac te quedan tan largo como tu brazo, puedes llevarte las opciones a un fichero de texto, por ejemplo argumentos.txt, y ejecutar java @argumentos.txt. Esta es una novedad en Java 9. Puedes incluso pasar varios ficheros, por ejemplo java @1.txt @2.txt.

Ejemplos

Multiproyecto Maven

Voy a empezar con un multiproyecto Maven.

Creo el proyecto

Creo el proyecto padre e hijo

  mvn archetype:generate \
  -DarchetypeGroupId=org.codehaus.mojo.archetypes \
  -DarchetypeArtifactId=pom-root \
  -DarchetypeVersion=RELEASE \
  -DgroupId=com.example \
  -DartifactId=SimpleHello \
  -Dversion=1.0-SNAPSHOT \
  -DinteractiveMode=false
  
  cd SimpleHello
  
  mvn archetype:generate \
  -DarchetypeGroupId=org.apache.maven.archetypes \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=RELEASE \
  -DgroupId=com.example.app \
  -DartifactId=Hello \
  -Dversion=1.0-SNAPSHOT \
  -DinteractiveMode=false 
  

Al crear el segundo aparece un aviso:

WARNING: Illegal reflective access by org.dom4j.io.SAXContentHandler 
(file:/Users/jano/.m2/repository/dom4j/dom4j/1.6.1/dom4j-1.6.1.jar) to method 
com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy.getEncoding()

El comportamiento por defecto es permitir el acceso ilegal y mostrar un warning la primera vez, así que lo que vemos es normal. Este es el equivalente de pasar --illegal-access=permit a la JVM, que es un flag descrito en este mail.

El siguiente problema que encontré fue que el Maven compila por defecto para Java 1.5. Esto se arregla fácil en el pom.xml del proyecto raíz:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
                <source>1.9</source>
                <target>1.9</target>
            </configuration>
        </plugin>
    </plugins>
</build>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

mvn package compila, y ejecuta las pruebas. Renombro App.java y AppTest.java a Hello.java y HelloTest.java. Compruebo que mvn package sigue funcionando. Ahora voy a ejecutar la clase principal así que añado el plugin de ejecución al pom.xml del módulo
hijo.

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.6.0</version>
            <configuration>
                <executable>java</executable>
                <arguments>
                    <argument>-classpath</argument>
                    <classpath/>
                    <argument>com.example.app.Hello</argument>
                </arguments>
            </configuration>
        </plugin>
    </plugins>
</build>
  

Ya tenemos un proyecto multimódulo en Maven donde podemos compilar, probar, y ejecutar.

  cd Hello
  mvn package exec:exec
  

Este es mi layout por ahora:

      SimpleHello
      ├── Hello
      │   ├── pom.xml
      │   └── src
      │       ├── main
      │       │   └── java
      │       │       └── com
      │       │           └── example
      │       │               └── app
      │       │                   └── Hello.java
      │       └── test
      │           └── java
      │               └── com
      │                   └── example
      │                       └── app
      │                           └── HelloTest.java
      └── pom.xml
  

El warning de antes

Antes me salté unos detalles sobre el warning de acceso ilegal. Como experimento probé a pasar deny a la JVM de Maven de estas dos maneras pero no funcionó:

  • Crea un fichero .mvn/jvm.config que contenga --illegal-access=deny
  • Pasa la opción -DargLine=--illegal-access=deny al comando Maven

La opción illegal-access solo funciona para paquetes que existían en el JDK 8, pero este sí existía así que debería haber funcionado. No sé que magia está haciendo Maven internamente para que esto falle (lanzando un nuevo proceso?).

Las opciones en sí, funcionan. Puedes probarlas con este ejemplo:

import java.lang.reflect.Method;
class Untitled {
    public static void main(String[] args) throws Exception {
        Method method = Class.forName("com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy").getMethod("getEncoding", new Class[0]);
        method.setAccessible(true);
        System.out.println(method);
    }
}

Para denegar la reflexión:

javac Untitled.java && java --illegal-access=deny Untitled

Para permitir la reflexión desde el módulo unnamed (ALL-UNNAMED):

javac Untitled.java && java --add-opens java.xml/com.sun.org.apache.xerces.internal.parsers=ALL-UNNAMED Untitled

Conversión a módulos

Para modularizar el proyecto he hecho esto:

  • Divido el proyecto y sus pruebas en proyectos diferentes.
  • Añado un module-info.java a cada uno para convertirlos en módulos.
  • Pongo el código y sus pruebas en paquetes diferentes (porque un mismo paquete no puede existir en diferentes módulos).
  .
  ├── Hello
  │   ├── pom.xml
  │   └── src
  │       └── Hello
  │           ├── com
  │           │   └── example
  │           │       └── app
  │           │           └── Hello.java
  │           └── module-info.java
  ├── HelloTest
  │   ├── pom.xml
  │   └── src
  │       └── HelloTest
  │           ├── com
  │           │   └── example
  │           │       └── junit
  │           │           └── HelloTest.java
  │           └── module-info.java
  └── pom.xml
  

Los descriptores son los siguientes:

  module Hello {
      requires java.logging;
      exports com.example.app;
  }
  

Para las pruebas necesitamos los módulos JUnit, Hello y sus dependencias, y tenemos que permitir el acceso de JUnit al código de ejemplo.

  module HelloTest {
      requires junit;
      requires transitive Hello;
      exports com.example.junit to junit;
  }
  

JUnit carece de descriptor de módulo, por tanto es un módulo automático. Cuando ejecutamos
mvn package aparece este warning:

  [WARNING] * Required filename-based automodules detected. Please don't publish this project to a public artifact repository!
  

Se refiere a que hay módulos en el module path cuyo nombre ha sido inferido del nombre de fichero y puede que no sea el correcto. Durante una larga temporada tendremos que tratar con warnings, y plugins no actualizados para JDK 9. En este caso
particular es un warning sin importancia.

Ejecución con Maven

Si ejecutamos
mvn package exec:exec desde el módulo Hello, el plugin exec-maven-plugin ejecuta el módulo como un JAR normal. Eso ocurre porque por defecto usa classpath y no modulepath. Vamos a cambiarlo para que ejecute módulos.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>exec</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <executable>${JAVA_HOME}/bin/java</executable>
        <arguments>
            <argument>--module-path</argument>
            <modulepath/>
            <argument>--module</argument>
            <argument>Hello/com.example.app.Hello</argument>
        </arguments>
    </configuration>
</plugin>
  

El comando para ejecutar las pruebas en el sistema de modularidad es un poco más largo. Hasta que Maven soporte mejor el sistema de modularidad este es posiblemente el mejor modo.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>exec</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <executable>java</executable>
        <arguments>
            <argument>--module-path</argument>
            <modulepath/>
            <argument>--add-modules</argument>
            <argument>HelloTest</argument>
            <argument>--module</argument>
            <argument>junit/org.junit.runner.JUnitCore</argument>
            <argument>com.example.junit.HelloTest</argument>
        </arguments>
    </configuration>
</plugin>
  

Ejecución con Makefile

Para ejecutar con make, he añadido estos Makefile a Hello y HelloTest

  run:
    javac `find src/Hello -name "*.java"` -d out/Hello
    java --module-path out/Hello --add-modules Hello com.example.app.Hello
  test:
    make -C ../HelloTest run
  
  run: ../Hello/out
    javac --module-path lib:../Hello/out `find src/HelloTest -name "*.java"` -d out/HelloTest
    java --module-path lib:../Hello/out:out/HelloTest --add-modules Hello,HelloTest -m junit/org.junit.runner.JUnitCore com.example.junit.HelloTest
  ../Hello/out:
    make -C ../Hello run
  

La ejecución de pruebas requería Junit y Hamcrest así que los he descargado a un directorio lib. Podría haber hecho una referencia al repositorio de Maven pero no he querido liarme más.

Idea

Para cargarlo en Idea, arranca y sigue estos pasos

  • Import project
  • From external model > Maven, pulsa Next
  • Pulsa estas opciones:
    • Import Maven projects automatically
    • Create module groups for multi-module Maven projects
    • Next
  • Pulsa Next, Next, Finish.

Dado que el código de pruebas está en un módulo Maven aparte, se importa correctamente y no hay problemas. Si hubiéramos dejado el layout original de Maven, Idea se quejaría de que los módulos tienen el mismo directorio raíz.

requires

Voy a pintar hola mundo con la clase Logger como excusa para importar un paquete.

  package com.example.app;
  import java.util.logging.Logger;
  public class Hello {
      private final static Logger LOGGER = Logger.getLogger(Hello.class.getName());
      public static void main(String[] args) {
          LOGGER.info("Hello World!");
      }
      public Hello(){}
  }
  

Observa que Logger está en el módulo java.logging, no java.util.logging. Aunque la documentación recomienda usar el nombre de paquete principal, los módulos de Java se toman libertades para abreviar el nombre del módulo.

  module Hello {
      requires java.logging;
      exports com.example;
  }
  

Añadir un servicio

Para instanciar un servicio hacemos un lookup con la ServiceLoader API y a continuación instanciamos con un la clase, u obtenemos un stream de
Providers. El stream de proveedores nos permite inspeccionar el servicio antes de instanciarlo.

Aquí estoy instanciado el servicio a partir del primer elemento del iterador. Este es un ejemplo trivial, pero en un proyecto real debes controlar el caso en el que el iterador no devuelva elementos.

  public static void main(String[] args) {
      LOGGER.info("Hello World!");
      Integer[] values = {1,9,7,3};
  
      Class clazz;
      try {
          clazz = (Class) Class.forName("algorithms.sort.Sortable");
          ServiceLoader loader = ServiceLoader.load(clazz);
          Sortable sortable = loader.iterator().next();
          sortable.sort(values);
          System.out.println(Arrays.toString(values));
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
  }
  

Código de ejemplo

Casi todos los ejemplos en este tutorial están en esta aplicación:

https://github.com/j4n0/SimpleHello

  .
  ├── Algorithms        Módulo que proporciona el servicio Sortable
  ├── Hello             Módulo cliente del servicio en Algorithms
  ├── HelloLogger       Módulo para ilustrar la reflexión
  ├── HelloTest         Módulo cliente del servicio en Algorithms
  ├── Makefile          Run make to compile, test, and run all modules
  ├── pom.xml           POM de la raíz del proyecto Maven
  └── showProviders.sh  Encuentra proveedores para servicios
  

Puedes ejecutar este código

  • Con make: haciendo un make en el directorio raíz, o dentro del directorio de cualquiera de los módulos.
  • Con maven: haciendo un
    mvn install en el directorio raíz, y un
    mvn exec:exec dentro del directorio HelloTest.
  • Con Idea: ejecutando el módulo HelloTest. Sin embargo con Idea no me ha funcionado uno de los ejemplos con el ServiceLoader, imagino que por tema de configuración de Idea, no le dedique tiempo, se admiten sugerencias!

Referencias

Y ahora, si te gusta pulsar en enlaces y leer documentación, pulsa en estos y finalmente serás feliz:

Youtube

3 COMENTARIOS

  1. Hola, he leído tu aportación y es bastante buena, agradezco el tiempo que dedicas para explicarnos de manera sencilla estos detalles sobre la modularidad de java 9.

    Ojalá me puedas orientar un poco mas, yo tengo una aplicación hecha en javafx, la cuál ejecuto con un jar que me genera la estructura tradicional para este caso era

    Proyecto
    — lib
    — ejecutable.jar

    Por poner un ejemplo, anteriormente yo solo tenía que ejecutar el «ejecutable.jar» y me desplegaba mi aplicación de escritorio. mi duda es con java 9 como aplica esto, ejerciendo la modularidad?

    He hecho algunas pruebas pero cuando genero el jar y trato de ejecutarlo siempre me dice que no encuentra las clases.

    Ojalá me puedas orientar un poco sobre esto.

    Gracias por tu dedicación y tiempo

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