W3docs

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 rimossijava.xml.bind (JAXB), java.activation, CORBA, ecc. sono stati rimossi dal JDK. Aggiungili nuovamente come dipendenze ordinarie.
  • Interni incapsulatisun.misc.Unsafe e simili non sono più accessibili. I flag --add-exports / --add-opens sono 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:

  1. Inizia con le librerie foglia — moduli che non dipendono da nulla di tuo.
  2. Assegna a ciascuna un module-info.java e spostala nel module path.
  3. 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:

  1. Modularizza prima il tuo codice di alto livello.
  2. Posiziona i JAR di terze parti ancora non modulari nel module path, dove diventano moduli automatic.
  3. Dichiarali con requires usando il loro nome automatico (dal nome del file JAR o da Automatic-Module-Name).
  4. Quando ogni libreria pubblica un vero module-info, sostituisci la dipendenza automatica con quella named — senza modificare i tuoi requires.

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

  • jdeps analizza le dipendenze effettive di un JAR e può persino generare una prima bozza di module-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.
  • opens per la reflection. I framework che riflettono sulle tue classi (Jackson, JPA, Spring) hanno bisogno di opens, altrimenti otterrai InaccessibleObjectException a runtime anche se la compilazione è passata.
Attenzione

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.

java— editable, runs on the server

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-info e 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 di requires, inclusa una dipendenza transitiva come java.transaction.xa o java.logging. Questa è la stessa informazione che espone jdeps — conoscere le dipendenze reali di un modulo è il primo passo per scrivere il suo module-info.java, e il descrittore le contiene già.
  • Alcuni requires hanno stampato (transitive). Queste sono le dipendenze che un modulo ri-esporta; quando si dichiara requires java.sql, si leggono automaticamente anche le sue dipendenze transitive. Identificarle indica quali righe requires transitive copiare quando si modularizza codice che racchiude tale modulo.
  • findModule ha restituito un Optional, ribadendo che un modulo potrebbe semplicemente non essere presente in un dato runtime — un'immagine ridotta con jlink potrebbe omettere completamente java.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 lista requires perché è implicito. Il conteggio totale dei moduli di boot mostra che il JDK è un grafo interrogabile programmaticamente — la base su cui strumenti come jdeps e jlink costruiscono per analizzare e ridurre un'applicazione.

Un ordine di migrazione ragionevole, riassunto

  1. Esegui sul classpath sul nuovo JDK; correggi gli errori dovuti a moduli rimossi e API interne.
  2. Usa jdeps per mappare le dipendenze e trovare i pacchetti divisi.
  3. Aggiungi Automatic-Module-Name a qualsiasi libreria che pubblichi.
  4. Modularizza bottom-up se possiedi l'albero, top-down (facendo affidamento sui moduli automatic) se non lo possiedi.
  5. Aggiungi opens dove 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.

Esercitazione

Pratica
Il tuo team possiede l'intero albero dei sorgenti di un'applicazione e vuole un incapsulamento forte ovunque. La libreria A non dipende da nulla di tuo; la libreria B dipende da A; l'eseguibile dipende da B. Quale ordine di migrazione mantiene ogni build intermedia funzionante con il minor numero di workaround con moduli automatic?
Il tuo team possiede l'intero albero dei sorgenti di un'applicazione e vuole un incapsulamento forte ovunque. La libreria A non dipende da nulla di tuo; la libreria B dipende da A; l'eseguibile dipende da B. Quale ordine di migrazione mantiene ogni build intermedia funzionante con il minor numero di workaround con moduli automatic?
Was this page helpful?