Applicazioni enterprise, application server e classloader

Gestire il deploy di applicazioni JavaEEdi

Il meccanismo alla base del deploy di una applicazione enterprise di media complessità è molto articolato dato che deve tener conto di molti aspetti. Il più importante è forse quello che riguarda la logica di caricamento delle classi e delle librerie. Impacchettare una web application o una applicazione EJB in modo errato può portare a comportamenti impredicibili e alquanto bizzarri.

Di recente mi sono trovato a dover completare la fase di sviluppo e deploy di una applicazione sull‘application server JBoss. L‘applicazione è composta da una parte EJB e una parte web: il tutto è stato impacchettato all‘interno di un file .ear.

Uno scenario quanto mai comune e dopo i dovuti test e controlli sulla piattaforma di sviluppo siamo passati ai test sulla piattaforma di verifica che era in tutto e per tutto analoga a quella di produzione.
Con nostra sorpresa, sebbene in fase di sviluppo non si fossero verificati particolari inconvenienti, in fase di verifica sono sorti problemi del tutto imprevisti e imprevedibili senza una apparente giustificazione razionale. Evidentemente nel passare dalla piattaforma di sviluppo (JBoss 4.0.0) a quelle di verifica (JBoss 4.0.2) si erano introdotte alcune variazioni che portavano a errori del tutto incomprensibili.

L‘insorgere di una NullpointerException in un punto della applicazione dove viene eseguito il caricamento di una libreria esterna (si tratta di FOP per la generazione di PDF da XML) ci ha confermato che il codice era presumibilmente corretto e che il problema risiedeva nella configurazione dell‘application server. Ma JBoss da questo punto di vista, pur offrendo un livello di personalizzazione molto complesso, non dovrebbe essere considerato come il punto debole.

Purtroppo le NullpointerException non offrono molti spunti da questo punto di vista: si è ipotizzato alora che il problema dipendesse dalla procedura di caricamento delle classi. Conferma si è avuta quando si è provato a spostare la libreria incriminata (contenuta in un file .jar) da dentro il file .war direttamente nella directory di libreria della configurazione di JBoss utilizzata (JBOSS_HOME/server/default/lib). A questo punto tutto è tornato a posto e l‘applicazione ha cominciato a funzionare alla perfezione. Era certamente un problema di classloading.

JBoss utilizza una politica di caricamento delle classi basata su un sistema unificato e gerarchico di classloader: quello che viene caricato al tempo x dalla applicazione y non verrà  più ricaricato risparmiando tempo di esecuzione e il proliferare di librerie differenti. Se il meccanismo è quanto mai performante (specie in concomitanza di chiamate remote), a volte risulta essere particolarmente inappropriato e di difficile gestione: tanto per fare un esempio non è detto debba imporre la copia di una libreria (al momento utilizzata solo dalla applicazione x) in uno spazio di indirizzamento comune a tutte le applicazioni.

Ogni programmatore apprende quasi subito una delle regole fondamentali della programmazione enterprise (e non solo): è infatti consigliabile che ogni applicazione lavori con un proprio spazio di indirizzamento e di caricamento delle classi (a meno di casi standard come il driver JDBC il cui deploy può essere effettuato in maniera condivisa).

Già  in passato ci eravamo trovati di fronte a comportamenti "bizzarri" causati da conflitti fra librerie e versioni. Dato che, almeno nella documentazione, il team di JBoss dichiarava di aver definitivamente scelto (almeno fino al prossimo ripensamento, verrebbe da aggiungere) la politica e la tecnica di caricamento delle classi, era forse questo il momento di indagare ulteriormente per capire una volta per tutte quale sia il corretto modo per scrivere, impacchettare e collocare con il deployment una applicazione in un container JavaEE.

Java e la storia del caricamento delle classi

Comprendere il perchà© oggi, agli albori di Java6, ci si debba ancora preoccupare di dettagli legati al versioning di classi e librerie, richiede un passo indietro fino alla genesi di Java. La specifica Java EE impone che, all‘interno di un container, ogni applicazione possa usare una qualsiasi libreria di una versione specifica, indipendentemente dal fatto che altre applicazioni in esecuzione in quel momento nel container possano utilizzare versioni identiche o differenti della libreria o della utility. Questo obiettivo viene in genere messo in pratica grazie al concetto di di ‘‘namespace isolation‘‘ o nel gergo dei container enterprise tramite la cosiddetta ‘‘class load domain‘‘.

