Introducción a los módulos de Java (JPMS)
Qué son los módulos en Java, los problemas que resuelve JPMS y cómo se relaciona con el classpath.
Introducción a los módulos de Java (JPMS)
El Java Platform Module System (JPMS), introducido en Java 9, añade una capa por encima de los paquetes. Un módulo es un grupo de paquetes con nombre y autodescriptivo que declara explícitamente dos cosas: qué necesita de otros módulos y qué les ofrece. Esa declaración reside en un único archivo, module-info.java, en la raíz del módulo. Esta parte del libro lo recorre; este capítulo explica por qué existe.
El problema que resuelve JPMS: el classpath
Antes de Java 9, cada JAR se arrojaba a un único classpath plano. Esa disposición tenía problemas crónicos:
- Sin encapsulación. Cada clase
publicen cada JAR era alcanzable por todos. Una clase pensada solo como ayudante interno (sun.misc.Unsafe,com.example.internal.*) podía ser usada por cualquiera, así que nunca podía cambiarse de forma segura. - Sin dependencias declaradas. Un JAR nunca decía qué otros JARs requería. Usted descubría una dependencia faltante solo cuando un
NoClassDefFoundErrorestallaba en tiempo de ejecución — posiblemente en producción. - El infierno de los JARs. Dos JARs que suministraban el mismo paquete, o dos versiones de la misma biblioteca, se fusionaban silenciosamente en el orden del classpath. Ganaba la clase que se cargaba primero.
Los módulos atacan los tres: un módulo oculta cada paquete que no export-a explícitamente, declara cada módulo que requires, y la JVM verifica todo el grafo al arrancar — los módulos faltantes o duplicados fallan rápido.
Módulos vs. paquetes vs. JARs
Estos tres son fáciles de confundir:
| Concepto | Qué agrupa | Regla de visibilidad |
|---|---|---|
| Paquete | clases | public/protected/privado-de-paquete dentro del JAR |
| JAR | paquetes + recursos | todo lo public es visible en el classpath |
| Módulo | paquetes | solo los paquetes exported son visibles a otros módulos |
Un módulo se empaqueta normalmente como un JAR (un «JAR modular» — un JAR corriente con un module-info.class en su raíz). La diferencia es el descriptor: deje el JAR en el classpath y las reglas se ignoran; póngalo en el module path y JPMS las hace cumplir.
La encapsulación fuerte, en una frase
La regla principal: un paquete es invisible a otros módulos a menos que su módulo lo export-e — incluso si sus clases son public. public ahora significa «accesible al código que puede leer este paquete», y leer un paquete requiere un exports más un requires. Por eso el propio JDK por fin pudo ocultar sus internos: java.base exporta java.util pero no jdk.internal.misc.
El JDK también es modular
Desde Java 9 el JDK está dividido en ~70 módulos (java.base, java.sql, java.xml, java.net.http, …). java.base es especial: lo requiere implícitamente cada módulo y contiene los elementos esenciales del lenguaje (java.lang, java.util, java.io). Cada clase que alguna vez haya usado vive en uno de estos módulos — lo que el ejemplo trabajado de abajo hace visible.
Un ejemplo trabajado: inspeccionar módulos en tiempo de ejecución
No necesita escribir un módulo para ver módulos: la API Module de tiempo de ejecución informa el módulo de cualquier clase. Este programa pregunta a varias clases a qué módulo pertenecen, comprueba su propio módulo y echa un vistazo a la capa de arranque con la que la JVM se inició.
Qué llevarse de la ejecución:
String,ArrayListyHttpClientinformaronjava.base,java.baseyjava.net.http. Cada clase pertenece a exactamente un módulo, ygetModule()le dice cuál — los tipos del lenguaje viven todos enjava.base, mientras queHttpClientestá en su propio módulo que tendría querequires java.net.httppara usar.- La clase propia del programa informó
isNamed() == falsey ungetName()denull. El código ejecutado desde el classpath aterriza en el módulo sin nombre, un cubo de compatibilidad que no requiere nada explícitamente y lee todos los demás módulos. Por eso los programas del classpath siguen compilando y ejecutándose sin cambios en Java 9+. ModuleLayer.boot()expuso el grafo de módulos que la JVM resolvió al arrancar — contar losjava.*muestra que el JDK realmente está dividido en muchos módulos, no en un monolito.java.baseno está abierto (isOpen() == false) y aun así exporta muchos paquetes; exponejava.langyjava.utila todos manteniendojdk.internal.*oculto. Exportar un paquete y abrir un módulo son interruptores diferentes — un capítulo posterior vuelve aopens.- Nada de esto requirió un
module-info.java. La API Module son metadatos reflexivos disponibles para cualquier programa; escribir su propio módulo (capítulo siguiente) es lo que le permite a usted declarar estas reglas en lugar de solo observar las del JDK.
Qué cubre el resto de esta parte
module-info.java— las directivas:requires,exports,opens,uses,provides.- Tipos de módulos — módulos con nombre, automáticos y sin nombre, y cómo se mezclan.
- Servicios — desacoplar una interfaz de su implementación con
uses/providesyServiceLoader. - Migración — mover una aplicación de classpath existente al module path sin una reescritura de golpe.
Los módulos son opcionales: una aplicación Java puede ejecutarse para siempre en el classpath. Pero entenderlos explica el JDK moderno, desbloquea los runtimes personalizados de jlink y da a las bibliotecas una encapsulación real. El siguiente capítulo escribe el descriptor que hace de un módulo un módulo.
Práctica
Un JAR de biblioteca contiene una clase 'public' en el paquete 'com.acme.internal' que los autores destinan solo para uso interno de la biblioteca. El JAR se construye como un JAR modular cuyo 'module-info.java' exporta 'com.acme.api' pero no 'com.acme.internal'. ¿Qué ocurre cuando este JAR se coloca en el MODULE PATH y otro módulo intenta importar 'com.acme.internal.Helper'?