MokaByte Numero  44  - Settembre 2000
 
Applicazioni 
stand-alone in Java
Parte seconda
di 
Lavinio Cerquetti
Teoria e pratica di un’applicazione stand-alone in Java

Esaurito nella prima parte di quest’articolo l’argomento delle scelte fondamentali su cui è basato il progetto Irmgard, questo mese esamineremo in dettaglio l’architettura della nostra applicazione, con particolare riguardo al suo framework.

Il framework
La riprova della validità di tutte le scelte sin qui effettuate, per quanto perfettamente giustificabili in sede teorica, non si sarebbe potuta avere che nel corso della realizzazione del framework.
Alla luce dei canoni odierni nello sviluppo del software, ed in misura ancora maggiore all’interno di quei progetti concepiti in seguito ad una precisa e rigorosa fase di modellazione, è innegabile come esso rappresenti la spina dorsale del programma e sia in grado in qualche modo di anticipare tanto quelli che saranno i punti di forza dell’applicazione definitiva quanto le sue debolezze.
Il nostro team era, in questo caso, perfettamente conscio di muoversi in un terreno per gran parte inesplorato: mentre infatti abbondavano le discussioni teoriche e pratiche in materia di framework per l’utilizzo del linguaggio Java lato server, documentazione analoga per il suo utilizzo in applicazioni stand-alone non era assolutamente rintracciabile. Il mondo dello sviluppo sembrava ignorare bellamente la questione, e chi si era già trovato ad affrontarla - quali che ne fossero gli esiti - si guardava bene dal renderne noti i risultati.
La prima scelta focale che ci trovammo davanti fu quella del modello di cui si sarebbe avvalso Irmgard per gestire la base di dati SQL. Identificammo fin dall’inizio due casistiche tipiche di utilizzo delle tabelle:
 
  1. Utilizzo “leggero”, tipicamente associato alle finestre riepilogative e di dettaglio che implementano l’interfaccia di gestione delle tabelle, in cui identificammo come prioritaria la facilità di codifica e design delle maschere di input. In questa modalità di utilizzo, infatti, è l’interazione tra utente e programma ad essere preponderante, sino al punto di coprire eventuali inefficienze del codice, tanto più che la mole di dati scambiati è quasi sempre limitata alla sola riga “corrente”;
  2. utilizzo “pesante”, richiesto dalle procedure batch di aggiornamento e stampa della base dati e - in misura minore - in alcuni particolari scenari dell’interfaccia utente, come quelli legati alla gestione di tabelle con vista gerarchica (master/detail). In questo caso ci trovammo tutti d’accordo nel privilegiare la velocità di gestione dei dati e di esecuzione del codice rispetto alla fase di codifica, ricorrendo a delle stored procedure nelle situazioni di maggior carico relazionale.


Le nostre passate esperienze ci permisero di scartare, velocemente e senza rimpianti, tutte le librerie di gestione dati prodotte da terze parti, vuoi per lacune implementative o prestazionali, vuoi per lo scarso senso di fiducia e solidità trasmessoci dalla più parte dei produttori. Avendo rinunciato sin dal principio anche alle API JBCL, considerate obsolete dalla stessa Borland/Inprise, ci restavano in mano due possibilità: JDBC e dbSwing, vale a dire il set di API, oggetti e componenti orientati alla gestione dei database realizzato da Borland/Inprise per il suo JBuilder 3.0 in sostituzione dell’obsoleto JBCL, e completamente basato su Swing.
Dal momento che sulla reale fattibilità della soluzione dbSwing molti di noi manifestavano delle fondate perplessità, essenzialmente legate a questioni di solidità e performance, stabilimmo che l’unico modo per chiarire completamente ed in maniera definitiva i pro ed i contro di ogni soluzione era quello di implementare dei framework di test, sia per l’utilizzo “leggero” che per quello “pesante”, tanto in JDBC che in dbSwing. I risultati di questa fase preliminare di codifica furono molto interessanti, e ci permisero di identificare chiaramente e con sicurezza le aree preferenziali di utilizzo delle API, dandoci inoltre la certezza di aver trovato una soluzione “allo stato dell’arte” e sufficientemente generale da poter venire applicata anche nei nostri progetti futuri:
 

  1. Il dbSwing si prestava realmente all’utilizzo “leggero” della base dei dati: il componente JdbTable, opportunamente personalizzato ed affiancato a strumenti di navigazione, ricerca e selezione, rappresentava un’ottima soluzione al problema della rappresentazione tabellare degli archivi SQL, così come i componenti orientati ai dati permettevano di realizzare con rapidità form di data-entry intuitivi e funzionali; al contrario, l’utilizzo del JDBC in queste situazioni, a fronte di una maggior complessità di codifica, non sembrava portare vantaggi significativi in termini di performance;
  2. gli scenari “pesanti”, in cui il dbSwing mostrava la corda, venivano invece risolti brillantemente da un approccio basato sul JDBC, sino al punto di rendere inutili nella grande maggioranza delle situazioni le stored procedure, cosa che – en passant – ci ha consentito di aumentare drammaticamente la portabilità dello strato SQL della nostra applicazione;
  3. le prove sul campo ci rivelarono l’esistenza di uno scenario per così dire “ibrido” e legato alla gestione di tabelle particolarmente complesse per numero di colonne o per ricchezza di join: in questi casi il dbSwing, con la sua gestione estremamente automatizzata, sembrava appesantire oltremodo la gestione dei dati. Queste situazioni sono state risolte limitando l’uso del dbSwing alle sole colonne più significative - ossia alle colonne primarie ed alle colonne per cui l’utente può eseguire ordinamenti, ricerche e selezioni – ed utilizzando il JDBC per le colonne rimanenti; la gestione dei relativi form di input in “mixed-mode” non ha presentato particolari problemi tecnici, garantendoci l’atteso guadagno in performance.