Java formalmente non contiene un nessun built-in che consenta di identificare la versione di una classe, per cui potrebbe risultare alquanto complesso implementare il class load domain senza l‘ausilio di alcun artificio. Il cosiddetto fully qualified name di una classe (FQN ovvero il nome della classe completo, compreso il suo package di appartenenza) permette infatti di distinguere due classi in maniera globlale all‘interno della applicazione ma non può distinguere se una classe appartenente a un determinato package e utilizzata all‘interno di un thread di esecuzione T1 sia la stessa che è utilizzata dentro un altro thread di runtime T2; quindi quando due applicazioni sono deployate all‘intero del container, l‘utilizzo di due versioni differenti della medesima classe rende le due classi indistinguibili agli occhi del container.

Sebbene questo aspetto possa apparire un "non problema", le complicazioni sono legate al fatto che in un container le classi sono caricate in maniera unificata; il motivo per cui questo avviene è frutto di una serie di scelte collegate alle prestazioni complessive della applicazione e alle differenti modalità  delle invocazioni locali/remote. Più avanti approfondiremo questo tema.

Per risolvere questo problema, Sun, con il rilascio della piattaforma Java2 ha esteso il concetto di identificativo univoco di una classe aggiungendo al Fully Qualified Name il Defining Classloader Domain (DCD), ovvero il nome del class loader che nel thread di esecuzione corrente (domain) ha eseguito il caricamento della classe. Una classe che viene utilizzata in un thread di esecuzione T1 sarà  perfettamente distinguibile da una classe analoga, ma ad esempio differente per versione, eseguita nel thread T2.

Dal punto di vista delle applicazioni JavaEE questo è in genere ottenuto grazie alla presenza di classloaders separati per ogni applicazione che è stata collocata nel container. Storicamente prima dell‘avvento di Java 2, alcuni importanti esperti del settore erano arrivati a dichiarare che Java non fosse type-safe: era una affermazione peraltro in controtendenza con le impressioni di quel periodo in cui si vedeva Java come linguaggio ancora non del tutto maturo ma sicuramente forte in materia di rigore formale e sicurezza a runtime.

La scelta di identificare una classe sulla base del FQN+DCD è da molti considerata un bene ma anche una limitazione. Ã? un bene perchà©, ad esempio, non è più possibile che un classloader ridefinito in maniera opportuna possa eseguire il caricamente di una versione personalizzata della classe java.lang.String spacciandola per quella ufficiale inserita nel JDK (e magari inserendo nella classe logica di controllo non corretta): il runtime infatti identifica immediatamente la diversa natura delle due versioni della java.lang.String provvedendo a lanciare una ClassCastException.

Inoltre è buona cosa utilizzare anche il classloader per identificare una classe perchà© si consente al container di introdurre il concetto di class loading domain. La grossa limitazione cui si va incontro emerge in tutta la sua drammaticità  in uno scenario distribuito tipico delle applicazioni Java EE: non è infatti possibile passare il reference di un oggetto fra due Class Loading Domain (si andrebbe incontro ad una ClassCastException) ma si deve sempre utilizzare il passaggio per copia (tramite serializzazione e deserializzazione), con un impatto notevolissimo sulle prestazioni.

Lo sviluppatore ponga la massima attenzione a questo fatto: nell‘ambito di una applicazione enterpise composta ad esempio da una parte web e da una parte EJB (organizzate in un file WAR e un JAR entrambi inseriti in un EAR), i vari moduli (web e server side) sono caricati da classloader differenti i quali, sebbene gerarchicamente figli di un padre comune (in JBoss il cosiddetto UnifiedClassLoader), sono a tutti gli effetti diversi fra loro.

