Módulos en Java (desde Java 9)
Introducción
El sistema de módulos es una de las características más importantes introducidas en Java 9, también conocida como Project Jigsaw. Este sistema representa un cambio fundamental en la forma en que se organizan, encapsulan y distribuyen las aplicaciones Java. Los módulos permiten definir explícitamente las dependencias entre componentes, proporcionan un mejor control de la visibilidad y permiten crear sistemas más seguros y eficientes. En este artículo, exploraremos en profundidad el sistema de módulos de Java, su propósito, sus beneficios y cómo implementarlos en tus propias aplicaciones.
¿Qué son los módulos en Java?
Un módulo en Java es una agrupación de paquetes relacionados y recursos junto con un descriptor que especifica:
- El nombre del módulo
- Los paquetes que exporta (hace visibles a otros módulos)
- Los módulos de los que depende
- Los servicios que ofrece y consume
- Otros metadatos relacionados
Antes de Java 9, el classpath era el mecanismo principal para gestionar dependencias, pero presentaba varias limitaciones: no había encapsulación real a nivel de JAR, las dependencias no se especificaban explícitamente y existía el problema conocido como "classpath hell", donde podían ocurrir colisiones de nombres y otras complicaciones.
El descriptor de módulo: module-info.java
El núcleo del sistema de módulos es el archivo module-info.java
, que debe estar ubicado en la raíz del código fuente del módulo. Este archivo define la relación del módulo con otros módulos del sistema.
Veamos un ejemplo básico:
module com.miempresa.aplicacion {
// Módulos de los que dependemos
requires java.base; // Implícito, pero puede especificarse
requires java.sql;
// Paquetes que exponemos a otros módulos
exports com.miempresa.aplicacion.api;
// Paquetes que exponemos, pero solo a módulos específicos
exports com.miempresa.aplicacion.interno to com.miempresa.plugin;
// Permitir reflexión desde otros módulos
opens com.miempresa.aplicacion.modelo;
// Servicios que proporcionamos
provides com.miempresa.servicio.MiServicio
with com.miempresa.aplicacion.MiServicioImpl;
// Servicios que consumimos
uses com.miempresa.plugin.Plugin;
}
Vamos a explicar cada una de estas directivas:
Directiva requires
Especifica los módulos de los que depende nuestro módulo. Por ejemplo:
requires java.sql; // Dependemos del módulo java.sql
Existe una variante transitiva que propaga la dependencia:
requires transitive java.sql; // Nuestros consumidores también dependerán de java.sql
Directiva exports
Define los paquetes que son accesibles desde fuera del módulo:
exports com.miempresa.aplicacion.api; // Este paquete es visible desde cualquier módulo
También podemos restringir la exportación a módulos específicos:
exports com.miempresa.aplicacion.interno to com.miempresa.plugin;
Directiva opens
Permite el acceso mediante reflexión a las clases de un paquete:
opens com.miempresa.aplicacion.modelo; // Permite reflexión sobre este paquete
También se puede limitar a módulos específicos:
opens com.miempresa.aplicacion.modelo to com.miempresa.orm;
Directivas provides
y uses
Se utilizan para declarar y consumir servicios en el contexto de ServiceLoader
:
// Proporcionamos una implementación de MiServicio
provides com.miempresa.servicio.MiServicio with com.miempresa.aplicacion.MiServicioImpl;
// Consumimos el servicio Plugin
uses com.miempresa.plugin.Plugin;
Creación de una aplicación modular
Veamos un ejemplo práctico de cómo crear una aplicación modular sencilla. Crearemos dos módulos: un módulo principal y un módulo de utilidades.
Estructura de directorios
proyectomodular/
├── src/
│ ├── com.miempresa.app/
│ │ ├── module-info.java
│ │ └── com/
│ │ └── miempresa/
│ │ └── app/
│ │ └── Principal.java
│ └── com.miempresa.utilidades/
│ ├── module-info.java
│ └── com/
│ └── miempresa/
│ └── utilidades/
│ └── Calculadora.java
Definición de los módulos
Módulo de utilidades (com.miempresa.utilidades):
// module-info.java
module com.miempresa.utilidades {
exports com.miempresa.utilidades;
}
// Calculadora.java
package com.miempresa.utilidades;
public class Calculadora {
public int sumar(int a, int b) {
return a + b;
}
public int restar(int a, int b) {
return a - b;
}
public int multiplicar(int a, int b) {
return a * b;
}
public double dividir(int a, int b) {
if (b == 0) {
throw new ArithmeticException("No se puede dividir por cero");
}
return (double) a / b;
}
}
Módulo principal (com.miempresa.app):
// module-info.java
module com.miempresa.app {
requires com.miempresa.utilidades;
}
// Principal.java
package com.miempresa.app;
import com.miempresa.utilidades.Calculadora;
public class Principal {
public static void main(String[] args) {
Calculadora calc = new Calculadora();
System.out.println("Suma: " + calc.sumar(10, 5));
System.out.println("Resta: " + calc.restar(10, 5));
System.out.println("Multiplicación: " + calc.multiplicar(10, 5));
System.out.println("División: " + calc.dividir(10, 5));
try {
calc.dividir(10, 0);
} catch (ArithmeticException e) {
System.out.println("Error controlado: " + e.getMessage());
}
}
}
Compilación y ejecución
Para compilar una aplicación modular:
-
Compilar el módulo de utilidades:
javac -d mods/com.miempresa.utilidades src/com.miempresa.utilidades/module-info.java src/com.miempresa.utilidades/com/miempresa/utilidades/Calculadora.java
-
Compilar el módulo principal:
javac --module-path mods -d mods/com.miempresa.app src/com.miempresa.app/module-info.java src/com.miempresa.app/com/miempresa/app/Principal.java
-
Ejecutar la aplicación:
java --module-path mods -m com.miempresa.app/com.miempresa.app.Principal
Módulos en la JDK de Java
La JDK de Java 9 y versiones posteriores ha sido modularizada. En lugar de un monolítico rt.jar
, ahora tenemos múltiples módulos en el directorio jmods
. Algunos de los módulos principales son:
java.base
: Módulo fundamental que contiene clases comoObject
,String
, colecciones, etc.java.sql
: API de acceso a bases de datosjava.desktop
: Bibliotecas AWT, Swing y JavaFXjava.xml
: Clases para procesamiento XML
Podemos ver todos los módulos disponibles con:
java --list-modules
Tipos de módulos
En Java se pueden distinguir varios tipos de módulos:
- Módulos con nombre: Los que hemos visto, definidos con
module-info.java
. - Módulos automáticos: JARs tradicionales en el module-path. Java les asigna un nombre automáticamente.
- Módulo unnamed (sin nombre): JARs tradicionales en el classpath.
Compatibilidad con código no modular
Java proporciona varios mecanismos para integrar código no modular:
- Módulos automáticos: Poniendo JARs tradicionales en el module-path.
- Classpath tradicional: Sigue funcionando como siempre.
- Reflective access: Con
--add-opens
,--add-exports
para permitir acceso en tiempo de ejecución.
Por ejemplo, para permitir acceso a un paquete interno:
java --add-exports java.base/sun.security.x509=ALL-UNNAMED --module-path ... -m ...
Beneficios del sistema de módulos
El sistema de módulos aporta numerosas ventajas:
- Encapsulación fuerte: Solo los paquetes explícitamente exportados son accesibles.
- Declaración explícita de dependencias: Mejora la claridad y reduce errores.
- Verificación temprana: Errores de dependencias detectados en tiempo de compilación.
- Rendimiento mejorado: La JVM puede optimizar mejor con información de módulos.
- Footprint reducido: Posibilidad de crear runtimes personalizados con
jlink
. - Seguridad mejorada: Menor superficie de ataque al controlar el acceso a las API internas.
Herramienta jlink: Creación de runtimes personalizados
Una herramienta muy útil que viene con el sistema de módulos es jlink
, que permite crear runtimes personalizados que incluyen solo los módulos necesarios para tu aplicación:
jlink --module-path mods:$JAVA_HOME/jmods --add-modules com.miempresa.app --output miruntime
Esto crea un directorio miruntime
con un runtime personalizado que incluye solo los módulos necesarios. Para ejecutar la aplicación:
miruntime/bin/java -m com.miempresa.app/com.miempresa.app.Principal
Un runtime personalizado puede ser significativamente más pequeño que una JRE completa, lo que lo hace ideal para dispositivos con recursos limitados o para distribución de aplicaciones.
Buenas prácticas para trabajar con módulos
- Diseña módulos con alta cohesión y bajo acoplamiento: Cada módulo debe tener una responsabilidad clara.
- Exporta solo lo necesario: Sigue el principio del mínimo privilegio.
- Utiliza la directiva
requires transitive
con cuidado: Puede crear dependencias innecesarias. - Evita ciclos entre módulos: Crean dependencias difíciles de mantener.
- Utiliza servicios para implementar patrones plugin: Mediante
provides
yuses
. - Divide los módulos según tu dominio: Refleja la arquitectura de tu aplicación.
- Considera la migración gradual: Puedes comenzar modularizando partes específicas de tu aplicación.
Resumen
El sistema de módulos de Java representa un cambio significativo en la plataforma Java, ofreciendo mejor encapsulación, dependencias explícitas y un control más granular sobre la visibilidad de los paquetes. Aunque requiere un cambio en la forma de pensar y diseñar aplicaciones, los beneficios en términos de mantenibilidad, rendimiento y seguridad son considerables.
Los módulos son especialmente útiles para aplicaciones grandes y complejas, donde la claridad en las dependencias y la encapsulación son cruciales. Si estás desarrollando nuevas aplicaciones con Java 9 o versiones posteriores, considerar una arquitectura modular desde el principio puede ahorrarte muchos problemas en el futuro y mejorar significativamente la calidad de tu código.