Chiarito così il ruolo del dbSwing, restava aperta la questione dell’implementazione del middleware di mappatura tra oggetti e JDBC: quanto l’SQL avrebbe dovuto venire innestato nel programma, e quanto invece avrebbe dovuto venire nascosto negli oggetti Row (del tutto slegati dalle classi ReadRow e ReadWriteRow del dbSwing) che costituivano il punto di accesso dell’applicazione ai dati ?
Nella risposta a questa domanda, come a molte altre questioni sorte durante lo sviluppo di Irmgard, il nostro team dovette accettare un compromesso sensato. Ancora più stringente dei vincoli di manutenibilità ed espandibilità era infatti per noi il requisito dell’usabilità. I test e le ricerche condotti nelle fasi preliminari di design ci avevano fatto conoscere troppi esempi di software in Java strutturalmente perfetti ma praticamente inutilizzabili, e avevano fatto capire a tutti noi che la nostra sfida, ed il successo di Irmgard, non sarebbe consistito nel realizzare un’impalcatura teorica impeccabile ed elegante da trasmettere ai posteri come misura delle nostre conoscenze e della nostra abilità, magnifica ma lontana dal mondo reale come un’opera d’arte in un museo. No, quello che noi volevamo, e che crediamo di essere riusciti a realizzare, è un programma che compensa alcune lacune teoriche, da noi introdotte per scelta al fine di garantire un accettabile livello di performance, con un’utilizzabilità effettiva e reale: Irmgard non è l’ennesimo esempio teorico di programmazione in Java, ma è uno strumento di lavoro.

In sostanza il nostro programma avrebbe utilizzato la metafora object-based delle Row per l’accesso ai dati nella maggioranza dei casi, scavalcandola con un utilizzo diretto dell’SQL solo in presenza di elaborazioni particolarmente pesanti ed onerose.
Un altro compromesso venne raggiunto proprio per quanto riguarda la gestione delle tabelle via dbSwing, accettando il fatto che i form realizzati con queste API prescindessero dal nostro framework JDBC object-based ed utilizzassero direttamente le classi di accesso ai dati di JBuilder, senza per questo creare alcun problema pratico, ma piuttosto aprendo una questione “ideale”, dal momento che le tabelle SQL di Irmgard si trovavano ad avere una sorta di doppia identità, venendo viste dal programma come una collezione di Row ma dal relativo form di gestione tabelle come un DataSet.

Assieme alla gestione dei dati, un punto focale per ogni applicazione basata sul data-entry consiste storicamente nella validazione dell’input dell’utente, per la quale si possono individuare due approcci principali:

  1. Approccio “rigido”, nel quale all’utente non viene permesso lo spostamento da un campo di input ad un altro in presenza di dati non validi: questo approccio fornisce un feedback immediato in presenza di errori, ma può trasformare il lavoro di data-entry in un’esperienza opprimente, in quanto limita in maniera artificiale le possibilità di navigazione dell’operatore, introducendo delle barriere che appaiono tanto più innaturali ed arbitrarie all’interno delle moderne interfacce grafiche, la cui caratteristica precipua sta proprio nella libertà di muoversi e spaziare tra applicazioni e finestre a proprio piacere;
  2. approccio “libero”, nel quale il movimento tra campi è sempre possibile, ed eventuali errori vengono segnalati al momento della conferma definitiva dell’intero form.
