Ir al contenido principal

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:

  1. 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
    
  2. 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
    
  3. 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 como Object, String, colecciones, etc.
  • java.sql: API de acceso a bases de datos
  • java.desktop: Bibliotecas AWT, Swing y JavaFX
  • java.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:

  1. Módulos con nombre: Los que hemos visto, definidos con module-info.java.
  2. Módulos automáticos: JARs tradicionales en el module-path. Java les asigna un nombre automáticamente.
  3. 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:

  1. Módulos automáticos: Poniendo JARs tradicionales en el module-path.
  2. Classpath tradicional: Sigue funcionando como siempre.
  3. 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:

  1. Encapsulación fuerte: Solo los paquetes explícitamente exportados son accesibles.
  2. Declaración explícita de dependencias: Mejora la claridad y reduce errores.
  3. Verificación temprana: Errores de dependencias detectados en tiempo de compilación.
  4. Rendimiento mejorado: La JVM puede optimizar mejor con información de módulos.
  5. Footprint reducido: Posibilidad de crear runtimes personalizados con jlink.
  6. 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:

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

  1. Diseña módulos con alta cohesión y bajo acoplamiento: Cada módulo debe tener una responsabilidad clara.
  2. Exporta solo lo necesario: Sigue el principio del mínimo privilegio.
  3. Utiliza la directiva requires transitive con cuidado: Puede crear dependencias innecesarias.
  4. Evita ciclos entre módulos: Crean dependencias difíciles de mantener.
  5. Utiliza servicios para implementar patrones plugin: Mediante provides y uses.
  6. Divide los módulos según tu dominio: Refleja la arquitectura de tu aplicación.
  7. 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.