Introduzione
Su queste colonne ci siamo occupati numerose volte di Java, della sua evoluzione attraverso le varie release, inclusi gli inevitabili problemi. In diverse occasioni, ci siamo trovati a commentare il progetto Jigsaw [1] ossia la modularizzazione del linguaggio Java e del suo ecosistema. Si tratta di un progetto assolutamente ambizioso iniziato originariamente addirittura dalla Sun Microsystem nel lontano 2008. Era stato incluso inizialmente nello scope di Java SE 7, poi fatto slittare in Java SE 8 [2] e finalmente rilasciato con Java 9.
Integrare molteplici feature
Lo scopo del progetto Jigsaw (“puzzle”) è disegnare e realizzare un sistema modulare standard per Java SE da applicare sia al linguaggio, sia al suo ecosistema in generale a partire dal JDK. Si tratta di implementare nativamente una ristrutturazione del linguaggio e dell’ambiente che integri molteplici feature di Maven [3] e altre di OSGi (Open Services Gateway initiative).
Maven è un tool per la gestione e comprensione dei progetti e quindi è uno strumento dimostratosi molto valido per organizzare progetti, gestirne il ciclo di vita, le informazioni connesse, e così via; Maven però, per sua natura, si limita ad aspetti essenzialmente relativi a tempo di costruzione (build–time). In altre parole, Maven non si occupa di aspetti di modularizzazione a tempo di esecuzione.
Prima di Java 9, tale lacuna si prestava ad essere colmata dal framework OSGi. Questo è un sistema modulare e una piattaforma di servizi per il linguaggio di programmazione Java che implementa un modello a componenti completo e dinamico. OSGi, pertanto, si occupa degli aspetti di modularizzazione a tempo di esecuzione: per essere precisi, interviene a partire dal packaging includendo il deployment e l’esecuzione. Ma OSGi consente molto meno di gestire gli aspetti di organizzazione del progetto.
Pertanto, almeno da un punto di vista teorico, Jigsaw può essere considerato un’opportuna combinazione tra Maven e OSGi, combinazione che, ad onore del vero, è già una sorta di realtà in diverse organizzazioni.
Un nuovo mondo
Il limite di questa strategia, tuttavia, è che si tratta di una soluzione creata a partire dalla piattaforma Java preesistente, mentre il progetto Jigsaw ha richiesto cambiamenti radicali a partire dalla stessa piattaforma. Si è passati da una soluzione esterna ad una interna, nativa.
Compito decisamente complicato considerando che la base di partenza è un code-base che si è evoluto negli anni secondo un modello non modulare, sfruttando una struttura non in grado di evolvere verso una vera modularizzazione dell’ambiente. Una conseguenza di ciò, per esempio, è che il codice JDK era profondamente interconnesso sia alle API sia ai vari livelli di implementazione.
Ciò ha richiesto una serie di attività propedeutiche necessarie affinché la piattaforma e le sue implementazioni potessero effettivamente diventare un insieme coerente di moduli interdipendenti. In termini di codice, questo ha richiesto la revisione dell’intero codebase Java.
In questo primo articolo, ripercorreremo le motivazioni alla base del progetto Jigsaw, i vari problemi connessi, e accenneremo alle idee base della soluzione. Rimandiamo all’articolo successivo l’illustrazione dettagliata delle soluzioni.
Obiettivi del progetto Jigsaw
L’obiettivo fondamentale del progetto Jigsaw consiste nell’introdurre una serie di nuovi meccanismi, al livello di linguaggio di programmazione, da utilizzarsi per la modularizzazione di grandi sistemi, applicati a partire dallo stesso JDK. Poiché si tratta di nuove caratteristiche al livello di linguaggio di programmazione, queste sono disponibili ai programmatori per organizzare i propri progetti. Si tratta di un cambiamento, per molti versi radicale che, giocoforza, avrà una notevole influenza sul processo di sviluppo e di deployment dei progetti Java, specie per quelli di medio-grandi dimensioni.
Piattaforma scalabile: argine alla continua crescita del Java Runtime
La disponibilità del JDK modularizzato consente agli sviluppatori di selezionare esplicitamente le funzionalità necessarie per creare il proprio JRE che quindi consiste dei soli moduli dichiarati. L’impatto di questa specifica feature potrebbe essere contenuto se guardiamo ad ambienti basati su server con configurazioni standard visto che in questi ambienti non è di certo il JRE a creare problemi.
Discorso molto diverso si ha nel caso di sistemi firmware e piccoli dispositivi (routers, TV box, centraline per le autovetture etc.) dove le risorse hardware vanno ancora utilizzate attentamente. Tra l’altro, come nosta storica, ricordiamoci che proprio questo tipo di ambienti era considerato destinatario principe del nuovo linguaggio agli albori della lunga avventura di Java.
L’implementazione del progetto Jigsaw permette di scomporre sia la piattaforma Java SE, sia le sue implementazioni in una serie di componenti indipendenti che possono essere assemblati in un’installazione customizzata contenente esclusivamente le funzionalità richieste dall’applicazione (JSR 376) [4].
Decrescita felice… del JRE
Questa feature va a risolvere una lacuna sempre più evidente del mondo Java: il continuo crescere del Java Runtime. In effetti, prima di Java 9 non era possibile creare delle configurazioni su misura dell’ambiente JRE. La distribuzione includeva necessariamente tutte le librerie, indipendentemente dal loro utilizzo. Si tratta di una lacuna che ha un impatto in ambienti di piccole dimensioni, caratterizzati dalla presenza ridotta di risorse.
Ma questo gigantismo del JRE genera sprechi anche in organizzazioni di grandi e grandissime dimensioni — esempio tipico sono le banche — caratterizzate da migliaia di applicazioni disponibili nei vari ambienti (sviluppo, integrazione, QA, produzione, etc.). Ognuna di queste applicazioni, installata su diversi server, finisce per richiedere un footprint decisamente superiore a quello di cui effettivamente ha bisogno.
Per completezza, bisogna dire che un primo passo verso quantomeno la razionalizzazione di Java SE era stato compiuto in Java 8 con i profili compatti (compact profiles) [5] che definiscono tre sottoinsiemi di Java SE. Tuttavia, si tratta di una feature in grado di alleviare il problema solo in casi ben definiti. Inoltre questi profili risultano troppo rigidi per poter rispondere a tutte le particolari esigenze di “razionalizzazione” del JRE.
Configurazione esplicita e affidabile per la gestione delle dipendenze
Con Java 9 i vari moduli sono dotati di una configurazione che permette di dichiarare esplicitamente le dipendenze da altri moduli. Queste configurazioni giocano un ruolo importante nell’arco dell’intero processo di sviluppo del software, a partire dalla compilazione, passando per il build e terminando al tempo di esecuzione. Pertanto, si finisce per avere a disposizione un meccanismo di controllo in grado di far fallire immediatamente un sistema qualora una o più dipendenze non siano soddisfatte o risultino in conflitto.
Questa feature dovrebbe porre fine ad una serie di vecchi problemi di Java, come le dipendenze non espresse (Unexpressed Dependencies), le dipendenze transitive (Transitive Dependencies) e i conflitti di versione (Version Collisions).
Lo scenario precedente
Prima di Java 9, un JAR non poteva dichiarare la lista di altri JAR da cui dipendeva secondo un meccanismo standard compreso dalla JVM. Pertanto, gli sviluppatori si dovevano far carico di individuare e soddisfare manualmente le varie dipendenze. Inoltre, la presenza di feature/processi “opzionali”, la cui esecuzione era infrequente, finiva per generare dipendenze a loro volta opzionali, che rendevano questo processo ulteriormente noioso. Ciò unito al fatto che JRE si occupava di soddisfare le varie dipendenze solo al momento in cui era esplicitamente richiesto, creava la premessa per il lancio della tediosissima eccezione: NoClassDefFoundError.
Problemi di questo tipo sono stati notevolmente ridotti grazie alla presenza di tool come Maven il quale permette anche di risolvere dipendenze transitive che si verificano quando un JAR dipende da una serie di altri JAR che a loro volta dipendono da un altro insieme, e così via.
La nuova feature sulla gestione esplicita delle dipendenze fornisce anche un valido supporto al processo di individuazione e risoluzioni di scenari caratterizzati da librerie in conflitto (versions collision): due o più librerie dipendono da una diversa versione di una stessa libreria terza. In situazioni del genere, se entrambe le versioni della libreria sono aggiunge al classpath, si possono avere tutta una serie di comportamenti sgraditi e non sempre predicibili.
Questo perché viene ad intervenire il problema dell’adombramento (shadowing): le classi appartenenti a entrambe le versioni della libreria vengono caricate una sola volta con evidenti problemi generati qualora abbiano un comportamento diverso. Inoltre, se una classe è presente in una sola versione, la classe viene caricata concorrendo a creare un comportamento non predicibile. Lo shadowing nel migliore degli scenari finisce per far lanciare un’eccezione NoClassDefFoundError, mentre in altri casi può addirittura introdurre dei bug di difficile individuazione.
Incapsulamento al livello di modulo: fine di un’altra lacuna
Un obiettivo fondamentale del progetto Jigsaw consiste nell’introdurre un forte incapsulamento al livello di modulo. In particolare, si ha ora la possibilità, per ogni modulo, di suddividere i package in due gruppi: quelli visibili da parte di altri moduli e quelli privati, visibili solo al suo interno.
I modificatori di visibilità Java si sono dimostrati un ottimo supporto per l’implementazione dell’incapsulamento tra classi dello stesso package. Tuttavia, l’unica visibilità permessa per superare i confini dei package è quella pubblica. Questo ha finito per indebolire il concetto di incapsulamento.
In effetti, il class loader di Java carica tutti i package in un medesimo “spazio” e questo fa sì che tutte le classi pubbliche siano visibili a tutte le altre classi. Prima di Jigsaw non era possibile creare una funzionalità visibile per esempio solo all’interno di uno stesso JAR ma non da altri.
Usando le parole di Mark Reinhold: “Una classe che è privata per un modulo dovrebbe essere privata nell’esatta stessa maniera in cui un campo è privato in una classe. In altre parole, i confini dei moduli dovrebbero determinare non solo la visibilità di classi e interfacce ma anche la loro accessibilità” [6]. Questo livello di incapsulamento finisce per generare tutta una serie di conseguenze positive menzionate di seguito.
Migliore sicurezza e manutenibilità
Il forte incapsulamento dei moduli descritto sopra ha anche importanti ripercussioni positive sia sulla sicurezza, sia sulla manutenibilità del codice.
Per quanto concerne il primo aspetto, è ora possibile far sì che il codice critico sia veramente nascosto/protetto in modo tale da non essere assolutamente disponibile ad altro codice che non dovrebbe utilizzarlo.
Inoltre, anche la manutenibilità del codice migliora, giacché le API pubbliche dei moduli possono essere mantenute leggere e concise. In passato, una tecnica spesso utilizzata per cercare di ottenere una sorta di incapsulamento delle classi interne consisteva nel ricorrere a un’organizzazione in package, non sempre naturale. L’utilizzo non previsto delle API interne di Java è sia un rischio per la sicurezza, sia un peggioramento della manutenibilità. Con l’incapsulamento al livello di modulo è ora possibile evitare l’utilizzo da parte del codice applicativo delle classi interne di Java.
Miglioramento delle performance
Con la definizione esplicita dei confini dei singoli moduli, e conseguentemente con la definizione dell’utilizzo del codice, è possibile sfruttare più efficacemente tutta una serie di tecniche di ottimizzazione dell’esecuzione a runtime del codice già esistenti. Tutta una serie di strategie che eseguono il look ahead del codice possono diventare più incisive grazie alla conoscenza a priori dello scope di utilizzo del codice.
Il nuovo concetto core: il modulo
Da quanto descritto fino a questo punto, è chiaro che il concetto di modulo è l’elemento chiave per la modularizzazione introdotta con il progetto Jigsaw; e non poteva essere altrimenti.
In particolare, secondo la descrizione formale, i moduli sono:
“Elementi di codice e dati auto-descriventi (self-describing), dotati di un nome. Un modulo deve essere in grado di contenere classi e interfacce Java organizzati in package, e anche parti di codice nativo, nella forma di librerie a caricamento dinamico. Un modulo di dati deve essere in grado di contenere file di risorse statiche e file di configurazioni editabili dall’utente” [7].
Un’analogia con le librerie Apache Commons
Il modo più semplice per immaginare il concetto di modulo Java consiste nel considerare come tali le varie librerie Apache Commons (per esempio Collections, Compress, Common IO). Chiaramente, ne esistono alcune che sono abbastanza granulari e quindi si prestano bene a essere considerate un modulo a sé stante, mentre altre, verosimilmente, verranno decomposte in una serie di moduli.
Le maggior parte delle applicazioni con cui abbiamo a che fare potrebbero ben presto essere risolte in una serie di moduli espliciti. Con Java 9 gli sviluppatori utilizzeranno questo nuovo concento come parte del loro bagaglio di progettazione e implementazione del software e soprattutto dovranno confrontarsi con un JDK a sua volta modularizzato.
Feature nuova, concetti preesistenti
Per molti, si tratterà esclusivamente di rendere più espliciti e precisi concetti utilizzati implicitamente. Secondo Mark Reinhold [6]: “Gli sviluppatori già pensano a elementi standard di componenti del programma come classi ed interfacce del linguaggio. I moduli dovrebbero essere solo un altro concetto di un programma e come le classi e le interfacce dovrebbero avere un significato in tutte le fasi dello sviluppo del software.”
Inoltre, giocoforza, tutti dovranno inevitabilmente fare i conti con i moduli giacché lo stesso JDK sarà organizzato in moduli, come mostrato dalla figura 1.
Conclusione
C’è voluto tempo, è vero, e questa caratteristica ha subito numerosi rinvii: dopo quasi un decennio e un percorso a dir poco accidentato, la modularizzazione Java è finalmente disponibile come parte delle feature rilasciate con Java SE 9. Si tratta di una serie di meccanismi la cui implementazione ha richiesto di toccare praticamente l’intero codebase di Java e che, in diverse occasioni, si è trovata in competizione con altre feature che a loro volta hanno finito per toccare tutto il codice di Java, basti pensare al progetto Lambda.
Da un punto di vista logico, le nuove feature implementate dal progetto Jigsaw possono essere considerate come una razionale fusione di Maven e OSGi eseguita nativamente, direttamente all’interno di Java. Il concetto fondamentale del progetto Jigsaw è indubbiamente il modulo con il quale è possibile suddividere package visibili esternamente e quelli interni, nascosti e quindi fornire un livello di incapsulamento fino ad ora assente in Java.
Queste nuove feature dovrebbero mettere per sempre la parola fine al cosiddetto “inferno del classpath” (classpath hell) già fortemente ridotto grazie a tool quali Maven; inoltre dovrebbero permettere di avere JRE più leggeri e fortemente customizzati, il che risulta particolarmente utile per ambienti caratterizzati da limitate risorse. Dovrebbero infine migliorare il disegno delle API, aumentare il livello di sicurezza e ottenere performance superiori grazie a un migliore sfruttamento delle varie feature utilizzate da moduli JIT, con l’ulteriore vantaggio di innalzare il livello di robustezza del codice grazie al controllo nativo delle dipendenze.