38. Compilare, Impacchettare ed Eseguire Moduli
Indice
- 38.1 Il Module Path vs il Classpath
- 38.2 Opzioni della Riga di Comando Relative ai Moduli
- 38.3 Compilare un Singolo Modulo
- 38.4 Compilare Moduli Multipli Interdipendenti
- 38.5 Impacchettare un Modulo in un JAR Modulare
- 38.6 Eseguire un’Applicazione Modulare
- 38.7 Spiegazione delle Direttive del Modulo
- 38.8 Moduli Named, Automatici e Unnamed
- 38.9 Approccio Top-Down e Bottom-Up per modularizzare un’applicazione
- 38.10 Ispezionare Moduli e Dipendenze
- 38.11 Creare Immagini Runtime Personalizzate con jlink
- 38.12 Creare Applicazioni Self-Contained con jpackage
- 38.13 Riepilogo Finale JPMS in Pratica
Una volta che un modulo è definito con un file module-info.java, deve essere compilato, impacchettato ed eseguito utilizzando strumenti consapevoli dei moduli.
Questa sezione spiega come cambia la toolchain Java quando sono coinvolti i moduli.
38.1 Il Module Path vs il Classpath
JPMS introduce un nuovo concetto: il module path.
Esiste accanto al tradizionale classpath, ma i due si comportano in modo molto diverso.
| Aspetto | Classpath | Module path |
|---|---|---|
| Struttura | Lista piatta di JAR | Moduli con identità |
| Incapsulamento | Nessuno | Forte |
| Verifica delle dipendenze | Nessuna | Rigorosa |
| Split packages | Consentiti | Vietati (moduli nominati) |
| Ordine di risoluzione | Dipendente dall’ordine | Deterministico |
Note
- Un JAR posizionato sul
module pathè trattato come unmodule:- Se contiene un
module-info.class, diventa unnamed module. - Se non contiene un descrittore di modulo, diventa un
automatic module.
- Se contiene un
- Un JAR posizionato sul classpath non è trattato come un modulo.
- Invece, diventa parte dell unnamed module, insieme a tutte le altre entry del classpath.
- Un JAR modulare (cioè un JAR che contiene module-info.class) può comunque essere utilizzato come un JAR regolare.
- Se è posizionato sul classpath invece che sul module path, è trattato come parte dell unnamed module, permettendo alle applicazioni non modulari di utilizzarlo senza adottare il module system.
- Split packages:
- Sono consentiti sul classpath (più JAR possono contenere classi nello stesso package).
- Sono vietati per i named modules o automatic modules sul module path.
38.2 Opzioni della Riga di Comando Relative ai Moduli
Quando si lavora con il Java Module System, sia java che javac forniscono opzioni specifiche per compilare ed eseguire applicazioni modulari.
Alcune opzioni sono condivise, mentre altre sono specifiche di uno strumento.
38.2.1 Opzioni Disponibili sia in java che in javac
Queste opzioni possono essere utilizzate sia durante la compilazione sia durante l’esecuzione:
-
--moduleo-m
Utilizzata per compilare o eseguire solo il modulo specificato. -
--module-patho-p
Specifica i percorsi nei qualijavaojavaccercheranno le definizioni dei moduli.
Il sistema dei moduli Java mette a disposizione tre opzioni speciali da linea di comando, utilizzabili sia con javac sia con java, che consentono di modificare temporaneamente le regole di accesso tra moduli senza alterare i file module-info.java. Queste opzioni hanno effetto solo per quella specifica esecuzione del comando e non modificano in modo permanente i descrittori dei moduli.
Le tre opzioni sono:
--add-reads--add-exports--add-opens
Sono generalmente utilizzate per test, retrocompatibilità, migrazione di applicazioni esistenti oppure quando si lavora con moduli di terze parti che non possono essere modificati.
Supponiamo, ad esempio, che moduleA debba accedere ai tipi pubblici di moduleB, ma che:
moduleAnon dichiarirequires moduleB;moduleBnon esporti il package richiesto versomoduleA
Invece di modificare i file module-info.java, è possibile concedere temporaneamente l’accesso necessario con:
javac --add-reads moduleA=moduleB \
--add-exports moduleB/com.modB.package1=moduleA \
...
java --add-reads moduleA=moduleB \
--add-exports moduleB/com.modB.package1=moduleA \
...
Significato delle opzioni:
-
--add-reads moduleA=moduleB
Dichiara temporaneamente chemoduleAleggemoduleB.
È equivalente ad aggiungererequires moduleB;nel descrittore dimoduleA.
In questo modo,moduleApuò accedere ai package esportati dimoduleB. -
--add-exports moduleB/com.modB.package1=moduleA
Esporta temporaneamente il packagecom.modB.package1dal modulomoduleBversomoduleA.
È equivalente ad aggiungere:
exports com.modB.package1 to moduleA;
nel descrittore dimoduleB.
Distinzione importante:
--add-readsstabilisce la leggibilità a livello di modulo.--add-exportsconcede l’accesso a specifici package.--add-opens(non mostrato sopra) è simile a--add-exports, ma consente anche l’accesso tramite reflection profonda (deep reflection), spesso necessario per alcuni framework.
Queste opzioni non modificano i metadati compilati del modulo; si limitano ad adattare il grafo dei moduli per quella specifica esecuzione di javac o java.
38.2.2 Opzioni Applicabili Solo a javac
Queste opzioni si applicano solo in fase di compilazione:
-
--module-source-path
(nessuna forma abbreviata)
Utilizzata dajavacper individuare le definizioni dei moduli sorgente. -
-d
Specifica la directory di destinazione nella quale verranno generati i file.classdopo la compilazione.
38.2.3 Opzioni Applicabili Solo a java
Queste opzioni si applicano solo in fase di esecuzione:
-
--list-modules
(nessuna forma abbreviata)
Elenca tutti i moduli osservabili e quindi termina. -
--show-module-resolution
(nessuna forma abbreviata)
Mostra i dettagli della risoluzione dei moduli durante l’avvio dell’applicazione. -
--describe-moduleo-d
Descrive un modulo specificato e quindi termina.
38.2.4 Distinzioni Importanti
L’opzione -d ha significati diversi a seconda dello strumento:
- In
javac,-ddefinisce la directory di destinazione per i file di classe compilati. - In
java,-dè una forma abbreviata di--describe-module.
Inoltre, -d non deve essere confusa con -D (D maiuscola).
-Dviene utilizzata durante l’esecuzione di un programma Java per definire proprietà di sistema come coppie nome-valore nella riga di comando.
java -Dconfig.file=app.properties com.example.Main
In questo esempio, -Dconfig.file=app.properties imposta una proprietà di sistema che può essere letta a runtime tramite System.getProperty("config.file").
38.3 Compilare un Singolo Modulo
Per compilare un modulo, devi specificare il percorso dei sorgenti del modulo e la directory di destinazione.
javac -d out \
src/com.example.hello/module-info.java \
src/com.example.hello/com/example/hello/Main.java
Un approccio più scalabile utilizza --module-source-path.
javac --module-source-path src \
-d out \
$(find src -name "*.java")
Note
--module-source-path indica a javac dove trovare più moduli contemporaneamente.
38.4 Compilare Moduli Multipli Interdipendenti
Quando i moduli dipendono l’uno dall’altro, le loro dipendenze devono essere risolvibili in fase di compilazione.
--module-path mods (directory di esempio contenente moduli interdipendenti) dovrebbe contenere JAR modulari già compilati o directory di moduli compilati (ognuna con il proprio module-info.class).
javac -d out \
--module-source-path src \
--module-path mods \
$(find src -name "*.java")
Qui:
--module-source-pathindividua gli alberi dei sorgenti dei moduli--module-pathfornisce moduli già compilati
38.5 Impacchettare un Modulo in un JAR Modulare
Dopo la compilazione, i moduli sono tipicamente impacchettati come file JAR.
Un JAR modulare contiene un module-info.class alla sua root.
Se module-info.class è presente, il JAR diventa automaticamente un modulo nominato e il suo nome è preso dal descrittore (non dal nome del file).
jar --create \
--file mods/com.example.hello.jar \
--main-class com.example.hello.Main \
-C out/com.example.hello .
Note
Un JAR con module-info.class è un modulo nominato, non un modulo automatico.
Quando un JAR contiene un module-info.class, il suo nome di modulo è preso da quel file e non è dedotto dal nome del file.
38.6 Eseguire un’Applicazione Modulare
Per eseguire un’applicazione modulare, si utilizza il module path e si specifica il nome del modulo.
java --module-path mods \
--module com.example.hello/com.example.hello.Main
Puoi abbreviare usando le opzioni -p e -m.
java -p mods -m com.example.hello/com.example.hello.Main
Note
Quando si usano moduli nominati, il classpath è ignorato per la risoluzione delle dipendenze tra moduli.
38.7 Spiegazione delle Direttive del Modulo
Il file module-info.java contiene direttive che descrivono dipendenze, visibilità e servizi.
Ogni direttiva ha un significato preciso.
38.7.1 requires
La direttiva requires dichiara una dipendenza da un altro modulo.
Senza di essa, i tipi del modulo dipendente non possono essere utilizzati.
module com.example.app {
requires com.example.lib;
}
Effetti di requires:
- La dipendenza deve essere presente a compile-time e a runtime
- I package esportati del modulo richiesto diventano accessibili
38.7.2 requires transitive
requires transitive espone una dipendenza ai moduli a valle.
Propaga la leggibilità.
module com.example.lib {
requires transitive com.example.util;
exports com.example.lib.api;
}
Significato:
- Qualsiasi modulo che richiede com.example.lib legge automaticamente com.example.util
- I chiamanti non devono dichiarare requires com.example.util esplicitamente
Note
Questo è simile alle “dipendenze pubbliche” in altri sistemi di moduli.
Leggibile ≠ esportato: un requisito transitivo non esporta automaticamente i tuoi package.
38.7.3 exports
exports rende un package accessibile ad altri moduli.
Solo i package esportati sono visibili all’esterno del modulo.
module com.example.lib {
exports com.example.lib.api;
}
I package non esportati rimangono fortemente incapsulati.
38.7.4 exports ... to (Export Qualificati)
Un export qualificato limita l’accesso a moduli specifici.
module com.example.lib {
exports com.example.internal to com.example.friend;
}
Solo i moduli elencati possono accedere al package esportato.
38.7.5 opens
opens consente un accesso riflessivo profondo a un package.
È usato principalmente da framework che utilizzano reflection.
module com.example.app {
opens com.example.app.model;
}
Note
opens NON rende un package accessibile a compile-time. Influenza solo la reflection a runtime.
38.7.6 opens ... to (Opens Qualificati)
Puoi limitare l’accesso riflessivo a moduli specifici.
module com.example.app {
opens com.example.app.model to com.fasterxml.jackson.databind;
}
Note
opens influenza la reflection; exports influenza la compilazione e la visibilità dei tipi.
38.7.7 Tabella delle Direttive Principali
| Direttiva | Scopo |
|---|---|
requires |
Dichiarare una dipendenza |
requires transitive |
Propagare una dipendenza |
exports |
Esporre un package |
exports ... to |
Esporre a moduli specifici |
opens |
Consentire reflection a runtime |
opens ... to |
Limitare l’accesso riflessivo |
38.7.8 Exports vs Opens — Accesso a Compile-Time vs Runtime
| Visibilità | Compile-time? | Reflection a runtime? |
|---|---|---|
exports |
Sì | No |
opens |
No | Sì |
exports ... to |
Sì (moduli limitati) | No |
opens ... to |
No | Sì (moduli limitati) |
Important
JPMS aggiunge un module path, ma il classpath esiste ancora. Possono coesistere, ma i moduli nominati hanno la precedenza.
38.8 Moduli Named, Automatici e Unnamed
JPMS supporta differenti tipi di moduli per permettere una migrazione graduale dal classpath.
JPMS deve interoperare con codice legacy.
Per supportare l’adozione graduale, la JVM riconosce tre differenti categorie di moduli.
38.8.1 Moduli Named
Un modulo named possiede un module-info.class e una identità stabile.
- Incapsulamento forte
- Dipendenze esplicite
- Supporto completo JPMS
38.8.2 Moduli Automatici
Un JAR senza module-info posizionato nel module path diventa un modulo automatico.
Il suo nome è derivato dal nome del file JAR.
- Legge tutti gli altri moduli
- Esporta tutti i package
- Nessun incapsulamento forte
Note
I moduli automatici esistono per facilitare la migrazione. Non sono adatti come design a lungo termine.
38.8.3 Modulo Unnamed
Il codice nel classpath appartiene al modulo unnamed.
- Legge tutti i moduli named
- Tutti i package sono aperti
- Non può essere richiesto da moduli named
Note
Il modulo unnamed preserva il comportamento legacy del classpath.
38.8.4 Riepilogo Comparativo
| Tipo di modulo | module-info presente? | Incapsulamento | Legge |
|---|---|---|---|
Named |
Sì | Forte | Solo dichiarati |
Automatic |
No | Debole | Tutti i moduli |
Unnamed |
No | Nessuno | Tutti i moduli |
38.9 Approccio Top-Down e Bottom-Up per modularizzare un’applicazione
Quando si migra un’applicazione esistente (non modulare) verso il Java Platform Module System (JPMS), è possibile adottare due strategie principali: top-down e bottom-up.
Entrambi gli approcci richiedono una chiara comprensione delle interazioni tra moduli nominati, moduli automatici e modulo non nominato.
38.9.1 Approccio Top-Down
In un approccio top-down, si inizia modularizzando il modulo principale dell’applicazione, per poi migrare progressivamente le sue dipendenze.
38.9.1.1 Regole fondamentali
- Un JAR posizionato sul module path diventa un modulo automatico.
- Il suo nome è determinato:
- Dall’eventuale voce
Automatic-Module-Namepresente nel manifest, oppure - Derivato dal nome del file JAR (i trattini vengono sostituiti con punti e la parte relativa alla versione viene ignorata).
Esempio:
mysql-connector-java-8.0.11.jar→mysql.connector.java
- Dall’eventuale voce
-
Un
modulo automatico:- Esporta tutti i suoi package.
- Legge tutti gli altri moduli.
-
Un JAR posizionato sul classpath appartiene al modulo non nominato.
- Il
modulo non nominato:- Esporta tutti i suoi package.
- Può leggere tutti gli altri moduli.
-
Tuttavia, non avendo un nome, nessun modulo può dichiarare una clausola
requiresnei suoi confronti. -
I moduli esplicitamente nominati (con file
module-info.java) - Possono dichiarare dipendenze tramite:
requires some.module; - Possono dipendere da:
- Altri moduli nominati
- Moduli automatici
- Non possono dipendere dal modulo non nominato (poiché privo di nome).
Conseguenza importante:
Un
modulo nominatopuò leggere unmodulo automatico, ma non può leggere ilmodulo non nominato.
38.9.1.2 Implicazioni pratiche
Supponiamo:
- JAR dell’applicazione =
A Adipende direttamente daBBdipende daC
Se modularizzi A per primo:
Adeve dichiararerequires B;- Di conseguenza,
Bdeve trovarsi sul module path (come modulo nominato o automatico) - Se
Bdiventa un modulo nominato: - Anche
Cdovrà essere spostato sul module path (nominato o automatico)
Quindi, in una migrazione top-down:
- Si parte dal livello dell’applicazione.
- Si modularizzano progressivamente le dipendenze verso l’esterno.
- I moduli automatici sono spesso utilizzati temporaneamente durante la fase di transizione.
38.9.1.3 Riepilogo delle regole di accesso
| Tipo di modulo | Esporta | Può leggere |
|---|---|---|
Modulo nominato |
Solo export dichiarati | Solo moduli richiesti |
Modulo automatico |
Tutti i package | Tutti i moduli |
Modulo non nominato |
Tutti i package | Tutti i moduli |
Important
- I
moduli automatici e non nominatisono permissivi. - I
moduli nominatiimpongono regole esplicite di dipendenza ed export.
38.9.2 Approccio Bottom-Up
In un approccio bottom-up, si inizia modularizzando le librerie di livello più basso, per poi risalire progressivamente verso i moduli di livello superiore, fino all’applicazione principale.
38.9.2.1 Strategia principale
Si convertono inizialmente le librerie fondamentali in moduli nominati correttamente definiti, dotati di un descrittore esplicito module-info.java.
Successivamente:
- Si modularizzano i moduli che dipendono da esse.
- Infine, anche l’applicazione principale diventa un modulo nominato.
Questo approccio enfatizza:
- Relazioni
requiresesplicite exportscontrollati- Una forte incapsulazione fin dall’inizio
38.9.2.2 Vantaggi architetturali
Rispetto ai moduli automatici:
- I moduli nominati esportano solo ciò che è dichiarato esplicitamente.
- Non leggono implicitamente tutti gli altri moduli.
- I confini di incapsulamento sono chiaramente definiti.
La modularizzazione bottom-up porta generalmente a:
- Un grafo delle dipendenze più pulito
- Maggiore manutenibilità
- Confini modulari più solidi
38.9.3 Confronto concettuale
Top-Down
- Si parte dall’applicazione principale.
- Le dipendenze vengono modularizzate secondo necessità.
- Si fa spesso affidamento temporaneo sui moduli automatici.
- Migrazione iniziale più rapida.
Bottom-Up
- Si parte dalle librerie di base.
- I descrittori di modulo vengono definiti in modo rigoroso fin dall’inizio.
- La migrazione procede verso l’alto.
- Produce un’architettura modulare più disciplinata e robusta.
38.9.4 Considerazioni sulla migrazione
Nella pratica, molti progetti reali combinano entrambe le strategie:
- Una migrazione top-down consente di attivare rapidamente l’esecuzione modulare.
- Una fase successiva di raffinamento bottom-up sostituisce i moduli automatici con moduli nominati correttamente definiti.
Questo approccio ibrido consente un’adozione incrementale del JPMS, rafforzando progressivamente l’incapsulamento e la chiarezza architetturale.
38.10 Ispezionare Moduli e Dipendenze
38.10.1 Descrivere Moduli con java
java --describe-module java.sql
Questo mostra exports, requires e services di un modulo.
38.10.2 Descrivere JAR Modulari
jar --describe-module --file mylib.jar
38.10.3 Analizzare le Dipendenze con jdeps
jdeps analizza staticamente le dipendenze di classi e moduli.
jdeps myapp.jar
jdeps --module-path mods --check my.module
Per rilevare l’uso di API interne del JDK:
jdeps --jdk-internals myapp.jar
38.10 Creare Immagini Runtime Personalizzate con jlink
jlink costruisce un runtime Java minimale contenente solo i moduli richiesti da una applicazione.
jlink
--module-path $JAVA_HOME/jmods:mods
--add-modules com.example.app
--output runtime-image
Benefici:
- runtime più piccolo
- avvio più rapido
- nessun modulo JDK inutilizzato
38.11 Creare Applicazioni Self-Contained con jpackage
jpackage costruisce installer specifici per piattaforma o immagini applicative.
jpackage
--name MyApp
--input mods
--main-module com.example.app/com.example.Main
jpackage può produrre:
- .exe / .msi (Windows)
- .pkg / .dmg (macOS)
- .deb / .rpm (Linux)
38.12 Riepilogo Finale JPMS in Pratica
JPMSintroduceincapsulamento fortee dipendenze affidabili- I
modulisostituiscono convenzioni fragili del classpath - I
serviziabilitano architetture disaccoppiate Moduli automaticiemodulo unnamedsupportano la migrazionejlinkejpackageabilitano modelli moderni di deployment