La soluzione da noi adottata nei progetti precedenti si poneva a metà fra questi due estremi, e costituiva secondo noi una soluzione elegante e funzionale all’annoso problema della gestione dell’input, che decidemmo di applicare in toto anche al progetto Irmgard.
Per la precisione, il nostro approccio “ibrido” combinava le due soluzioni suddette, rendendo però non bloccanti i messaggi di errore della soluzione “rigida”, i quali venivano visualizzati in un’apposita barra di stato.
Una situazione nuova, e non prevista, fu invece legata alla coesistenza - anche nel medesimo form – dei controlli orientati ai dati, vale a dire dbSwing, con i normali controlli Swing. I primi, infatti, effettuavano una validazione ed una riformattazione dell’input che, seppur molto utile, confondeva l’utente, che si trovava nella situazione di vedere gli stessi dati trattati differentemente se inseriti in un punto del form piuttosto che in un altro. La soluzione consistette nel creare due gerarchie parallele di controlli di input, una orientata ai dati ed una no, le quali implementavano le medesime regole di gestione dell’input (riformattazione delle date inserite dall’utente, visualizzazione di importi valutari con inserimento di punti decimali per aumentarne la leggibilità, e così via), ed utilizzarle in maniera esclusiva all’interno dei form dell’applicazione.
Infine, il cerchio del nostro framework venne chiuso dall’implementazione di un certo numero di classi controller – tipiche di ogni ambiente gestionale – dotate dei comportamenti atti alla gestione ed alla conversione di diversi tipi di dati, quali valori numerici interi e decimali, oggetti java.util.Date, java.sql.Date e java.sql.Timestamp. A queste classi se ne aggiunsero altre necessarie per colmare alcune lacune dello Swing, o per sfruttarne al meglio le potenzialità, fornendo nel contempo un look & feel ed una operatività coerenti al nostro programma – in particolare, ci trovammo a lavorare sui Model delle classi JTree e JTable – nonché una fondamentale helper class per l’accesso a sorgenti di dati JDBC, in grado sia di semplificare ed uniformare la gestione di Statement e ResultSet all’interno di Irmgard, a tutto vantaggio di performance e stabilità, sia di farsi carico in prima persona della gestione dei diversi livelli di API JDBC e delle loro implementazioni da parte dei driver.
 
 
 

La gestione dei BigDecimal e delle colonne BLOB
Il solo fatto che questi argomenti meritino un paragrafo a parte è piuttosto significativo. Infatti, come chiunque abbia esperienza di database SQL non può fare a meno di provare un vago senso di nausea alla sola idea di una query in cui uno dei parametri sia una data o – peggio ancora – una colonna data/ora o timestamp (ed è un vero sollievo scoprire come il JDBC grazie ai PreparedStatement e ad una sintassi “ufficiale” per l’escaping delle espressioni data/ora risolva definitivamente questi problemi), così i componenti del mio team si trovarono ben presto ad essere inseguiti, nei loro sogni, da orde di BigDecimal e colonne BLOB.
Per quanto riguarda i BigDecimal, essi hanno rappresentato per noi una vera sfida, fortunatamente vinta, sia per quanto riguarda la loro gestione all’interno del programma che a livello di SQL. Essi sembrano in effetti possedere una filosofia di funzionamento propria, e non esattamente intuitiva:

  1. Il costruttore BigDecimal(String) - che è senz’altro quello da preferire in quanto non comporta perdite di precisione od errori di arrotondamento – si trova nella difficile situazione di dover inferire la scala del BigDecimal da istanziare alla luce dell’argomento ricevuto. Questo significa, in pratica, che un BigDecimal costruito con new BigDecimal(“0”) ed un altro costruito con new BigDecimal(“0.00”) non si comporteranno allo stesso modo e obbliga ad inizializzare tutti i valori decimali con delle stringhe contenenti dei valori a virgola fissa e con un numero di zeri decimali pari alla precisione specificata nella clausola di dominio della corrispondente colonna DECIMAL a livello di SQL;
  2. in tutte le operazioni su BigDecimal passibili di perdite di precisione è bene specificare sempre la scala desiderata, e – in caso di operazioni con valori monetari - utilizzare ROUND_HALF_UP come modalità di arrotondamento. Inoltre, come è d’altronde prassi in queste situazioni, le operazioni “a rischio” dovrebbero sempre venire eseguite in coda ad una serie di calcoli, per evitare la propagazione degli errori di arrotondamento.
  3. i driver JDBC non allineati alla versione 2 - come quello, già menzionato, di DB2 - richiedono che per ogni operazione con colonne BigDecimal venga esplicitamente fornita la scala della colonna stessa; questa situazione, assieme a quelle citate ai due punti precedenti, rende la gestione dei BigDecimal piuttosto ridondante ed onerosa, costringendo a specificarne la scala praticamente ad ogni utilizzo.
