Migración a módulos de Java
Estrategias para migrar aplicaciones Java basadas en classpath al sistema de módulos de la plataforma Java.
No es necesario modularizar para migrar a Java 9+. Una aplicación en classpath se ejecuta sin cambios — todo se convierte en un gran módulo sin nombre. La migración es un paso opcional que se da cuando se quiere encapsulación fuerte, un runtime personalizado con jlink, o un grafo de dependencias más limpio. El arte está en hacerlo de forma incremental, porque los tres tipos de módulos — nombrados, automáticos y sin nombre — permiten que el código modular y el no modular coexistan.
Este capítulo cubre cuándo vale la pena migrar, cómo hacer que una aplicación existente funcione primero en un JDK moderno, las dos direcciones en que se puede modularizar (bottom-up vs. top-down), las herramientas que hacen el trabajo pesado, y un ejemplo ejecutable que inspecciona el grafo de módulos tal como lo hace jdeps. Si los términos aquí resultan desconocidos, comienza con la introducción a los módulos y la declaración de un módulo.
Primero: simplemente ejecutar en Java 9+ (sin módulos)
Antes de agregar cualquier module-info.java, recompila y ejecuta tu aplicación existente en el nuevo JDK con todo en el classpath. Los fallos probables no tienen nada que ver con tus módulos sino con el JDK ahora modular:
- Módulos Java EE eliminados —
java.xml.bind(JAXB),java.activation, CORBA, etc. fueron eliminados del JDK. Agrégalos de vuelta como dependencias ordinarias. - Internos encapsulados —
sun.misc.Unsafey similares ya no son accesibles. Las opciones--add-exports/--add-opensson una salida de emergencia mientras eliminas el uso. - Paquetes divididos — dos JARs que aportan el mismo paquete ahora entran en conflicto en el module path (pero no en el classpath).
Primero haz que la aplicación funcione en el classpath. Solo entonces comienza a modularizar.
Migración bottom-up
La estrategia preferida cuando controlas toda la base de código:
- Comienza con las librerías hoja — módulos que no dependen de nada tuyo.
- Añade a cada una un
module-info.javay muévela al module path. - Sus dependientes ya pueden hacer
requiresde ellas. Trabaja hacia arriba en el árbol de dependencias hacia el punto de entrada de la aplicación.
Esto funciona porque un módulo nombrado puede requerir otros módulos nombrados y módulos automáticos — así, siempre que las dependencias propias de una hoja sean al menos automáticas, puedes modularizarla. Cada paso mantiene la compilación en verde. Si una hoja expone comportamiento conectable, este es también un punto natural para introducir un límite de servicios para que sus dependientes hagan uses de una interfaz en lugar de una clase concreta.
Migración top-down
Cuando no controlas las librerías (JARs de terceros sin descriptores), procede en el otro sentido:
- Modulariza primero tu propio código de nivel superior.
- Coloca los JARs de terceros aún no modulares en el module path, donde se convierten en módulos automáticos.
- Haz
requiresde ellos por su nombre automático (a partir del nombre del archivo JAR o deAutomatic-Module-Name). - A medida que cada librería incluya un
module-inforeal, sustituye la dependencia automática por la nombrada — sin cambios en turequires.
Los módulos automáticos son el andamiaje que hace posible el enfoque top-down; permiten que tu código nombrado dependa de JARs que aún no son modulares.
Herramientas y aspectos a tener en cuenta
jdepsanaliza las dependencias reales de un JAR e incluso puede generar un primer borrador demodule-info.java(jdeps --generate-module-info). Empieza por ahí en lugar de escribir directivas a mano.Automatic-Module-Name— si publicas una librería, añade esta entrada al manifiesto antes de escribir un descriptor completo. Fija un nombre de módulo estable para que los usuarios de módulos automáticos que dependen de ella no se vean afectados cuando luego renombres el JAR.- Los paquetes divididos deben fusionarse. El module path prohíbe que dos módulos posean el mismo paquete; el classpath lo toleraba. Este es el bloqueador de migración más común.
openspara la reflexión. Los frameworks que usan reflexión sobre tus clases (Jackson, JPA, Spring) necesitanopens, o bien obtendrásInaccessibleObjectExceptionen tiempo de ejecución aunque la compilación haya pasado.
Los paquetes divididos son el fallo que más sorprende a la gente. En el classpath, dos JARs con clases en com.example.util simplemente se fusionaban; en el module path el resolver los rechaza con un error "module reads package ... from both". No existe ningún flag que lo permita — debes fusionar el paquete duplicado en un único módulo (o renombrar uno). Audita en busca de paquetes divididos con jdeps antes de comenzar a escribir descriptores.
Un ejemplo práctico: inspeccionar un grafo de dependencias como jdeps
La planificación de la migración comienza con "¿qué depende de qué?" Este programa lee los descriptores de módulos de la capa de arranque en tiempo de ejecución — la misma información de requires que reporta jdeps — y también detecta si él mismo se ejecuta como módulo nombrado o en el classpath, exactamente la verificación que indica cuánto ha avanzado la migración.
Qué extraer de la ejecución:
- El programa reportó que se estaba ejecutando como módulo UNNAMED — exactamente lo que se espera de código en classpath, y la señal en tiempo de ejecución de que esta base de código no ha sido modularizada aún. Volver a ejecutarlo después de añadir un
module-infoy moverse al module path cambiaría esto a NAMED, dándote una verificación concreta de "¿ya llegamos?" durante la migración. inspect("java.sql")imprimió su lista real derequires, incluyendo una dependencia transitiva estilojava.transaction.xaojava.logging. Esta es la misma información que exponejdeps— conocer las dependencias reales de un módulo es el primer paso para escribir sumodule-info.java, y el descriptor ya las contiene.- Algunos requires imprimieron
(transitive). Esas son las dependencias que un módulo re-exporta; cuando hacesrequires java.sql, automáticamente lees también sus dependencias transitivas. Identificarlas te dice qué líneasrequires transitivecopiar cuando modularices código que envuelve dicho módulo. findModuledevolvió unOptional, reforzando que un módulo puede simplemente no estar presente en un runtime determinado — una imagen recortada conjlinkpodría omitirjava.desktoppor completo. Los planes de migración deben tener en cuenta qué módulos realmente se incluyen en el runtime destino.- Cada módulo depende en última instancia de
java.base, y este nunca aparece en una lista derequiresporque es implícito. El recuento total de módulos de arranque muestra que el JDK es un grafo que se puede consultar programáticamente — la base sobre la que herramientas comojdepsyjlinkconstruyen para analizar y reducir una aplicación.
Un orden de migración sensato, resumido
- Ejecuta en el classpath en el nuevo JDK; corrige los fallos por módulos eliminados y APIs internas.
- Usa
jdepspara mapear dependencias y encontrar paquetes divididos. - Añade
Automatic-Module-Namea las librerías que publiques. - Modulariza bottom-up si eres dueño del árbol, top-down (apoyándote en módulos automáticos) si no lo eres.
- Añade
opensdonde los frameworks usen reflexión; verifica en tiempo de ejecución, no solo en tiempo de compilación.
Esto completa la parte del sistema de módulos de la plataforma Java: ahora conoces qué son los módulos, cómo declarar uno, los tres tipos y cómo coexisten, cómo los servicios desacoplan módulos, y cómo migrar una aplicación existente al module path sin reescribirla.