Sappiamo bene che realizzare una applicazione JavaEE prevede la realizzazione di package che dovranno poi essere deployati in un application server. Ma questo processo può comportare alcuni problemi e inconvenienti che vanno affrontati nel modo corretto. Vediamo in questo articolo alcune strategie per massimizzare la portabilità dei package.
Il problema
La realizzazione di una applicazione JavaEE è un processo che si basa, fra le altre cose, sull’assemblaggio di moduli precostituiti unitamente al codice scritto dal programmatore, il tutto impacchettato all’interno di un archivio EAR (o simili). Per questo è necessario includere classi o librerie terze parti, tramite le quali è possibile disporre di quelle funzionalità che non sono fornite dal codice dell’applicazione o dalle API della piattaforma J2EE; la procedura è elementare da un punto di vista teorico ma all’atto pratico necessita di particolare attenzione e conoscenza dettagliata del modello di interdipendenza alla base del modello JavaEE. Oltre a vedere nel dettaglio i meccanismi che la piattaforma J2EE mette a disposizione per includere librerie esterne in applicazioni portabili, si presenteranno alcuni casi in cui si pone la necessità di usare delle strategie leggermente diverse per utilizzare tali meccanismi. In tale contesto, data la disponibilità di tool appositi per assistere lo sviluppatore in queste operazioni, concentreremo l’attenzione anche sull’analisi di tali strumenti.
È noto come il discorso sulla portabilità delle applicazioni sia cruciale per la piattaforma J2EE: sia il programma Java Blueprint, sia l’AVK per la piattaforma Enterprise (Application Verification Kit) rappresentano una risposta a questa esigenza. La Java AVK per la piattaforma Enterprise serve a identificare quelle applicazioni enterprise realizzate per essere portabili in diverse implementazioni della piattaforma J2EE. Si tratta di una serie di strumenti che assistono lo sviluppatore a verificare che le applicazioni da loro create usino correttamente le API J2EE e rispondano al criterio di portabilità sui diversi application server compatibili con J2EE.
Il programma Java Blueprint, invece, rappresenta una ampia raccolta di linee guida, pattern di design, applicazioni di esempio che gli sviluppatori possono usare come riferimento quando realizzano le loro applicazioni portabili. In particolare ci sono due applicazioni di esempio che risultano molto utili: la Java Adventure Builder Reference insegna come si impostano servizi portabili e che interagiscono fra loro (J2EE 1.4), mentre la Java Pet Store Sample Application dimostra come usare la piattaforma J2EE per realizzare applicazioni enterprise robuste, scalabili, portabili e facilmente manutenibili. Si tratta di esempi sicuramente da studiare.
Tipi di dipendenze e meccanismi di gestione
Uno degli aspetti più importanti è legato alle dipendenze statiche nel codice dell’applicazione e ai relativi meccanismi per gestire queste dipendenze; alcune applicazioni prevedono delle dipendenze dinamiche, e in tal caso non è necessario utilizzare i meccanismi che vedremo.
Che cosa significa dipendenza statica? L’esempio tipico è l’applicazione che usa una libreria di log per mostrare all’utente il progresso di un’azione oppure le informazioni di errore: si tratta di una libreria indispensabile per la corretta esecuzione dell’applicazione che quindi deve specificarne la dipendenza secondo uno dei meccanismi che vedremo più avanti.
Una dipendenza dinamica invece è quella in cui l’applicazione usa un generico package di log: l’applicazione non dovrebbe essere legata strettamente all’implementazione del logging, con conseguente “autoconfigurazione” del package di logging. In pratica, sempre che non ci sia una configurazione prestabilita e scritta direttamente nel codice, a livello logico dovrebbe avvenire quanto segue:
- se c’è disponibile l’API log4j, si usa questa;
- se c’è disponibile J2SE 1.4 logging, si usa questa;
- se non ci sono, si fa ricorso a una semplice implementazione del logging su file di testo.
La dipendenza dinamica viene quindi risolta a runtime: non è richiesta la presenza dell’API log4j (e quindi non viene stabilita una dipendenza statica). Lo strumento di verifica “statica”, AVK per esempio, ci dirà comunque che questa API non è presente, ma ciò non si concretizzerà in un fallimento della verifica. Gli strumenti di verifica dinamica ci riferiranno automaticamente i package che vengono usati a runtime.
Un altro termine che necessita di essere specificato è quello di “libreria” con il quale si intende una raccolta di classi che verrà “impacchettata” tramite l’utilità JAR. Possono essere classi sviluppate da terze parti, possono far parte del “corredo” a disposizione della propria organizzazione, e così via: in ogni caso si tratta in genere di classi che forniscono funzionalità di uso comune, utilizzabili da svariate applicazioni diverse.
Detto questo, quali sono i casi in cui dovremmo usare dei precisi meccanismi di packaging delle librerie per “corredare” le nostre applicazioni portabili? Be’, non mancano le situazioni abbastanza comuni in cui ciò accade: si consideri il caso di una applicazione basata su architettura MVC (Model View Controller) realizzata da una applicazione che usi Struts e le sue API, che quindi dipende sia dalle classi di Struts che dalle API standard J2EE. Oppure un’applicazione che usi JSTL (JavaServer Pages Standard Tag Library) e che quindi necessita di un file jstl.jar per fornire alcuni tag usati dalla pagine JSP dell’applicazione. O ancora, pensiamo a un’applicazione che usa una qualche utility di logging (che potrebbe essere una libreria esterna in JAR oppure una libreria sviluppata internamente, parallelamente all’applicazione): conviene sicuramente impachettare questa libreria in un JAR separato, per avere poi un libreria che può essere riutilizzata da tante applicazioni differenti.
Quando poi si è stabilito di usare una di queste librerie, ci si troverà davanti a un’ulteriore scelta di progettazione: sarà infatti necessario stabilire nel design della propria applicazione il modo in cui queste librerie extra verranno inserite all’interno dell’applicazione. Le scelte adottate dovranno tener presenti diversi aspetti, quali la portabilità dell’applicazione, la dimensione dei file WAR e EAR, la facilità di manutenzione e, non ultimo, il controllo di versione, a mano a mano che le librerie e gli application server vengono aggiornati.
Meccanismi per l’uso delle librerie nelle applicazioni J2EE
Volendo analizzare i vari meccanismi che consentono di usare package opzionali tipo i JAR di classi di utilità o librerie è necessario prendere in esame due aspetti:
- in che “luogo” collocare il file JAR con le librerie;
- in che modo i file dell’applicazione principale (JAR, EAR, WAR, RAR o quello che è) possono indicare dove trovare il file JAR: se ci sarà un riferimento esplicito, se saranno collocati in posizione nota (nella classica WEB-INF/lib o nella directory lib/ext directory del J2EE Runtime Environment).
In ogni caso, va anche tenuto presente come alcune delle soluzioni possibili per trattare questi JAR file di librerie non siano “universali” ma si adattano specificamente, o esclusivamente, a determinati application server. In questo senso occorre conoscere bene i diversi meccanismi anche per evitare di scegliere quello sbagliato per il proprio scenario. Alcuni particolari application server, per esempio, hanno delle posizioni specifiche in cui devono essere messi i file JAR da condividere fra le varie applicazioni e i vari moduli. Vedremo pertanto alcuni dei più comuni e conosciuti meccanismi, con i loro pro e contro.
Meccanismo 1: la directory WEB-INF/lib
Collocare nella directory WEB-INF/lib del file WAR un archivio JAR con le librerie opzionali (per esempio, struts.jar) permetterà a una web application o a un modulo web di usare le API Struts. Questa soluzione si adatta solo ai moduli web (file WAR) perche’ il file JAR della libreria viene incluso come parte del WAR. In tal senso, la directory WEB-INF/lib è la collocazione “nota” per librerie varie ed è supportata direttamente dalla piattaforma J2EE. Il file JAR della libreria risulta disponibile solo al modulo web all’interno della cui directory WEB-INF/lib si trova, ma non può essere utilizzata da altri moduli o applicazioni. Questo meccanismo, inoltre non può essere utilizzato da moduli EJB.
Meccanismo 2: classi opzionali in bundle
Consiste nell’usare l’attributo Class-Path nel file manifest, per referenziare uno o più JAR inclusi nel file EAR. Con questo meccanismo, il file JAR con le librerie aggiuntive viene incluso come parte del file EAR che effettua il riferimento: in questo caso si usa dire che la libreria è in bundle (bundled). Dal momento che sia gli EAR, che i WAR, che i RAR sono in definitiva dei file JAR, possono usare il meccanismo del JAR per effettuare il riferimento ad altri file JAR dipendenti. In pratica si include un file manifest (META-INF/MANIFEST.MF) all’interno del file JAR che effettua il reference, e nel file manifest si utilizza l’attributo Class-Path per specificare il JAR della libreria, incluso nel file EAR dell’applicazione. Da notare che nel Class-Path si può specificare più di una libreria.
Le specifiche J2EE 1.4 stabiliscono che i file JAR di livello più alto, come gli EAR, non dovrebbero contenere riferimenti Class-Path, poiche’ finerebbero per fare riferimento a file esterni al JAR. Con questo meccanismo, il file JAR della libreria viene collocato in bundle come parte dell’EAR o del WAR e quindi non risulta esterno.
Ecco l’esempio per un file EAR che contenga un modulo web che usa Struts:
application.ear
META-INF/application.xml struts.jar webapp.war META-INF/MANIFEST.MF: Manifest-Version: 1.0 Class-Path: struts.jar WEB-INF/web.xml
Meccanismo 3: package installati
Si tratta di usare l’attributo Extension-List del file manifest per effettuare il riferimento a uno o più JAR con librerie, che non sono allegate in bundle con il file EAR, ma sono comunque installate in qualche collocazione ben nota, tipo la directory lib/ext del JRE (Java Runtime Environment). In questo meccanismo, il file JAR con la libreria non viene incluso come parte del file EAR, ma qualsiasi libreria contenuta in un file JAR, cui si fa riferimento, viene ad essere esterna al file EAR, installata nella directory lib/ext. Quindi occorre che questa collocazione esterna venga referenziata tramite l’attributo Extension-List. Questa soluzione viene spesso definita quella delle “librerie installate”: nel file EAR dell’applicazione, l’attributo Extension-List del file manifest è usato per per esprimere la dipendenza del file EAR da un file JAR di libreria. Anche per l’attributo Extension-List è possibile specificare più di una libreria. Da notare come il deploy delle applicazioni non avverrà correttamente se il server non riesce a trovare la libreria e a risolvere la dipendenza. Inoltre, con questo meccanismo dei package installati, il file JAR della libreria diventa disponibile a tutte le applicazioni.
Di seguito riportiamo un esempio (specifica della piattaforma J2EE 1.4) in cui il file util.jar è collocato nella directory di installazione libext. In questo esempio la dipendenza util.jar viene utilizzata dall’applicazione app1.ear:
app1.ear
META-INF/application.xml ejb1.jar: META-INF/MANIFEST.MF: Extension-List: util util-Extension-Name: com/example/util util-Extension-Specification-Version: 1.4 META-INF/ejb-jar.xml
util.jar
META-INF/MANIFEST.MF: Extension-Name: com/example/util Specification-Title: example.com's util package Specification-Version: 1.4 Specification-Vendor: example.com Implementation-Version: build96
Uno sguardo alle estensioni J2SE 1.4 per i package opzionali
Il tema dell’uso di package opzionali riguarda non solo la piattaforma enterprise, ma anche quella standard. I meccanismi J2EE 1.4 per effettuare l’impacchettamento dei file JAR usano il meccanismo J2SE che consente alle applicazioni di usare librerie “impacchettate” come JAR. Vediamo brevemente questi concetti, ricordando come un tempo si parlasse di “estensioni” (standard extensions) mentre adesso è invalso l’uso del nuovo termine “package opzionale”. La piattaforma J2SE consente l’uso dei package opzionali in due maniere: install e download.
I package opzionali installati sono file JAR collocati dentro la directory lib/ext (nel JRE) o jre/lib/ext (nel SDK) e che hanno un file manifest che li descrive.
I package opzionali scaricati (bundled optional package) sono file JAR specificati dall’attributo Class-Path nel file manifest di un altro file JAR: le classi nei package opzionali in download possono essere usate da classi che fanno parte del JAR che effettua il riferimento. A differenza dell’altro caso, quello dei package installati, la collocazione dei file JAR che fungono da package opzionali in bundle o in download è irrilevante, proprio perche’ un package opzionale in download è un pacchetto opzionale specificato dal valore dell’attributo Class-Path del file manifest di un altro file JAR, e non perche’ si trova in una particolare collocazione.
Scenari
Adesso il quadro dovrebbe cominciare a essere chiaro: vediamo come i concetti presentati finora si applicano a scenari abbastanza comuni nel mondo delle applicazioni J2EE.
Primo scenario: l’applicazione è un file WAR stand-alone che usa uno o più file di librerie.
Come applicazione, abbiamo un file WAR che non è “impacchettato” in un file EAR: ciò è possibile in J2EE 1.4, dove un modulo stand-alone tipo un file WAR rappresenta a tutti gli effetti una applicazione J2EE valida (non è necessario un EAR che la inglobi). In questo caso, si può usare il meccanismo 1, della directory WEB-INF/lib, includendo i file JAR (struts.jar, per esempio) nella directory WEB-INF/lib dell’applicazione, ossia del file WAR. Tutto qui: le librerie aggiuntive (file JAR) sono collocate nel package come parte del file WAR.
Il meccanismo 2 delle classi opzionali in bundle, in questo caso, non funzionerebbe. Il file WAR dell’applicazione, infatti, sta al livello più alto della gerarchia (ossia si tratta di un file stand-alone di tipo WAR o EAR) e i file al livello gerarchico più elevato non devono possedere un attributo Class-Path che implicherebbe un file esterno al file di livello più elevato e non quello che è inglobato dentro di esso.
Funzionerebbe invece il meccanismo 3 dei package opzionali installati. Per esempio, una file di libreria in formato JAR, come struts.jar, potrebbe essere collocato nella directory delle estensioni (lib/ext) a patto che l’applicazione includa un file manifest che usa l’attributo Extension-List per indicare che gli servono uno o più package opzionali.
Secondo scenario: l’applicazione è un file EAR che contiene più di un WAR. Si desidera che i web module condividano lo stesso unico file JAR (con libreria comune) piuttosto che inserire varie copie duplicate della libreria.
Si tratta di una situazione appena più complessa, ma abbastanza comune: immaginiamo di avere una applicazione EAR, con due file WAR che usano entrambi Struts: il file struts.jar dovrebbe essere condiviso tra i due WAR, invece che essere presente dentro di essi in copie duplicate. In tal caso, si può ricorrere o al meccanismo 2 delle classi opzionali in bundle o al meccanismo 3 dei package installati. Non è invece possibile usare il meccanismo 1 della directory WEB-INF/lib: si tratta esattamente della soluzione che vogliamo evitare, perche’ prevede proprio che ciascun WAR abbia la sua copia della libreria JAR. Vediamo in pratica le due soluzioni possibili.
Il meccanismo 2 delle classi opzionali in bundle vede questa applicazione: ciascun WAR utilizza l’attributo Class-Path del file manifest per puntare alla stessa collocazione del file JAR con la libreria necessaria. Questo JAR di utilità si trova da qualche parte dentro l’archivio EAR e così basta includere una sola copia di, per esempio, struts.jar dentro l’applicazione.
Il meccanismo 3, quello dei package installati, prevede che ciascun web module WAR utilizzi un file manifest con l’attributo Extension-List che fa riferimento alla libreria installata nella directory delle estensioni, che non è inclusa dentro il file EAR. Anche in questo modo ciascun WAR condivide lo stesso file JAR.
Terzo scenario: applicazioni multiple (diversi EAR e/o vari WAR stand-alone) devono condividere lo stesso file JAR contenente una libreria di utilità.
Molte applicazioni, unica libreria: quello che non si vuole è dover “moltiplicare” il file JAR per collocarlo in ciascuna applicazione: un caso classico è quello delle librerie di tag JSTL che devono essere usate da molte applicazioni. La soluzione è quella del meccanismo 3, i package opzionali installati. Una copia della libreria si colloca nella directory delle estensioni e si utilizza poi il file manifest con l’attributo Extension-List per specificare questa dipendenza.
Quarto scenario (cambiamento di prospettiva): il problema dal punto di vista dello sviluppatore di librerie di utilità che devono essere usate da altre applicazioni.
Fin qui abbiamo visto le situazioni dal punto di vista di uno sviluppatore che crea applicazioni principali le quali hanno una dipendenza da qualche libreria: abbiamo spiegato come sia possibile risolvere i problemi di portabilità di tali applicazioni. Ma se cambiamo prospettiva? Come deve comportarsi lo sviluppatore di una libreria di questo tipo (come Struts o JSTL) o di classi di utilità che devono risultare portabili sui diversi application server J2EE 1.4? Come comportarsi se, addirittura, la libreria che si crea ha anch’essa dipendenze da codice di terze parti? Come si realizzano correttamente i package di queste librerie, in modo che possano essere usate in maniera portabile in una applicazione J2EE?
Le specifiche della piattaforma J2EE 1.4 (sezioni 8.2 e 8.4) dicono chiarmente che il file da usare come package opzionale deve essere “impacchettato” come JAR (.jar) rispettando le regole della Extension Mechanism Architecture. Inoltre, il file JAR deve avere un file manifest che dichiari a sua volta chiaramente le sue dipendenze ne caso esse esistano. Chiaramente, anche in questa occasione ci vengono in aiuto gli strumenti visti in precedenza (Java AVK for the Enterprise), che diventano preziosi per verificare la corretta realizzazione del “pacchetto”. Se la nostra libreria non ha dipendenze da codice esterno, per il suo impacchettamento si può usare uno qualsiasi dei tre meccanismi descritti in precedenza: le regole e i meccanismi che valgono per l’utilizzatore della libreria valgono anche per il creatore della libreria stessa.
Problemi e buone pratiche
Nelle specifiche J2EE 1.4 ci sono zone d’ombra, per esempio riguardo al modo in cui gestire versioni multiple di una libreria: siccome il classloader nei diversi application server può essere differente, ci potrebbe essere un trattamento non uniforme di queste librerie. E allora come ci si può ovviare a questi problemi? Occorre avere una precisa policy per la gestione delle versioni e tale policy può essere stabilita solo a partire dalla comprensione delle varie modalità in cui i diversi server caricano le classi.
Una di queste modalità riguarda l’ordine di precedenza: che succede se il file EAR di un’applicazione utilizza più di un meccanismo per effettuare il riferimento, all’interno della sua struttura a package? Oppure, che succede se sono presenti versioni multiple (o copie multiple della stessa versione) di un file JAR con la libreria? A runtime, quale istanza del file JAR dovrà essere usato dall’applicazione? Va notato oltretutto che questo ordine di precedenza può anche essere diverso in J2SE rispetto alla piattaforma J2EE… In ogni caso, le specifiche per la piattaforma J2EE raccomanda un ordine di precedenza per “rintracciare” un package opzionale, pertanto l’application server dovrebbe cercare una classe in un file JAR di librerie con il seguente ordine:
- secondo il meccanismo 1, cioè nella directory WEB-INF/lib
- secondo il meccanismo 2, cioè se ci sono classi opzionali in bundle
- secondo il meccanismo 3: cioè se si trovano package installati
Nel caso la classe non venga trovata, il deploy dell’applicazione non va a buon fine.
Va notato inoltre che in genere gli application server “fanno delle scelte”: per esempio spesso non consentiranno a un file JAR di libreria di utilità di sostituire una API standard della piattaforma: includere il proprio file servlet.jar, anche usando i corretti meccanismi illustrati in questo articolo, potrebbe portare a un nulla di fatto, in quanto tale file “personalizzato” potrebbe “perdere il confronto” con l’API standard. Chiaramente si tratta di una misura precauzionale per impedire al container malfunzionamenti anche in situazioni standard, e questo va tenuto presente quando si passano applicazioni da un server a un altro.
In definitiva, occorre avere ben presente la situazione e seguire delle linee guida ragionate per fronteggiare eventuali problemi: dando per scontato che i server seguano nel caricamento l’ordine di precedenza raccomandato nelle specifiche, le applicazioni dovrebbero in qualche modo includere una versione della libreria nell’archivio, sotto forma di package opzionale in bundle, in modo che venga selezionata questa (secondo posto dell’ordine) anche se nel server esiste un’altra versione della libreria, già installata nella directory delle estensioni (terzo posto dell’ordine di precedenza).
Conclusioni
Conoscere i meccanismi alla base della portabilità di applicazioni J2EE consente di realizzarle senza incorrere nei problemi che di solito si verificano quando si realizzano applicazioni da usare sui diversi application server. In definitiva, questi meccanismi possono essere ridotti a tre soluzioni
- includere la libreria nella directory WEB-INF/lib;
- allegare in bundle la libreria se essa deve essere condivisa fra componenti diversi dell’applicazione;
- installare la libreria nella directory delle estensioni lib/ext, da dove la potranno usare diverse applicazioni.
Abbiamo anche visto come esistano degli strumenti per verificare il corretto “confezionamento” dei package: il “Java AVK for the Enterprise” ha tutto quel che necessita per sottoporre un package a test e capire se ci sono errori nella sua struttura e debolezze nella sua portabilità. Abbiamo infine visto una serie di scenari, ossia situazioni possibili, con le opportune soluzioni, nonche’ una serie di problemi e di linee guida riguardanti la sottile arte della portabilità.
Riferimenti
[1] La specifica J2EE 1.4 in PDF
http://java.sun.com/j2ee/j2ee-1_4-fr-spec.pdf
[2] La specifica sui file JAR, con i dettagli sul file manifest
http://java.sun.com/j2se/1.4.2/docs/guide/jar/jar.html
[3] Dettagli importanti sui file JAR si trovano nella Extension Mechanism Architecture
http://java.sun.com/j2se/1.4.2/docs/guide/extensions/spec.html
[4] I package opzionali in J2SE
http://java.sun.com/j2se/1.4.2/docs/guide/extensions/extensions.html
[5] Il tool AVK per la verifica dei package
[6] Inderjeet Singh – Vijay Ramachandran, “Packaging and Deployment”, un capitolo di Designing Enterprise Applications with the J2EE Platform, Second Edition