La situazione si presenta invece migliore per i BLOB, sulla cui gestione pratica mi soffermerò brevemente: infatti il gran numero di metodi ad essi associati nelle API JDBC sembra confondere molti sviluppatori. Essi possono comodamente venire letti come stringhe (utilizzando il metodo getString() di un ResultSet) o, qualora essi contengano una mole significativa di informazioni, attraverso i metodi getBlob() e getClob() – il primo per i BLOB propriamente detti, vale a dire per quelli contenenti informazioni binarie, il secondo per i Character BLOB contenenti informazioni testuali – o attraverso le “scorciatoie” getCharacterStream(), getAsciiStream() e getBinaryStream(). In particolare, questi ultimi due metodi sono gli unici utilizzabili in presenza di driver JDBC di livello 1. Per quanto riguarda la loro archiviazione la via più semplice è quella di utilizzare dei PreparedStatement usufruendo dei metodi paralleli setCharacterStream(), setAsciiStream() e setBinaryStream() o, qualora ci si trovi a modificare delle righe già presenti nel database, dei metodi setBlob() e setClob(). Tipicamente i dati da memorizzare veranno inviati al layer JDBC attraverso dei ByteArrayInputStream o – solamente nel caso di Character BLOB ed in presenza di API JDBC di livello 2 - degli StringReader.
Infine, per impostare una colonna BLOB a null l’approccio raccomandato è quello di far ricorso alla chiamata setNull() – sempre associata ad un PreparedStatement – specificando come tipo di dato Types.BLOB per BLOB binari e Types.CLOB per Character BLOB; nel caso di un driver JDBC di livello 1 il tipo da utilizzare sarà invece Types.LONGVARCHAR.
Per onestà devo ricordare come nessuno dei driver JDBC da me conosciuti implementi completamente le API di livello 2, e questo non solamente nella gestione dei cursori updateable o delle estensioni ad oggetti – che, in tutta franchezza, mi sento di definire delle pure illusioni – ma persino nella gestione dei BLOB. Lo stesso driver Informix, che rimane comunque una delle implementazioni più solida ed affidabile, pecca fortemente in questo senso fermandosi ancora alle API JDBC 1.22 e segnalandosi per diversi e gravi bug nella gestione di questo tipo di colonne.
 
 
 

Le stampe
Le applicazioni gestionali sono per tradizione grandi produttrici di stampe cartacee, e – per quanto sia ragionevole pensare di eliminarne una buona parte avvalendosi degli strumenti di condivisione di informazioni disponibili negli attuali sistemi informativi distribuiti – alcuni documenti, per loro stessa natura e a cause di precise motivazioni legali e fiscali, non possono prescindere dalla semplice realtà fisica di un foglio di carta stampata. Dunque anche la nostra applicazione si trovava nella necessità di dover gestire un vasto bagaglio di stampe, alcune delle quali avrebbero dovuto essere eventualmente consultabili on-line. Forti di una prassi consolidata nella loro gestione, messa prima d’allora alla prova in diverse applicazioni native, ed essendo a conoscenza di una generica, intrinseca “debolezza” nelle gestione delle stampe propria del linguaggio di Sun, decidemmo sin dall’inizio di non lavorare direttamente sulle classi del package java.awt.print – tra l’altro estese e rinnovate nel JDK 1.2 – ma di avvalerci di un buon tool di reporting, e cominciammo quindi le nostre ricerche in tal senso. Il mercato offriva un vasto insieme di scelte, più o meno portabili, alcune delle quali dotate di tool visuali per la definizione delle stampe, e molti di questi ambienti di gestione affiancavano alla versione tradizionale del loro prodotto una versione server-based, nella quale le stampe venivano inviate dai client ad un server, e da questo processate e riversate fisicamente in stampa. Il prodotto che ci sembrò più completo e ci colpì in virtù dell’utilizzo per la memorizzazione dei report non di un formato proprietario, ma di un linguaggio XML – cosa che ci faceva ben sperare nell’ottica di future possibili integrazioni - fu il printing tool ReportStyle Pro [1]. Come se ciò non bastasse questo prodotto consisteva in realtà di un insieme di componenti ed API Java, per cui i report potevano venire generati sia a partire da un file XML sia in modalità code-driven, gestendone in sostanza il contenuto in maniera affine ad una sorgente di dati JDBC. Si trattava di una soluzione di un’eleganza e di una purezza estreme, che permetteva di implementare in maniera assolutamente trasparente l’export delle stampe nei più diversi formati, quali i linguaggi HTML e PDF.