Per questo una classe di trasporto che trasferisca informazioni dalla parte web a quella EJB o viceversa potrebbe anche piuttosto frequentemente portare a una ClassCastException durante il tragitto da un layer a un altro. Agli occhi del programmatore, strato web e strato server comunicano in maniera remota; ma in realtà  le ottimizzazioni messe in atto dal container, che esegue invocazioni locali appena ve ne sia la possibilità , rendono di fatto impossibile il passaggio di parametri per reference.

Si potrebbe genealizzare lo scenario in questo modo: sebbene la specifica non lo imponga e anzi in un certo senso lo vieti, il passaggio di un oggetto fra due applicazioni EJB (come invocazioni di metodi remoti fra due session bean), viene sempre ottimizzato all‘interno del container tramite il passaggio del reference diretto senza nessuna operazione di copia. Il problema è ben noto a Sun che ha cercato in qualche modo di porre rimedio con l‘introduzione delle interfacce locali al fianco delle remote, ma è una soluzione... di facciata. Una soluzione a tale problema è presentata al termine dell‘articolo.

Applicazioni enterprise: redeploy a caldo e container

Si è quindi compreso come l‘utilizzo dei classloader differenti sia il meccanismo tramite il quale il container esegue l‘isolamento fra i name space delle applicazioni deployate: ogni applicazione può fare affidamento su un classloader differente per cui può avere la certezza di utilizzare una propria versione delle classi di libreria.

Il redeploy a caldo è, presumibilmente, la funzionalità  più importante di cui ogni application server dispone dato che di fatto, fra le altre cose, consente l‘utilizzo della tecnologia enterprise in contesti reali di produzione: poter ridistribuire una applicazione nel container, senza dover spegnere tutto, consente una migliore gestione del sistema, offre la possibilità  di eseguire aggiornamenti in modo più o meno indolore ed è la base per garantire la disponibilità  24x7.

Sfortunatamente Java4 non contiene al suo interno nessuna funzionalità  esplicita che permetta il redeploy a caldo di una applicazione: una volta che una classe viene aggiunta al pool delle classi del runtime in esecuzione non potrà  più essere rimossa o sostituita con una nuova versione. Fortunatamente lo scenario non è cosଠrigido proprio grazie alla presenza dei classloader all‘interno di un container enterprise: si supponga infatti che una applicazioni App1 faccia riferimento per il suo funzionamento ad una altra applicazione App2 e che entrambe siano deployate all‘interno di un container. Se App1 non ha nessun riferimento diretto alle classi della App2 (ovvero le dinamiche avvengono sulla base di invocazioni remote sui metodi di una e dell‘altra), allora si può pensare di sostituire App2 con una nuova versione senza che App1 si accorga minimamente della sostituzione (a patto di mantenere le interfacce identiche). Il forzamento sulla sostituzione può avvenire rimovendo il classloader che ha in carico App2 e rilanciando da capo la procedura di caricamento. Il container farà  il resto, ripulendo tutti i reference non più utilizzati e passandoli al garbage collector.

Questa procedura si basa sull‘assunto che due applicazioni separate possano eseguire le invocazioni tramite passaggio di parametri per copia e non per reference. Questo è quanto Sun raccomanda o impone nelle specifiche, ma è anche il modo peggiore dal punto di vista delle performance di realizzare il collegamento fra due applicazioni. Per chi non fosse del tutto convinto della differenza che sussiste fra le operazioni che stanno dietro a un chiamata diretta (passaggio per reference) e una per copia (serializzazione, deserializzazione dei parametri), riporto questo schema di sintesi preso direttamente dalla documentazione JBoss:

Chiamata "by reference"

  1. il chiamante invoca un qualche metodo remoto ejb.someMethod()
  2. l‘invocazione viene inoltrata all‘ejb container
  3. il container esegue la chiamata sul session bean bean.someMethod()
  4. il risultato viene passato al chiamante

Chiamata "by value"

  1. il chiamante invoca un qualche metodo remoto ejb.someMethod()
  2. l‘invocazione è passata al processo di marshalling: i parametri sono convertiti in array di byte e immessi in un ObjectStream.
  3. il container con un altro classloader esegue l‘operazione di unmarshalling: da un array di byte si passa a oggetti - byte[] - > java Objects -
  4. l‘invocazione è propagata all‘interno dell‘EJB container
  5. il container esegue il metodo sul bean bean.someMethod()
  6. il risultato viene nuovamente convertito (marshalled) e poi immesso in un ObjectStream
  7. il risultato viene "unmarshalled" tramite il classloader del chiamante: nuovamente da array di byte si ottengono oggetti
  8. il risultato viene passato al chiamante.

