Migrazione ai Moduli Java
Strategie per migrare applicazioni Java basate sul classpath al Java Platform Module System.
Non è necessario modularizzare per passare a Java 9+. Un'applicazione su classpath funziona senza modifiche — tutto diventa un unico grande modulo senza nome. La migrazione è un passo opzionale che si intraprende quando si desidera un'incapsulamento forte, un runtime personalizzato con jlink, o un grafo delle dipendenze più pulito. L'arte sta nel farlo in modo incrementale, perché i tre tipi di modulo — named, automatic e unnamed — permettono la coesistenza di codice modulare e non modulare.
Questo capitolo tratta quando vale la pena migrare, come far funzionare un'applicazione esistente su un JDK moderno prima di tutto, le due direzioni di modularizzazione (bottom-up vs. top-down), gli strumenti che svolgono il lavoro pesante e un esempio eseguibile che ispeziona il grafo dei moduli come fa jdeps. Se i termini qui sono sconosciuti, inizia con l'introduzione ai moduli e la dichiarazione di un modulo.
Prima: eseguire su Java 9+ (senza moduli)
Prima di aggiungere qualsiasi module-info.java, ricompila ed esegui l'applicazione esistente sul nuovo JDK con tutto sul classpath. I possibili errori non hanno nulla a che fare con i tuoi moduli e tutto con il JDK ora modularizzato:
- Moduli Java EE rimossi —
java.xml.bind(JAXB),java.activation, CORBA, ecc. sono stati rimossi dal JDK. Aggiungili nuovamente come dipendenze ordinarie. - Interni incapsulati —
sun.misc.Unsafee simili non sono più accessibili. I flag--add-exports/--add-openssono una via di fuga mentre si rimuove l'utilizzo. - Pacchetti divisi — due JAR che contribuiscono allo stesso pacchetto ora entrano in conflitto sul module path (ma non sul classpath).
Assicurati che l'applicazione funzioni correttamente sul classpath. Solo allora inizia a modularizzare.
Migrazione bottom-up
La strategia preferita quando si controlla l'intero codebase:
- Inizia con le librerie foglia — moduli che non dipendono da nulla di tuo.
- Assegna a ciascuna un
module-info.javae spostala nel module path. - I loro dipendenti possono ora dichiararle con
requires. Lavora risalendo l'albero delle dipendenze verso il punto di ingresso dell'applicazione.
Questo funziona perché un modulo named può richiedere altri moduli named e moduli automatic — quindi finché le dipendenze di una foglia sono almeno automatic, puoi modularizzarla. Ogni passo mantiene la build operativa. Se una foglia espone comportamenti componibili, questo è anche il momento naturale per introdurre un confine di servizi in modo che i dipendenti usino un'interfaccia piuttosto che una classe concreta.
Migrazione top-down
Quando non si controllano le librerie (JAR di terze parti senza descrittori), si procede al contrario:
- Modularizza prima il tuo codice di alto livello.
- Posiziona i JAR di terze parti ancora non modulari nel module path, dove diventano moduli automatic.
- Dichiarali con
requiresusando il loro nome automatico (dal nome del file JAR o daAutomatic-Module-Name). - Quando ogni libreria pubblica un vero
module-info, sostituisci la dipendenza automatica con quella named — senza modificare i tuoirequires.
I moduli automatic sono l'impalcatura che rende possibile il top-down; permettono al tuo codice named di dipendere da JAR che non sono ancora modulari.
Strumenti e insidie
jdepsanalizza le dipendenze effettive di un JAR e può persino generare una prima bozza dimodule-info.java(jdeps --generate-module-info). Inizia da lì piuttosto che scrivere le direttive a mano.Automatic-Module-Name— se pubblichi una libreria, aggiungi questa voce nel manifest prima di scrivere un descrittore completo. Fissa un nome di modulo stabile in modo che gli utenti downstream di moduli automatic non vengano interrotti quando rinomini il JAR in seguito.- I pacchetti divisi devono essere unificati. Il module path vieta a due moduli di possedere lo stesso pacchetto; il classpath lo tollerava. Questo è il blocco alla migrazione più comune.
opensper la reflection. I framework che riflettono sulle tue classi (Jackson, JPA, Spring) hanno bisogno diopens, altrimenti otterraiInaccessibleObjectExceptiona runtime anche se la compilazione è passata.
I pacchetti divisi sono il problema che sorprende di più. Sul classpath, due JAR contenenti classi in com.example.util si fondevano semplicemente; sul module path il resolver le rifiuta con un errore "module reads package ... from both". Non esiste alcun flag che renda ciò legale — devi unire il pacchetto duplicato in un singolo modulo (o rinominarne uno). Controlla i pacchetti divisi con jdeps prima di iniziare a scrivere i descrittori.
Un esempio pratico: ispezionare un grafo delle dipendenze come jdeps
La pianificazione della migrazione inizia con "chi dipende da chi." Questo programma legge i descrittori di modulo del boot layer a runtime — le stesse informazioni requires che riporta jdeps — e rileva anche se esso stesso è in esecuzione come modulo named o sul classpath, il controllo che indica quanto sia avanzata una migrazione.
Cosa ricavare dall'esecuzione:
- Il programma ha riportato che era in esecuzione come modulo UNNAMED — esattamente quello che ci si aspetta dal codice su classpath, e il segnale a runtime che questo codebase non è stato ancora modularizzato. Rieseguendo dopo aver aggiunto un
module-infoe spostato nel module path, questo diventerebbe NAMED, fornendo un concreto controllo "siamo arrivati?" durante la migrazione. inspect("java.sql")ha stampato la sua lista reale direquires, inclusa una dipendenza transitiva comejava.transaction.xaojava.logging. Questa è la stessa informazione che esponejdeps— conoscere le dipendenze reali di un modulo è il primo passo per scrivere il suomodule-info.java, e il descrittore le contiene già.- Alcuni requires hanno stampato
(transitive). Queste sono le dipendenze che un modulo ri-esporta; quando si dichiararequires java.sql, si leggono automaticamente anche le sue dipendenze transitive. Identificarle indica quali righerequires transitivecopiare quando si modularizza codice che racchiude tale modulo. findModuleha restituito unOptional, ribadendo che un modulo potrebbe semplicemente non essere presente in un dato runtime — un'immagine ridotta conjlinkpotrebbe omettere completamentejava.desktop. I piani di migrazione devono tenere conto di quali moduli sono effettivamente disponibili nel runtime di destinazione.- Ogni modulo dipende in ultima analisi da
java.base, che non appare mai in una listarequiresperché è implicito. Il conteggio totale dei moduli di boot mostra che il JDK è un grafo interrogabile programmaticamente — la base su cui strumenti comejdepsejlinkcostruiscono per analizzare e ridurre un'applicazione.
Un ordine di migrazione ragionevole, riassunto
- Esegui sul classpath sul nuovo JDK; correggi gli errori dovuti a moduli rimossi e API interne.
- Usa
jdepsper mappare le dipendenze e trovare i pacchetti divisi. - Aggiungi
Automatic-Module-Namea qualsiasi libreria che pubblichi. - Modularizza bottom-up se possiedi l'albero, top-down (facendo affidamento sui moduli automatic) se non lo possiedi.
- Aggiungi
opensdove i framework usano la reflection; verifica a runtime, non solo in fase di compilazione.
Questo completa la parte del Java Platform Module System: ora sai cosa sono i moduli, come dichiararne uno, i tre tipi e come coesistono, come i servizi disaccoppiano i moduli e come migrare un'applicazione esistente nel module path senza riscriverla.