Ebbene, alla prova dei fatti la realtà quotidiana dell’utilizzo di questo prodotto si rivelò tristemente negativa. Trattandosi di uno strumento multipiattaforma e Pure Java il suo tool visuale era interamente realizzato in tale linguaggio, ed era di una lentezza e di una instabilità tali da costringerci ben presto a metterlo da parte ed a scrivere direttamente i file di definizione dei report in XML – buon per noi che avessimo scelto un prodotto “aperto” in questo senso. Ma i guai non finirono qui: il modello di reporting di ReportStyle Pro era infatti semanticamente incompleto, e semplici operazioni come la definizione di un header o di un footer standard in report multi-pagina non erano possibili, sia passando attraverso i template XML sia realizzando le stampe avvalendosi delle API Java, e contatti diretti con il servizio di supporto confermarono questa situazione grottesca. In sostanza, ci trovavamo con un tool di reporting che non era in grado di generare una fattura. Se i problemi fossero stati “solo” questi, avremmo semplicemente potuto optare per un prodotto concorrente, ma alcuni altri segnali ci spinsero a rivedere le nostre convinzioni in maniera più radicale. In effetti persino le stampe più semplici si rivelavano nella pratica di una esosità, in termini di risorse, e di una fragilità disarmanti. Stampe di due, tre pagine al massimo provocavano l’invio allo spooler di non meno di 20-30 MB di dati, sufficienti a mettere in ginocchio il client da cui venivano eseguiti, mandando in crash il servizio di spooling se non addirittura l’intera macchina. Quando ciò non accadeva il tempo necessario per l’esecuzione della stampa era ovviamente così elevato da indurre l’utente nella convinzione che, per qualche motivo, la stampa non fosse stata accettata; lo stesso utente, quindi, provvedeva solertemente a rilanciarla, con i risultati che i lettori potranno facilmente immaginare. Purtroppo il responsabile di questa situazione non era ReportStyle Pro, ma lo stesso JDK 1.2, che – in omaggio alla portabilità – si trova costretto a gestire ogni stampa come un’immensa bitmap. Il nostro tool di reporting forniva in realtà dei driver ottimizzati per un certo insieme di stampanti, che riuscivano a ridurre della metà le dimensioni delle richieste inviate allo spooler. Tuttavia essi non potevano rappresentare in alcun modo una soluzione a questi problemi, dal momento che, anche con il loro aiuto, una stampa di 10 pagine superava comunque abbondantemente i 100 MB. Inoltre essi erano disponibili solo per un insieme limitato di stampanti, e comunque solo per il sistema operativo Windows: le workstation Linux avrebbero dovuto continuare ad usare i driver standard del JDK.
In sostanza quest’esperienza ci insegnò che tentare di trasporre in Java la gestione delle stampe, eminentemente client-based, cui ci aveva abituati la piattaforma Windows era semplicemente impossibile. Una nuova fase di analisi e di design ci spinse ad identificare come un paradigma più adatto al linguaggio di Sun quello delle stampe centralizzate, non a caso già implementato da diversi produttori di tool di questo genere. Per evitare brutte sorprese decidemmo di implementare in-house un server di stampa di prova, basato – per guadagnare tempo - sul protocollo RMI e limitato inizialmente alla gestione di stampe in modalità testo. Tale scelta si rivelo sorprendentemente felice, e ci spinse a riprogettare in tale ottica tutto il printing system della nostra applicazione. Oggi Irmgard, secondo una prassi consolidata nel mondo Unix, genera internamente le stampe in formato PostScript che invia in RMI al server di stampa, generalmente una macchina Linux, la quale redirige tale stampa alla coda specificata: il modello di stampante associato, se necessario, converte il codice PostScript [2] nel linguaggio nativo della stampante, tipicamente il PCL – in ambito Unix questo passo viene effettuato dal Ghostscript [3]. Questo approccio, completamente rivoluzionario rispetto a quanto accade nel mondo Windows, ha reso il printing system di Irmgard di una solidità, efficienza, manutenibilità e sicurezza adamantine:
 

  1. La configurazione delle stampanti va eseguita in un unico punto di rete, e non più replicata e tenuta aggiornata presso ogni client;
  2. il sistema di stampa di un server Unix è per sua natura altamente flessibile e scalabile, e permette di associare delle code virtuali a dei gruppi di stampanti – usualmente omogenee - gestendo così in maniera assolutamente trasparente per Irmgard il bilanciamento del carico di lavoro tra un insieme di dispositivi di stampa. Esso è inoltre in grado di gestire in maniera intelligente, sempre senza richiedere passi espliciti da parte di Irmgard, i periodi di inattività o di fermo forzato di tali dispositivi, provvedendo a sospendere, trasferire e riavviare le stampe in funzione della loro disponibilità;
  3. il passaggio di tutte le richieste di stampa attraverso uno snodo centrale consente di effettuare un accounting ed un’archiviazione precisa e puntuale delle stampe stesse, esigenza molto sentita ma non risolvibile in un ambito completamente decentralizzato come quello di Windows. Ed è proprio questo sistema di archiviazione centrale a risolvere brillantemente il problema della consultazione on-line delle stampe: esse vengono memorizzate in uno o più repository, che l’utente può in ogni momento consultare e visualizzare comodamente dall’interno di Irmgard (la visualizzazione di file PostScript è un altro dei servizi offerti dal programma GhostScript, già menzionato in precedenza);


