Skip to content

38. Compilare, Impacchettare ed Eseguire Moduli

Indice


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 un module:
    • Se contiene un module-info.class, diventa un named module.
    • Se non contiene un descrittore di modulo, diventa un automatic module.
  • 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:

  • --module o -m
    Utilizzata per compilare o eseguire solo il modulo specificato.

  • --module-path o -p
    Specifica i percorsi nei quali java o javac cercheranno 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:

  • moduleA non dichiari requires moduleB;
  • moduleB non esporti il package richiesto verso moduleA

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 che moduleA legge moduleB.
    È equivalente ad aggiungere requires moduleB; nel descrittore di moduleA.
    In questo modo, moduleA può accedere ai package esportati di moduleB.

  • --add-exports moduleB/com.modB.package1=moduleA
    Esporta temporaneamente il package com.modB.package1 dal modulo moduleB verso moduleA.
    È equivalente ad aggiungere:
    exports com.modB.package1 to moduleA;
    nel descrittore di moduleB.

Distinzione importante:

  • --add-reads stabilisce la leggibilità a livello di modulo.
  • --add-exports concede 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 da javac per individuare le definizioni dei moduli sorgente.

  • -d
    Specifica la directory di destinazione nella quale verranno generati i file .class dopo 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-module o -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, -d definisce 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).

  • -D viene 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-path individua gli alberi dei sorgenti dei moduli
  • --module-path fornisce 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 No
opens No
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 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

  1. Un JAR posizionato sul module path diventa un modulo automatico.
  2. Il suo nome è determinato:
    • Dall’eventuale voce Automatic-Module-Name presente 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.jarmysql.connector.java
  3. Un modulo automatico:

    • Esporta tutti i suoi package.
    • Legge tutti gli altri moduli.
  4. Un JAR posizionato sul classpath appartiene al modulo non nominato.

  5. Il modulo non nominato:
    • Esporta tutti i suoi package.
    • Può leggere tutti gli altri moduli.
  6. Tuttavia, non avendo un nome, nessun modulo può dichiarare una clausola requires nei suoi confronti.

  7. I moduli esplicitamente nominati (con file module-info.java)

  8. Possono dichiarare dipendenze tramite:
    requires some.module;
    
  9. Possono dipendere da:
    • Altri moduli nominati
    • Moduli automatici
  10. Non possono dipendere dal modulo non nominato (poiché privo di nome).

Conseguenza importante:

Un modulo nominato può leggere un modulo automatico, ma non può leggere il modulo non nominato.

38.9.1.2 Implicazioni pratiche

Supponiamo:

  • JAR dell’applicazione = A
  • A dipende direttamente da B
  • B dipende da C

Se modularizzi A per primo:

  • A deve dichiarare requires B;
  • Di conseguenza, B deve trovarsi sul module path (come modulo nominato o automatico)
  • Se B diventa un modulo nominato:
  • Anche C dovrà 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 nominati sono permissivi.
  • I moduli nominati impongono 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 requires esplicite
  • exports controllati
  • 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

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

  • JPMS introduce incapsulamento forte e dipendenze affidabili
  • I moduli sostituiscono convenzioni fragili del classpath
  • I servizi abilitano architetture disaccoppiate
  • Moduli automatici e modulo unnamed supportano la migrazione
  • jlink e jpackage abilitano modelli moderni di deployment