Introduzione
Nel corso del precedente articolo abbiamo descritto il progetto Jigsaw (“puzzle”) ossia la modularizzazione del linguaggio Java e del suo ecosistema [1]. Abbiamo visto come lo scopo primario di tale progetto sia disegnare e realizzare un sistema modulare standard per Java SE da applicare sia al linguaggio, sia al suo ecosistema.
Per tutti gli sviluppatori che conoscono OSGi [2], Jigsaw presenta molte similitudini proprio con questo. Tuttavia, come illustrato nell’articolo precedente, ci sono anche importanti differenze, ad esempio:
- Jigsaw intende modularizzare Java anche dall’interno, ossia a partire dallo stesso Java Runtime Environment (JRE);
- il modello di modularizzazione proposto, giocoforza, deve essere utilizzato dalle applicazioni e librerie
Pertanto il progetto Jigsaw si è occupato di organizzare il monolitico JRE in un insieme razionale di moduli interdipendenti: allo stato attuale sono 63. La tabella JDK Module Summary riassume questi moduli e le loro interdipendenze [3]. Per esempio, il modulo core, da cui tutti gli altri moduli dipendono, è stato denominato java.base e ha le sue caratteristiche salienti.
Il modulo
La definizione del concetto di modulo Java è stata definita dal JEP 200: The Modular JDK [4]. Le idee base per il disegno del modulo sono:
- un modulo può contenere file di classi Java, risorse, librerie native e file di configurazione;
- un modulo deve avere un nome; e — aggiungiamo noi — questo nome deve essere univoco;
- un modulo può dipendere da altri moduli — uno o più — attraverso il suo nome: l’unico modulo che non dipende da nessuno è il base, mentre tutti gli altri devono dipendere almeno da questo;
- un modulo può esportare (tutti) i tipi pubblici presenti in uno o più package, definiti package API, che li contengono: in questo modo, tali tipi diventano disponibili per l’uso da parte del codice presente in altri moduli che dipendono da esso e anche da codice non organizzato in un modulo, ossia non ancora modularizzato.
- attraverso il nome dei moduli, un modulo può limitare l’insieme dei moduli a cui vengono esportati i tipi pubblici definiti nei propri package API;
- un modulo può anche riesportare a sua volta tutti i tipi pubblici esportati da uno o più moduli da cui esso dipende.
Aspetti peculiari
Il punto 5 permette, a un insieme definito di moduli, di condividere API interne di implementazione senza dover necessariamente esporre tali tipi pubblici anche a tutti gli altri moduli. Il vantaggio di questo costrutto consiste nell’evitare accorpamenti artificiosi di moduli: questi accorpamenti infatti non erano strettamente necessari in termini di disegno, ma erano dettati da considerazioni volte a evitare tipi “interni”. In poche parole, questa caratteristica permette di dar luogo a una modularizzazione chiara ed elegante.
Il punto 6 consente ai vari moduli di poter evolvere nel tempo, ossia di essere oggetto di processi di refactoring, preservando le serie di tipi pubblici esportati. Inoltre, questa strategia supporta la definizione di moduli aggregatori, che raccolgono insieme tutti i tipi pubblici esportati da un insieme di moduli tra loro collegati.
Esempio di costruzione e definizione di moduli
Per avere un’idea concreta della costruzione e definizione dei moduli riporteremo semplici file XML utilizzati all’inizio del progetto per la definizione dei moduli [4]. Questo template ha esclusivamente un valore illustrativo e non è stato implementato. Tuttavia lo riportiamo in questo articolo in quanto è funzionale alla comprensione dei paragrafi successivi.
Vediamo anzitutto il modulo java.sql:
<module> <!-- The name of this module --> <name>java.sql</name> <!-- Every module depends upon java.base --> <depend>java.base</depend> <!-- This module depends upon the java.logging and java.xml modules, and re-exports their exported API packages --> <depend re-exports=“true”>java.logging</depend> <depend re-exports=“true”>java.xml</depend> <!-- This module exports the java.sql, javax.sql, and javax.transaction.xa packages to any other module --> <export><name>java.sql</name></export> <export><name>javax.sql</name></export> <export><name>javax.transaction.xa</name></export> </module>
Questo secondo listato mostra invece il modulo java.security.sasl:
<module> <name>java.security.sasl</name> <depend>java.base</depend> <depend>java.logging</depend> <export> <name>javax.security.sasl</name> </export> <!-- Export the com.sun.security.sasl.util API package only to the java.security.jgss module --> <export> <name>com.sun.security.sasl.util</name> <to>java.security.jgss</to> </export> </module>
Andiamo a vedere rapidamente le varie parti.
Name
È quello che ci si aspetta: la dichiarazione del nome univoco del modulo.
Depend
Permette di specificare la lista dei moduli da cui dipende quello oggetto di definizione. Tutti i moduli, come illustrato poco sopra, dipendono almeno da quello base: java.base. Dal primo listato si evince che il modulo java.sql dipende dal logging standard java (java.logging) e dal modulo XML (java.xml).
Depend/re-export
Il tag depend permette anche di riesportare l’API di qualsiasi altro modulo da cui quello attuale dipende: a tal fine è utilizzato l’attributo XML re-export. Ciò al fine di supportare processi di refactoring e, in particolare, fornisce la possibilità di poter dividere e unire i moduli senza rompere le dipendenze giacché quelli originali possono continuare a essere esportati. Nel primo listato, java.sql riesporta entrambi i moduli da cui dipende: logging e xml.
Export
Permette di definire i package esportati dal modulo in questione, quindi solo i tipi pubblici presenti in tali package saranno visibili ad altri moduli. Chiaramente, per poter poi accedere a tali tipi, eventuali moduli “client” dovranno dichiarare esplicitamente la dipendenza dal modulo. Come si può notare dal secondo listato, esiste anche l’export qualificato, ossia il package API è esportato soltanto ai moduli dichiarti. Sempre nel secondo listato, per esempio, com.sun.security.sasl.util è esportato esclusivamente al modulo java.security.jgss. Quindi, a differenza dell’export standard il quale non limita l’utilizzo del package API e quindi non ha alcun interesse di sapere quali moduli lo utilizzeranno, l’export qualificato specifica chiaramente i moduli a quali è concesso accedere ai tipi presenti nel package API.
E il versioning?
In questo codice XML è da notare è la mancanza di un sistema esplicito e robusto di versioning. Questo avrebbe potuto portare a strascichi nella versione Java 9 del precedente “JAR hell” (“l’inferno dei Java archives”). Tutto ciò è ben evidenziato da Nicolai Parlog a proposito della mancanza di versioning [5]:
“A prima vista questa lacuna sembrerebbe creare ‘l’inferno dei moduli’ al posto ‘dell’inferno dei JAR’ perché invece di avere molteplici JAR dipendenti da diverse versioni dello stesso JAR, si avranno molteplici moduli dipendenti da diverse versioni dello stesso modulo.”
Tuttavia, è importante notare che questo semplice XML era stato introdotto solo per capire a fondo la modularizzazione Java. La versione finale, come mostrato di seguito, include un sistema di dipendenza da versioni speficiche.
I principi alla base della progettazione del modulo
I principi che hanno guidato il disegno del modulo sono riportati di seguito.
Come primo principio, tutti i moduli standard, le cui specifiche sono regolate dal JCP, devono avere nomi che iniziano con il prefisso java.
In secondo luogo, tutti gli altri moduli che sono semplicemente parte del JDK, devono avere nomi che iniziano con il prefisso jdk.
In terza istanza, se un modulo A esporta un tipo che contiene un membro public o protected che, a sua volta, si riferisce a un tipo definito in qualche altro modulo B, allora il modulo A deve esportare nuovamente i tipi pubblici del modulo B. Ciò serve ad assicurare che la catena di invocazione metodi funzioni come atteso senza comportamenti imprevisti.
Il quarto principio ci dice che un modulo standard può contenere package sia di API standard, sia diverse. Tuttavia:
- esso può esportare uno qualsiasi dei suoi package di API standard, senza alcuna restrizione;
- esso può esportare uno qualsiasi dei sui package API, sia standard sia diversi, in modo limitato per un insieme specifico di moduli standard e non standard;
- il modulo non può esportare i package API non standard in altri modi se non quelli appena detti al punto precedente;
- se poi si tratta di un modulo Java SE — modulo da proporre per l’inclusione nel Java SE Platform Specification — allora non deve esportare alcun pacchetto API non SE.
Quinto aspetto: un modulo standard può dipendere da uno o più moduli non standard. Non può tuttavia riesportare i tipi pubblici dei moduli non standard. Se si tratta di un modulo Java SE, allora non può riesportare i tipi pubblici solo ed esclusivamente di moduli SE.
Sesto: un modulo non standard non deve esportare i package API standard. A un modulo non standard è consentito riesportare i tipi pubblici esportati da un modulo standard.
Una conseguenza importante di principi 4 e 5 è che il codice che dipende solo su moduli Java SE dipenderà solo da tipi Java SE standard, e quindi sarà portabile su tutte le implementazioni della piattaforma Java SE.
Dichiarazione di modulo: module-info.java
Abbiamo visto sopra la semplice sintassi del file XML che, lo ricordiamo, è stata presentata solo a fini divulgativi. Andiamo invece ora a vedere il modo in cui avviene la dichiarazione del modulo: analizziamo il file module-info.java (l’iniziale “m” è volutamente minuscola) [6].
Indovinate un po’? “Hello World”!
Ripartiamo dal classico “Hello World”. Ora, per poter funzionare correttamente nel mondo dei moduli, la “famosa” classe ha bisogno di due file: la classe stessa, con il main eseguibile, e il file con le informazioni relative al modulo.
Ogni modulo Java deve includere una dichiarazione di modulo attraverso il file module-info.java nel quale, in maniera analoga al file XML, si devono specificare
- il nome del modulo
- la versione
- le sue dipendenze.
Ad esempio, il seguente è il module-info.java per il modulo com.greetings. La convenzione prevede che il codice sorgente del modulo sia inserito in una cartella che ha lo stesso nome del modulo. Per esempio:
src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java
In questo caso Main prevede il seguente codice:
package com.greetings; public class Main { public static void main(String[] args) { System.out.println(“Hello World”); } }
module-java prevede questo:
module com.greetings { }
Una volta compilato il tutto nella directory mods/com.greetings, dalla stessa directory è possibile eseguire il codice:
java -modulepath mods -m com.greetings/com.greetings.Main
Come si può notare, vi sono due nuove opzioni: -modulepath e -m.
-modulepath permette di specificare una o più directory che contengono i moduli. -m permette di specificare il modulo principale, il main.
Un ulteriore passo, con la dipendenza
Si supponga ora di eseguire una seconda versione del codice in cui sono presenti due moduli, di cui quello precedente questa volta stampa un messaggio definito in un altro modulo, quindi dipende da un altro per ottenere il testo da mostrare. Vediamo la struttura:
src/org.astro/module-info.java src/org.astro/org/astro/World.java src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java // codice di src/org.astro/module-info.java module org.astro { exports org.astro; } // codice di src/org.astro/org/astro/World.java package org.astro; public class World { public static String name() { return “Hello world”; } } // codice di src/com.greetings/module-info.java module com.greetings { requires org.astro; } // codice di src/com.greetings/com/greetings/Main.java package com.greetings; import org.astro.World; public class Main { public static void main(String[] args) { System.out.format(“%s!%n”, World.name()); } }
In questo caso, nel file module-info.java è stato necessario dichiarare la dipendenza dei moduli per mezzo dell’istruzione requires.
JAR modulari
Per quanto attiene al packaging, anche i JAR sono diventati modulari. Un JAR modulare (modular jar) non è altro che uno JAR standard con in aggiunta il file module-info nella dirctory top-level. Inoltre, questi JAR possono anche includere la definizione della versione, premessa dal carattere chiocciola. Per esempio:
com.greetings@1.0.jar
Nel prossimo articolo vedremo tra l’altro qualche esempio più complesso e parleremo di altri elementi come i services presenti nella tabella riassuntiva di cui si è parlato all’inizio di questo articolo.
Conclusione
Java 9 è finalmente disponibile per il download e con esso arriva finalmente la modularizzazione Java… dopo quasi un decennio di attese. Il monolitico JRE è stato decomposto in un insieme razionale di moduli interdipendenti, a partire dal modulo java.base.
Dopo aver rivisto la modularizzazione nel suo complesso nel precedente articolo, in questo ci siamo occupati del concetto di modulo a partire delle assunzioni iniziali e dai principi che ne hanno guidato il disegno e l’implementazione. Inoltre, abbiamo iniziato a dare una sbirciatina al file module-info.java che contiene la dichiarazione della “struttura” del modulo.
Nel prossimo articolo entreremo ancora più in dettaglio dentro questo file e presenteremo altri elementi, come i servizi.