In definitiva, io credo che ognuno di noi abbia sentito dire, più e più volte, che “in Java le stampe non funzionano”. Ebbene, oggi mi sento di poter dire che le cose non stanno veramente così. Semplicemente, la verità è che in Java le stampe non funzionano come in Windows.
 
 
 

Scenario di deployment
Dopo mesi passati a discutere, modellare, sviluppare, testare ed ottimizzare, giunse finalmente il momento di far uscire Irmgard dal proprio mondo ovattato. Come si sarebbe comportato il nostro programma alla prova dei fatti ?
Penso sia facilmente immaginabile l’atmosfera dei giorni immediatamente precedenti il rilascio della prima beta: in quei momenti venne fuori, in maniera chiara come mai era accaduto prima di allora, il carattere di ognuno di noi, ed i pareri sul futuro e sul senso dei nostri sforzi, all’interno del mio stesso team, erano ampiamente discordanti. Tuttavia il solo fatto che io stia scrivendo questo articolo testimonia come, in realtà, Irmgard abbia saputo dare fino ad ora ottima prova di sé.
In un progetto innovativo come il nostro, ed in cui solo in minima parte potevamo avvalerci di esperienze passate, decidemmo di curare particolarmente anche gli scenari di deployment: l’approccio tipico nel mondo Windows, dove l’installazione di un’applicazione costituisce un’altra applicazione a sé stante – per la quale esistono diversi tool, dotati addirittura di specifici linguaggi di scripting – era per noi assolutamente inaccettabile, dal momento che trasforma la gestione degli aggiornamenti del software, in reti di dimensione medio-grande, in un vero e proprio incubo. Dunque, nonostante il mercato offrisse soluzioni di questo genere, in grado di realizzare procedure di installazione multipiattaforma in Java efficienti, esteticamente gradevoli e caratterizzate da una operatività assolutamente analoga a quella cui siamo abituati sotto Windows – cito ad esempio Zero G InstallAnywhere [4] ed InstallShield [5] – scartammo questa possibilità sin dall’inizio, e stabilimmo i seguenti criteri per la nostra piattaforma di deployment:

  1. Punto di aggiornamento unico: esiste uno ed un solo repository per l’applicazione, le sue librerie e tutti i suoi file di supporto. L’aggiornamento di Irmgard, presso tutte le workstation, non deve richiedere nessun’altra operazione oltre l’aggiornamento di detto repository; eventuali operazioni di post-configurazione necessarie, sia presso i client che presso i server, devono venire eseguite automaticamente, senza richiedere passi espliciti agli utenti. Tipicamente, queste operazioni verranno eseguite alla prima esecuzione di una nuova versione di Irmgard;
  2. aggiornamenti remoti incrementali: l’aggiornamento del programma richiede la modifica del repository relativamente alle sole parti dell’applicazione o dei suoi dati di supporto che abbiano subito delle effettive modifiche;
  3. sicurezza e stabilità dell’ambiente di esecuzione: per impedire agli utenti di manomettere l’applicazione, o di comprometterne in qualsiasi modo il corretto funzionamento, tutti i dati di controllo e di configurazione devono essere protetti ed aggiornabili solamente dal personale autorizzato.


