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
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; } |
|
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
|
|
// 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 comomodule 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
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; }
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? | |
---|---|
sí |
|
sí pero solo para ciertos módulos |
|
no |
|
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
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
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
Provider
s. 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(); } }
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
[…] 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 […]
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
Muchas gracias por compartir tus conocimientos.
Genial post !