Consci del fatto che le chiamate per copia sono formalmente preferibili ma all‘atto pratico altamente inefficienti, e che spesso il container esegue ottimizzazione trasformando quelle che ufficialmente dovrebbero essere invocazioni per copia in chiamate per reference, in alcuni casi si possono fare alcune ipotesi al fine di limitare il degrado delle prestazioni ma consentire un uso flessibile del sistema.

Ad esempio può capitare che App1 e App2 siano eseguite in contemporanea senza che mai una delle due venga modificata indipendentemente dall‘altra: ogni cambiamento ad App1 implica una modifica anche ad App2 per cui entrambe saranno sostituite e quindi rideployate nello stesso momento. Questa ipotesi si verifica piuttosto spesso in un comune ciclo di vita di una applicazione complessa, quando l‘applicazione o il servizio messo in piedi si basa solamente su App1 e App2. Ã? però quanto mai raro che il parco applicazioni si limiti a un numero costante e prefissato, e anzi spesso alle applicazioni iniziali se ne affiancano di nuove ampliando il set di servizi base.

Le librerie: comuni o personalizzate?

Nasce quindi il problema delle librerie comuni o delle classi che compongono lo zoccolo comune a tutte le applicazioni sviluppate: non è raro infatti che il team di sviluppo di una azienda sviluppi un set di funzionalità  base comune a tutte le applicazioni. Si pone quindi il problema se sia meglio creare un unico package JAR da inserire nella libreria comune di JBoss (la directory lib di cui prima) oppure se predisporre ogni applicazione di una propria versione della libreria. Nel primo caso, al variare del contenuto e della versione della libreria, si potrà  essere sicuri che nel sistema sia installata una sola versione della libreria per tutte le applicazioni, limitando, se non eliminando il problema del proliferare delle versioni, aspetto molto importante per la maggior parte dei capi progetto.

Dall‘altra parte, centralizzare una libreria impone di dover effettuare per ogni modifica alla libreria, un lungo e dispendioso test di tutte le applicazioni che usano tale libreria. Personalmente ho sempre preferito la pulizia e l‘ordine in modo da semplificare il lavoro di controllo e poter in ogni momento stabilire deterministicamente il comportamento del sistema (quindi tramite una sola libreria sotto un ferreo controllo di versioning). In realtà  l‘esperienza insegna come all‘atto pratico sia improponibile tenere sotto controllo le modifiche alle librerie e come in genere i test di compatibilità  siano noiosi, costosi, e difficilmente automatizzabili.

Il consiglio è quindi quello di controllare con attenzione la politica relativa al classloading utilizzata dal container e cercare di isolare ogni applicaizone dalle altre in modo da ridurre le interdipendenze, sia per quello che concerne le chiamate remote fra applicazioni, sia per il collegamento alle classi di libreria.

Un caso comune

Nel documento riportato in [JbossCL] che ha rappresentato la maggior fonte di ispirazione per risolvere i problemi incontrati durante lo sviluppo della nostra applicazione enterprise, sono analizzate in maniera piuttosto complicata le varie casistiche che si possono presentare durante lo sviluppo di applicazioni enterprise; in questa sede concentreremo l‘attenzione a un caso particolare ma piuttosto frequente (che con i dovuti adattamenti potrà  essere applicato alla maggior parte dei casi comuni): si tratta infatti di una applicazione enterprise composta di una parte web, una EJB e che complessivamente fa uso di alcune librerie ipotetiche sviluppate internamente o scaricate da Internet.

Vediamo anzitutto come è configurato il tutto. La porzione che riguarda l‘applicazione web, confezionata in un file WAR, sarà  composta dalle seguenti parti

applicazione web
mokabytelibrary.jar (libreria sviluppata da MokaByte)
somedriver-web.jar (libreria sviluppata da terze parti)