Questi tre punti, che rappresentano un’assoluta utopia per chiunque sia uso operare in ambienti Microsoft – e di questo si è resa conto la stessa casa di Redmond, che ha a più riprese tentato di trovare un rimedio ai limiti manifestati da Windows in questo senso, pur senza riuscire a trovare soluzioni veramente efficaci per via della stessa impostazione del suo sistema operativo – si sono rivelati al contrario perfettamente realizzabili in Java. Si noti, en passant, che l’aggiornamento incrementale rappresentava per noi un requisito fondamentale solamente in remoto: vale a dire, esso doveva essere possibile dal sistema del cliente al nostro repository centrale, così da permettere al cliente stesso di effettuare rapidamente aggiornamenti via Internet, mentre non veniva richiesto per l’aggiornamento dal server del cliente alle workstation dei suoi utenti. Infatti quest’ultimo passo, verificandosi in un ambito di rete locale, non richiedeva una gestione egualmente attenta della banda di trasmissione dati. In effetti, durante le nostre accalorate discussioni sui fini e sulle modalità del deployment, una parte del mio team giunse addirittura ad eliminare del tutto il concetto di aggiornamento in locale dell’applicazione dal server alle workstation, affermando che queste ultime avrebbero semplicemente potuto caricare dinamicamente il programma dal server ad ogni esecuzione. Alla luce di questa osservazione, ci trovammo in sostanza a dover valutare due soluzioni al nostro problema:

  1. Deployment server: queste applicazioni, tra le quali possiamo citare il tool di deployment – ormai defunto – realizzato da Borland/Inprise per le sue precedenti versioni di JBuilder ed il nuovissimo Web Start [6] di Sun, gestiscono un repository archiviato in un server di rete. Ogni client che desideri eseguire un’applicazione contatta il deployment server, il quale verifica che il client possegga una copia aggiornata dell’applicazione e di tutti i componenti richiesti al suo funzionamento. Se così è, il client esegue direttamente la propria copia locale dell’applicazione, mentre nel caso contrario esso riceve dal server la versione aggiornata del programma – limitatamente alle sole componenti che necessitano effettivamente dell’aggiornamento – la memorizza nella sua cache locale, e la esegue. Questo approccio, completamente centralizzato, rende possibili una distribuzione ed un aggiornamento assolutamente trasparenti delle applicazioni, e facilita l’esecuzione delle eventuali procedure di post-configurazione, grazie alla presenza di un sistema “intelligente” come il deployment server e di un vero e proprio dialogo con il client. Si noti come in questo scenario persino il runtime Java possa venire considerato alla stregua di un componente dell’applicazione, e quindi assoggettato alla logica dell’aggiornamento automatico, e come ne consegua che sia possibile associare a diverse applicazioni runtime Java specifici, gestendo nella maniera più semplice quelle situazioni ingarbugliate che vengono a crearsi in presenza di software realizzati in momenti differenti e sfruttando ambienti Java non omogenei. Infine questa scelta permette di risolvere con la massima naturalezza i problemi di sicurezza e stabilità, dal momento che il deployment server può controllare la coerenza dell’ambiente di esecuzione del client, forzandolo eventualmente ad un aggiornamento delle procedure o dei dati di configurazione;
  2. file server: l’applicazione, e tutti i dati ad essa associati, sono esportati dal server all’interno della rete locale, ed eseguiti in maniera trasparente dai client. Quest’approccio, enormemente più semplice, comporta tuttavia un tempo di caricamento delle classi Java e dei dati di configurazione maggiori, dal momento che essi vanno sempre cercati in rete, e complica la gestione delle procedure di post-configurazione e di controllo della sicurezza. Venendo infatti a mancare il “componente attivo” rappresentato nella precedente soluzione dal deployment server, tali procedure vanno forzatamente spostate all’interno dell’applicazione.


La soluzione basata su un deployment server, per quanto funzionalmente superiore, presentava il non indifferente ostacolo dei tempi di codifica: infatti nessuna delle soluzioni offerte dal mercato soddisfaceva i nostri bisogni. D’altro canto tutti i test di usabilità che avevamo condotto sin dalle primissime fasi di sviluppo della nostra applicazione, e che continuammo sempre ad eseguire parallelamente al design ed alla codifica, ci avevano fornito dei dati su cui meditare, e che riassumo brevemente:

  1. Un normalissimo Celeron a 366 MHz, con 64 MB Ram ed hard-disk EIDE (una macchina “standard” al tempo del rilascio della prima beta di Irmgard) è in grado di offrire un’esperienza di utilizzo più che accettabile;
  2. la memoria, più ancora che la CPU, mantiene soddisfacente la velocità del programma anche in periodi di prolungato utilizzo (oltre 6 ore consecutive), vero tallone d’achille di tutte le applicazioni basate su Java;
  3. la velocità di risposta di Irmgard si mantiene positiva anche installando un’unica copia dell’applicazione, e di tutti i dati ad essa associati, presso un server di rete;
  4. di più, le performance non decadono anche se le classi costituenti l’applicazione ed il nostro framework vengono archiviate, nel server, non in un file .jar ma in un albero di file .class.
Questi fattori, e specialmente l’ultimo – che in sostanza risolve il problema dell’aggiornamento remoto incrementale - ci convinsero ad utilizzare l’approccio basato su file server per il deployment di Irmgard, perlomeno nelle fasi iniziali. Arrivati ad un sufficiente punto di maturazione dell’applicazione sarebbe stato possibile liberare una parte del team dal lavoro diretto sul programma, ed assegnargli il compito di sviluppare il nostro deployment server.
Per la cronaca, i nostri server sono basati su Linux, ed esportano l’applicazione Irmgard verso tutti i client di rete utilizzando il server Samba [7], soluzione ideale in reti locali eterogenee, e che si fa preferire al classico NFS sia dal punto di vista delle performance che della portabilità.

Infine, non si può parlare di deployment senza citare le numerose implementazioni di JVM disponibili, le cui performance sono state studiate ed analizzate in maniera approfondita ed in funzione delle diverse casistiche di utilizzo. Chi scrive, così come chi ha collaborato e collabora con lui al progetto Irmgard, crede fermamente nel software open-source; logico quindi che la scelta della JVM da utilizzare in questo progetto si sia effettuata tra le implementazioni liberamente disponibili – vale a dire, prive di qualsiasi costo di licensing o royalty – e prediligendo tra queste quelle che si segnalavano per uniformità di comportamento nelle diverse piattaforme, stabilità e velocità. Alla luce di queste considerazioni, le implementazioni di JVM che ci hanno soddisfatto di più sono state quelle di IBM e Sun. La scelta finale è ricaduta su Sun, semplicemente perché delle due è l’unica ad avere implementato la versione 1.2 dell’ambiente Java sulle nostre due piattaforme di riferimento, vale a dire Linux e Windows.
 
 

Il punto della situazione
Concluso lo studio dell’architettura della nostra applicazione non ci rimane che discutere di alcuni particolari scenari di ottimizzazione del linguaggio Java, e dare infine uno sguardo d’insieme a tutto il progetto Irmgard, accennando a quelle che ne saranno le future linee di sviluppo: questi punti verranno tratti nella terza, ed ultima, parte di questo articolo.
 
 

Bibliografia

[1] - ReportStyle Pro: http://www.inetsoftcorp.com
[2] - PostScript: http://www.cs.indiana.edu/docproject/programming/postscript/postscript.html, http://www.tronche.com/web-directory/science-and-technology/computer/languages/postscript
[3] - Ghostscript: http://www.cs.wisc.edu/~ghost
[4] - Zero G InstallAnywhere: http://www.zerog.com/products/products.html
[5] - InstallShield: http://www.installshield.com/java
[6] - Web Start: http://java.sun.com/products/javawebstart
[7] - Samba: http://www.samba.org


Lavinio Cerquetti si occupa di modellazione e sviluppo del software (prevalentemente in Java, C/C++ e Delphi) in ambienti Windows e Unix, concentrandosi sulla programmazione distribuita e di rete. Può essere contattato all’indirizzo di e-mail lavinio@poboxes.com
 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it