mentre la parte EJB, confezionata in un file JAR, conterrà :

applicazione EJB
mokabytelibrary.jar (libreria sviluppata da MokaByte)
somedriver-ejb.jar (libreria sviluppata da terze parti)

quindi entrambe le parti web e EJB verranno inglobate in un file .ear, come indicato dalle specifiche.

Importante è quindi organizzare in modo corretto le librerie (in particolare mokabytelibrary.jar che viene utilizzata da entrambe le applicazioni) al fine di consentire il funzionamento della applicazione. Come appare ovvio ad una prima valutazione si potrebbe pensare di inserire la mokabytelibrary.jar come libreria di supporto del file .ear, mentre la somedriver-web.jar verrà  inserita nella WEB-INF/lib della web application e la somedriver-ejb.jar in allegato alla applicazione EJB.

Questa organizzazione è presumibilmente la più logica ma in talune situazioni può non funzionare, in particolare se si ha a che fare con un application server che adotta fedelmente la politica di deploy specificata da Sun. Peggio ancora se il server decide di modificare la logica di classloading al variare delle release; ecco cosa è riportato nella documentazione di JBoss:

JBoss makes possible for applications to share classes. JBoss 3.x does that by default. JBoss 4.0 does this for the "standard" configuration, but maintains class namespace isolation between applications for its "default" configuration. JBoss 4.0.1 reverts to the 3.x convention.

Di seguito è riportata quella che allo stato attuale potrebbe essere considerata la "soluzione definitiva al problema del classloader durante la fase di deployment". L‘applicazione di cui prima dovrà  essere organizzata in un file EAR avente la seguente struttura:

enterprise-application.earwebapplication.warejbapplication.jarlib/mokabytelibrary.jarlib/somedriver-web.jarlib/somedriver-ejb.jar

ovvero isolando le librerie utilizzate dai componenti EJB e web application in una directory comune esterna ai file JAR e WAR, ma contenuta nell‘archivio EAR.

Oltre a questo è necessario specificare nella applicazione web e in quella EJB la collocazione di tali librerie (che quindi diventano a tutti gli effetti locali al dominio di esecuzione della applicazione stessa), tramite il file MANIFEST.MF inserito sia nel file WAR della web application che nel JAR della parte EJB; ad esempio dentro il file .war si dovrà  inserire un manifest con il seguente contenuto:

Manifest-Version: 1.0Class-Path: mokabytelibrary.jar lib/somedriver-web.jar

mentre il .jar dovrà  contenere un manifest analogo cosଠfatto:

Manifest-Version: 1.0Class-Path: mokabytelibrary.jar lib/somedriver-ejb.jar

Conclusione

Come si è potuto vedere, il meccanismo di caricamento delle classi è di centrale importanza sia in una JVM semplice che in un container enterprise. Ã? fondamentale che lo sviluppatore (o meglio il deployer seguendo la definizione dei ruoli coinvolti in una applicazione Java EE) ponga la massima attenzione nella fase di produzione dei file di deploy e nella scelta della locazione delle librerie esterne alla applicazione. Per ulteriori informazioni consiglio la documentazione riportata nei riferimenti bibliografici.

Riferimenti bibliografici

[packaging]
Packaging Utility Classes or Library JAR Files in a Portable J2EE Application
http://java.sun.com/j2ee/verified/packaging.html

[JbossCL]
Advanced JBoss Class Loading
http://www.jboss.org/wiki/Wiki.jsp?page=JBossClassLoadingUseCases

[CONF]
ClassLoadingConfiguration
http://www.jboss.org/wiki/Wiki.jsp?page=ClassLoadingConfiguration

Condividi

Pubblicato nel numero
110 settembre 2006
Giovanni Puliti lavora come consulente nel settore dell’IT da oltre 20 anni. Nel 1996, insieme ad altri collaboratori crea MokaByte, la prima rivista italiana web dedicata a Java. Da allora ha svolto attività di formazione e consulenza su tecnologie JavaEE. Autore di numerosi articoli pubblicate sia su MokaByte.it che su…
Ti potrebbe interessare anche