FUNZIONE QUALITÀ E SICUREZZA Controllo delle copie Il presente documento, se non preceduto dalla pagina di controllo identificata con il numero della copia, il destinatario, la data e la firma autografa del Responsabile della Qualità, è da ritenersi copia informativa non controllata. Guida Metodologica Java Best Pratices SQC609005 ver. 2 Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 1/33 FUNZIONE QUALITÀ E SICUREZZA Sommario 1 1 Scopo e campo di applicazione 3 2 Riferimenti 2.1 Controllo del Documento: Stato delle revisioni 2.2 Documenti esterni 3 3 3 • Java Performance Tuning (Jack Shirazi; O’reilly; 2001) 3 • Thinking in Java, 3rd Edition (Bruce Eckel, 2003) 2.3 Documenti interni 2.4 Termini e Definizioni 3 3 3 3 Introduzione 3 4 Java Programming Best Practices 4.1 manutenibilità 4.1.1 Scrivere programmi orientati alle persone e non alle macchine 4.1.2 Aumentare la comprensione visuale del codice 4.1.3 Rendere il più possibile espliciti i legami e le dipendenze del codice 4.1.4 Evitare le scorciatoie funzionali. 4.2 Prestazioni 4.2.1 Fare un uso accurato delle risorse condivise 4.2.2 Limitare l’esecuzione di operazioni particolarmente gravose 4.2.3 Eseguire la profilazione del codice come parte del test unitario 4.3 Evoluzione o riuso 4.3.1 Descrivere la soluzione di un problema il più possibile attraverso relazioni tra interfacce specifiche 4.3.2 Nascondere la complessità e chiarire la logica funzionale 4.3.3 Consentire l’applicabilità ed il corretto funzionamento dei singoli moduli in maniera il più possibile indipendente dall’architettura 4.3.4 Evitare il ‘copy & paste’ del codice sorgente (come forma di riuso) 4 4 4 4 4 5 6 6 7 9 11 12 15 17 17 5 Java Server Pages Best Practices 19 5.1 manutenibilità 19 5.1.1 Limitare il codice JSP alla presentazione “pura” e “pulita” 19 5.1.2 Favorire un approccio model2 (web mvc) 20 5.1.3 Mantenere semplice ed intuitivo il modello di navigazione 21 5.1.4 Evitare il ricorso a tag libraries non standard 22 5.2 Prestazioni 22 5.2.1 Limitare i roundtrip sul server 22 5.2.2 Fare un uso accorto della bufferizzazione dei dati delle risposte 23 5.2.3 Tenere basso il numero di informazioni cui accedere o generare dinamicamente ad ogni richiesta utente 24 5.3 Sicurezza 25 5.3.1 Eseguire sempre un “escaping” dell’output costruito dinamicamente 26 6 Servlets Best Practices 29 6.1 Manutenibilità 29 6.1.1 Usare le servlet il meno possibile (appoggiarsi al model2) 29 6.1.2 Evitare di eseguire elaborazioni o generare output direttamente nelle servlet 29 6.1.3 Usare i Servlet Filters per concentrarvi logiche di validazione ed utilità orizzontali e non specifiche alla singola richiesta utente 29 6.2 Prestazioni 30 6.2.1 Mantenere il più possibile un modello stateless e quindi thread safe (senza necessità di sincronizzazione) 31 6.2.2 Usare i metodi di inizializzazione per preparare ed effettuare il caching di risorse costose 31 6.2.3 Usare con moderazione il tracing 31 7 Enterprise JavaBeans Best Practices 7.1 Prestazioni 7.1.1 Minimizzare la durata delle transazioni relative alla logica di persistenza 7.1.2 Velocizzare la localizzazione e l’accesso alle funzionalità remote 7.1.3 Facilitare una gestione dinamica della memoria occupata 31 32 32 32 33 1 Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 2/33 FUNZIONE QUALITÀ E SICUREZZA 1 Scopo e campo di applicazione Lo scopo di questo documento è quello di catalogare un set di buone abitudini (o best practices) che è opportuno seguire nelle varie fasi di progettazione e implementazione del software basato sulla piattaforma J2EE e che dovrebbe quindi essere conosciuto e tenuto in considerazione tanto dagli analisti quanto dai programmatori. Come conseguenza, essendo il tema trattato di natura prettamente tecnica, ovvero corredato nella sua esposizione da diagrammi, codici d’esempio, scenari comuni e casi reali, si ritiene necessario che il lettore possieda uno skill di base sull’OO Analisys e Design e sulla programmazione in Java. 2 Riferimenti 2.1 Controllo del Documento: Stato delle revisioni Vers. 2 Descrizione delle modifiche apportate nella revisione alla versione precedente Revisione Logo Aziendale Cap. modificati N.A. 2.2 Documenti esterni • • • • • NORMA UNI EN ISO 9001:2008 - Sistemi di gestione per la qualità – Requisiti; NORMA UNI EN ISO 9004:2000 - Sistemi di gestione per la qualità - Linee guida per il miglioramento delle prestazioni; NORMA UNI EN ISO 9000:2005 - Sistemi di gestione per la qualità – Fondamenti e terminologia Java Performance Tuning (Jack Shirazi; O’reilly; 2001) Thinking in Java, 3rd Edition (Bruce Eckel, 2003) 2.3 Documenti interni Manuale della Qualità ACI Informatica; Sistema di Gestione per la Qualità vigente. Linee Guida Naming & Coding Conventions per Java (JOAGM01 Ver. 2) 2.4 Termini e Definizioni Per i termini, sigle e acronimi contenuti in questo documento si fa riferimento al Glossario dei termini utilizzati nel Sistema Qualità. 3 Introduzione Java, come qualunque altro linguaggio di programmazione, mette a disposizione di progettisti e programmatori più di uno strumento o di una modalità per raggiungere un determinato scopo. Ciononostante, non ogni possibile soluzione o approccio per la progettazione o l’implementazione del software si può automaticamente considerare una buona opzione. Infatti, quand’anche equivalenti dal punto di vista funzionale, alcune scelte tecniche potrebbero influire sensibilmente e diversamente su aspetti parimenti importanti quali le performance, la leggibilità, l’estendibilità e la manutenibilità di una applicazione, ovvero su tutto l’insieme dei cosiddetti requisiti non funzionali. In questo senso, l’individuazione e la raccolta di casistiche di comprovata utilità tanto nella definizione dell’architettura, quanto nella progettazione e nella realizzazione del software, se documentata nel contesto dei vari scenari di utilizzo e nelle varie finalità considerate, può evidentemente fornire un utilissimo riferimento per la semplificazione dell’intero processo di codifica. Le best practices qui presentate sono appunto questo: buone abitudini che possono, in molti casi, migliorare notevolmente la qualità del codice. Tuttavia sono “solo” buone abitudini, non regole rigide, e pertanto come tali devono essere considerate. Esse non escludono che in alcuni casi sporadici o particolari debbano o possano essere interpretate o applicate parzialmente. Le best practices esposte in questo documento si distinguono sulla base del loro ambito di applicazione nella piattaforma J2EE e, all’interno di questo, nella loro aderenza ad uno specifica problematica o punto di vista. I temi individuati e trattati nel seguito del documento sono: • Java Programming best practices (organizzate per manutenibilità, prestazioni, evoluzione o riuso) • Java Server Pages best practices (organizzate per manutenibilità, prestazioni, sicurezza) • Java Servlets best practices (organizzate per manutenibilità, prestazioni) • Enterprise JavaBeans best practices (organizzate per prestazioni) Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 3/33 FUNZIONE QUALITÀ E SICUREZZA 4 Java Programming Best Practices Le java programming best practices sono buone abitudini di codifica valide in generale e indipendentemente dallo scenario di applicazione. Ciò significa che esse non riguardano una particolare tecnologia della piattaforma J2EE, ma piuttosto il linguaggio java nel senso più semplice del termine. 4.1 manutenibilità Nella produzione del software bisogna rammentare sempre che un programma è scritto una sola volta, ma letto decine di altre. Agevolare il lavoro di chi in futuro manuterrà, evolverà o modificherà il codice è senza dubbio uno dei compiti primari dello sviluppatore ed è pertanto bene che ogni sviluppatore tenga presenti le seguenti indicazioni: • Scrivere programmi orientati alle persone e non alle macchine. • Aumentare la comprensione visuale del codice. • Rendere il più possibile espliciti i legami e le dipendenze del codice. • Evitare le scorciatoie funzionali. Tali indicazioni sono di seguito dettagliate. 4.1.1 Scrivere programmi orientati alle persone e non alle macchine Significa appunto che il codice aumenta il suo valore quando è facilmente comprensibile (tendenzialmente da chiunque). Si deve applicare questa norma rammentando che: • Usare la naming convention rende il codice molto più leggibile e quindi più facilmente manutenibile. • Una classe ben scritta dovrebbe essere autodocumentata. I nomi usati per variabili, attributi e metodi dovrebbero chiarire subito ed in maniera evidente il loro uso e funzionamento rendendo meno indispensabile la lettura della documentazione tecnica. • Essere prodighi di commenti all’interno del codice è un obbligo per chiunque, cosi come separare le sezioni di codice che compiono operazioni differenti (meglio usando più classi o metodi, ma anche lasciando righe vuote all’interno dello stesso metodo). • Un buon programmatore java dovrebbe conoscere ed usare i tag per la generazione dei JavaDoc. • La corretta indentazione del codice rende i programmi decisamente più leggibili; inoltre per agevolare gli sviluppatori alcuni editor (tra cui l’editor Eclipse e quindi anche WSAD) permettono l’indentazione automatica secondo criteri configurabili usando una apposita combinazione di tasti (<CTRL> + <Shift> + <F> su Eclipse/WSAD). 4.1.2 Aumentare la comprensione visuale del codice Significa che il codice dovrebbe rendere immediatamente evidente il proprio significato operativo e funzionale evitando di confondere l’attenzione del lettore. Si deve applicare questa norma rammentando di: • Evitare i “magic numbers”. I “magic numbers” sono il vero incubo di chi deve modificare un codice. Nessuno potrà mai capire, per esempio, che “100” indica “Nuovo Utente”. Al loro posto è sempre opportuno usare costanti con nomi adeguatamente descrittivi. • Dichiarare e usare una variabile solo quando serve. Dichiarare una variabile solo al momento del suo utilizzo e proprio laddove si conosce il primo valore che essa dovrà assumere minimizza la sua visibilità ed il suo ciclo di vita riducendo conseguentemente la possibilità di errori o fraintendimenti. • Non usare la stessa variabile per cose diverse, ma piuttosto dichiarare e inizializzare una nuova variabile ogni volta che serve. 4.1.3 Rendere il più possibile espliciti i legami e le dipendenze del codice Significa documentare le relazioni tra i moduli che compongono il software. Si deve applicare questa norma rammentando di: • Minimizzare l’uso della forma * nelle indicazioni di import. L’utilizzo dell’asterisco (*) per l’importazione dei package rende meno evidenti le dipendenze esistenti tra le classi e per tale ragione il suo uso è scoraggiato. In tal senso alcuni editor (tra cui l’editor Eclipse e quindi anche WSAD) permettono, attraverso precise combinazioni di tasti (su Eclipse/WSAD <CTRL> + <Shift> + <O>), di organizzare automaticamente le classi importate eliminando la forma con l’asterisco. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 4/33 FUNZIONE QUALITÀ E SICUREZZA • • Ricorrere all’uso della forma * nell’import quando vi siano riferimenti ad un vasto numero di classi di un dato package e tale riferimento non abbia valore specifico ma generale. Ad esempio, se si utilizza una libreria java per la gestione dell’XML, nel caso si faccia uso dei diversi oggetti Node, Attribute, Document, Element, ecc. è possibile ricorrere alla forma di import con l’asterisco (*) perché l’informazione di dipendenza è più verso la libreria che non verso questa o quella singola classe. Evitare, laddove possibile, l’uso dei nomi fully dot-qualified. I nomi fully dot qualified sono quelli formati dal nome completo del package della classe. Se da un lato l’uso di tali nomi evita ogni possibile ambiguità, il costo pagato in termini di leggibilità e manutenibilità ne contrasta fortemente l’uso (i nomi fully dot-qualified richiedono inoltre modifiche del codice in più punti se cambia il nome del package). 4.1.4 Evitare le scorciatoie funzionali. Significa non introdurre diversità nel modo di accedere o utilizzare le funzioni o i dati all’interno del codice e favorire invece l’uniformità. Si deve applicare questa norma rammentando di: • Non fare accesso diretto, se possibile, a funzioni che siano parte di un processo logico più vasto. Quando si salta il naturale punto di ingresso di una funzionalità logica complessa (ad esempio una funzionalità di inserimento dati che si componga di tre diverse funzioni chiamate in cascata, una per la logica di autorizzazione, una per la logica di validazione ed una per la logica di esecuzione) e si accede direttamente ad eseguirla solo da un certo punto in poi (ad esempio saltando la parte di autorizzazione ed invocando solo la parte di ‘validazione’ ed ‘esecuzione’) si commette un doppio errore: si rende il codice più esposto a bug e comportamenti anomali e si aumenta il costo di intervento per le future evoluzioni (come quella di una eventuale nuova logica di ‘integrazione’ da eseguire prima della logica di ‘validazione’). • Se presenti, chiamare i metodi setter e getter anche dall’interno di un oggetto. Laddove non si richiamino i metodi setter e getter, quando essi siano presenti, facendo accesso diretto alle variabili membro private, si può comportare il malfunzionamento del software e/o l’introduzione di bug talvolta molto difficili da individuare. Inoltre, la mancata definizione o uso dei metodi setter e getter comporta comunque la perdita della possibilità (o l’aumento del costo) di introdurre controlli efficaci sulle variazioni di stato dell’applicazione. • Evitare di definire proprietà accessibili direttamente (variabili public o protected). Rendere direttamente accessibili le variabili di istanza permette una gestione incondizionata delle stesse dal di fuori della classe, con la conseguente perdita di affidabilità delle informazioni ivi contenute. Del resto, è comunque buona norma che tutti i dati restino ben incapsulati e che se ne consenta l’accesso solo tramite esplicite implementazioni di metodi accessori (setter e getter): questa pratica, infatti, permette di avere un maggior controllo dei valori inseriti sollevando, se è il caso opportune eccezioni. Val la pena ricordare che molti editor (tra cui anche Eclipse e di conseguenza WSAD) agevolano lo sviluppatore nel compito, spesso noioso, di scrivere il codice dei metodi getter e setter fornendo una specifica combinazione di tasti che li genera automaticamente (su Eclipse/WSAD tale combinazione è <Alt> + <Shift> + <S> + la voce Generate Getters and Setters). Esempio public class Man { private Integer m_piAge = null; /** * @return Returns Man's age. */ public Integer getAge() { return m_piAge; } /** * @param piAge The man's new age. * * @throws InvalidAgeException */ public void setAge(Integer piAge) throws InvalidAgeException Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 5/33 FUNZIONE QUALITÀ E SICUREZZA { if (piAge.intValue() < 18 || piAge.intValue() > 65) { throw (new InvalidAgeException(“Age must be between 18 and 65!”)); } else { m_piAge = piAge; } } } 4.2 Prestazioni L’impatto di talune scelte di implementazione sulle prestazioni non è talvolta immediatamente evidente. Differenze di pochi millesimi di secondo nell’esecuzione di un codice rispetto ad un altro potrebbero sembrare accettabili ad una prima considerazione superficiale, ma rivelarsi poi disastrose in uno scenario reale. Il codice scritto bene dovrebbe essere sempre anche un codice ‘performante’. Tuttavia la ricerca delle migliori prestazioni non deve e non può comportare il sacrificio della facilità di manutenzione, evoluzione o comprensione del software. In generale è doveroso tener presenti le seguenti indicazioni: • Fare un uso accurato delle risorse condivise. • Limitare l’esecuzione di operazioni particolarmente gravose. • Eseguire la ‘profilazione’ del codice come parte del test unitario. Eccone il dettaglio: 4.2.1 Fare un uso accurato delle risorse condivise Significa proprio considerare che ogni risorsa condivisa (il tempo macchina, l’accesso al disco, le connessioni alla base di dati, la rete, ecc..) è tale perché costosa, preziosa o limitata. In scenari applicativi multi-utente, l’accesso a tali risorse andrebbe gestito secondo il principio ‘acquire late, release early’, ovvero acquisire il più tardi possibile e liberare il prima possibile (in scenari mono-utente o con un numero fisso e piccolo di utenti potrebbe essere in alcuni casi preferibile sostenere anche il principio contrario). Si deve applicare questa norma rammentando che: • Il codice scritto per essere ‘data-consumer’ dovrebbe preferibilmente essere attivato da una logica a gestione di eventi (triggering) piuttosto che da una azione di controllo continuo (polling). In sostanza se un codice per essere attivato ha bisogno che si produca un determinato cambiamento di stato è preferibile implementare una gestione per cui al cambiamento di stato atteso corrisponda una chiamata al codice piuttosto che una logica in cui sia il codice a controllare periodicamente se si sia verificato il cambiamento di stato atteso. L’uso di una logica di triggering consente infatti di risparmiare tempo macchina e di non perdere eventi senza dover per forza introdurre costosi meccanismi di sincronizzazione. • Le operazioni di I/O dovrebbero essere bufferizzate. In java, il comportamento delle classi più note che gestiscono l’input e l’output è normalmente non bufferizzato. Ciò significa ad esempio, nel caso di I/O su disco, che ogni singolo byte scritto in uno stream è passato immediatamente al sistema operativo e quindi tendenzialmente scaricato sul supporto: come noto, ogni operazione di I/O su disco ha un notevole costo prestazionale per cui tale pratica andrebbe il più possibile evitata. Una soluzione comune a questo problema è utilizzare esplicitamente le classi bufferizzate che java mette a disposizione. Per esempio, sempre nel caso di I/O su disco, piuttosto che le classi InputStream/OutputStream è possibile usare le classi estese BufferedInputStream/ BufferedOutputStream: Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 6/33 FUNZIONE QUALITÀ E SICUREZZA Esempio: public InputStream getInputStream(String sFileName) throws FileNotFoundException { FileInputStream pFileInputStream = new FileInputStream(sFileName); BufferedInputStream pBufferedInputStream = new BufferedInputStream(pFileInputStream); return(pBufferedInputStream); } • • L’uso del tipo stringa java (String) dovrebbe essere evitato per le operazioni di modifica o costruzione dinamica di nuove stringhe. E’ noto infatti che le operazioni di costruzione dinamica o modifica di stringhe che fanno uso del tipo String di java hanno un impatto molto negativo sulle prestazioni di un programma, in particolare sulla memoria ed il tempo macchina. Infatti, l’oggetto String è definito immutabile, ovvero le operazioni di modifica su di esso si traducono sempre nella creazione di un numero variabile di oggetti temporanei e quindi in un elevato uso della memoria, della CPU, del Garbage Collector. Se è necessario costruire o modificare dinamicamente delle stringhe in java è opportuno utilizzare la classe bufferizzata java.lang.StringBuffer avendo cura di inizializzare la dimensione del buffer al valore massimo di caratteri che ci si aspetta di dover trattare. Le risorse acquisite dovrebbero sempre essere esplicitamente rilasciate. E’ opportuno definire sempre, quando si acquisiscono risorse che necessitano di istruzioni esplicite di rilascio, porre il codice necessario al clean-up in un unico metodo ben definito (per esempio ‘dispose’) da invocare al momento opportuno. Tale metodo dovrebbe, all’occorrenza, essere invocato direttamente anche dal metodo ‘finalize’ (che si rende quindi in questo caso necessario implementare) allo scopo di consentire al garbage collector, all’atto della distruzione dell’oggetto, di liberare a sua volta tutte le risorse impegnate qualora esse non siano state liberate in precedenza. E’ inoltre buona norma, volendo considerare risorse anche gli oggetti eventualmente utilizzati, ricordarsi di assegnare esplicitamente il valore null a tutte le variabili oggetto quando esse concludano il loro utilizzo all’interno del programma. Questa pratica (con particolare riferimento agli elementi degli array) permette infatti, sempre al garbage collector, di eseguire il rilascio delle locazioni di memoria non più utilizzate in un modo più efficiente. 4.2.2 Limitare l’esecuzione di operazioni particolarmente gravose Significa cercare di svolgere le operazioni prestazionalmente impegnative solo nei momenti opportuni, cercando cioè il modo di eseguirle in contesti in cui il peso dell’operazione non sia aumentato dalle scelte di codifica. Si deve applicare questa norma rammentando di: • Ricorrere all’uso di collezioni solo quando serve e nella maniera in cui serve. L’accesso ai vari tipi di collezione è considerevolmente più lento e più costoso in termini di memoria e di utilizzo di CPU dell’accesso ad un array, per cui, se possibile, preferire l’uso degli array alle collezioni. Ovviamente ci sono scenari in cui il ricorso a collezioni è indispensabile sia per il tipo di dati trattato (collezioni di elementi eterogenei ad esempio) sia per le modalità con le quali è utile accedere o creare elementi della collezione (accesso per stringa identificativa e aggiunta/rimozione dinamica di elementi ad esempio). Le collezioni che java mette a disposizione sono di due tipi: Collection e Map. Il tipo Collection gestisce gruppi di oggetti (interfacce List e Set, il primo è un insieme generico, il secondo è un insieme che non consente ripetizioni e modifiche), mentre il tipo Map gestisce gruppi di coppie chiave/valore (interfaccia Map). Java fornisce vari oggetti che implementano le funzionalità di List, Set e Map il cui utilizzo ha impatto sulle prestazioni secondo lo scenario di lavoro: Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 7/33 FUNZIONE QUALITÀ E SICUREZZA Interfaccia List Interfaccia Set Vector Lista Thread safe HashSet Stack Lista Thread safe (ottimizzata per accessi LIFO) TreeSet ArrayList Lista non Thread safe (ottimizzata per la lettura e l’iterazione) LinkedList Lista non Thread safe (ottimizzata per la modifica dinamica) • • • Insieme non ordinato (ottimizzato per modifiche e letture puntuali) Insieme ordinato (ottimizzato per iterazioni in cui l’ordine è importante) Interfaccia Map HashMap Collezione non Thread safe Hashtable Collezione Thread safe TreeMap Collezione ordinata (ottimizzata per iterazioni in cui l’ordine è importante) Gestire le eccezioni nella maniera canonica. E’ importante evitare il più possibile l’uso di eccezioni generiche (classe Exception) tanto nei blocchi ‘catch’ quanto nelle istruzioni ‘throw’. In generale non deve preoccupare l’overhead prestazionale aggiunto dall’uso di un blocco try/catch per gestire un’ eccezione: esso è infatti decisamente molto ridotto (perlomeno quando la condizione di eccezione non si verifica - che è poi il comportamento che dovrebbe essere ritenuto più probabile). Tuttavia bisognerebbe evitare di definire blocchi try/catch all’interno di istruzioni di loop (for, while, ecc…) spostando tale gestione, se possibile, al di fuori del ciclo. Evitare di eseguire operazioni onerose nell’uso della memoria o della CPU all’interno delle istruzioni di loop. Occorrerebbe cercare di non eseguire controlli di variabili o chiamate a metodi all’interno delle istruzioni di loop (for, while, ecc…). Egualmente, se possibile, è prestazionalmente conveniente gestire come condizione di uscita dai cicli la comparazione con 0. Strutturare le gerarchie di classi, le composizioni e le aggregazioni ad una profondità non molto elevata. Le gerarchie di classi molto profonde sono decisamente impegnative sia in termini di memoria che in termini di rapidità di inizializzazione: java deve infatti eseguire tutto il codice di costruzione della catena di classi base fino ad arrivare ad Object prima di poter preparare l’istanza dell’oggetto ad ogni esecuzione dell’operatore ‘new’. Le composizioni presentano problemi similari, ma vi aggiungono, come anche le aggregazioni, il costo implicito nell’accesso alle risorse composte (o aggregate). Se non è possibile realizzare aggregazioni o composizioni non troppo profonde, occorre perlomeno procurarsi di non navigare sempre inutilmente tutta la gerarchia di aggregazione o composizione. Esempio: Al codice: public void printMoney(Uomo pUomo) { System.out.println(“\ntotale monete da un euro:” + pUomo.getAbito().getCalzoni().getTasca().getBorsello().getTotaleMoneteDaUnEuro()); System.out.println(“\ntotale monete da due euro:” + pUomo.getAbito().getCalzoni().getTasca().getBorsello().getTotaleMoneteDaDueEuro()); System.out.println(“\ntotale monete da cinque euro:” + pUomo.getAbito().getCalzoni().getTasca().getBorsello().getTotaleMoneteDaCinqueEuro()); System.out.println(“\ntotale monete da dieci euro:” + pUomo.getAbito().getCalzoni().getTasca().getBorsello().getTotaleMoneteDaDieciEuro()); } (20 metodi chiamati per la profondità di aggregazione) E’ di gran lunga preferibile: public void printMoney(Uomo pUomo) { Borsello pBorsello = pUomo.getAbito().getCalzoni().getTasca().getBorsello(); System.out.println(“\ntotale monete da un euro:” + pBorsello.getTotaleMoneteDaUnEuro()); System.out.println(“\ntotale monete da due euro:” + pBorsello.getTotaleMoneteDaDueEuro()); System.out.println(“\ntotale monete da cinque euro:” + pBorsello.getTotaleMoneteDaCinqueEuro()); Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 8/33 FUNZIONE QUALITÀ E SICUREZZA System.out.println(“\ntotale monete da dieci euro:” + pBorsello.getTotaleMoneteDaDieciEuro()); } (8 metodi chiamati nonostante la profondità di aggregazione) 4.2.3 Eseguire la profilazione del codice come parte del test unitario Significa considerare l’uso degli strumenti di profilazione come prassi comune alla fase di test unitario del codice prodotto. L’obbiettivo principale della profilazione è quello di fornire una immagine dell’impatto che ogni singolo metodo ha sulle risorse che il sistema ha assegnato all’applicazione. Questa pratica fornisce quindi una buona indicazione sui cosiddetti ‘colli di bottiglia’ presenti nel flusso applicativo ed è probabilmente lo strumento più potente a disposizione degli sviluppatori per individuare le parti di codice su cui focalizzare lo sforzo di ottimizzazione. Si deve applicare questa norma rammentando che: • E’ sempre opportuno eseguire una sessione di test dell’applicazione usando gli strumenti di profilazione che si hanno a disposizione e controllare la presenza di eventuali criticità relative all’impiego di CPU o alla memoria. I profiler più semplici limitano il loro campo di indagine al campionamento periodico dello stack in modo da rilevare il tempo speso nell’applicazione per l’esecuzione di ogni singolo metodo. Data la loro natura, queste metodologie non sono affidabili al 100%, e soffrono infatti di un certo errore di campionamento. Tuttavia, anche per questi profiler, si può affermare che quando i tempi di esecuzione rilevati per un dato metodo sono nell’ordine dei decimi di secondo (quando quindi è probabilmente necessaria una indagine sulle performance) l’errore di campionamento diventa trascurabile e le indicazioni fornite risultano attendibili. La Java Virtual Machine fornisce nativamente delle opzioni di esecuzione che permettono una semplice profilazione dei metodi. Fermo restando che quello fornito con la virtual machine non è certo al livello dei più noti profiler commerciali, usarlo può servire a rendere l’idea del concetto e della utilità della profilazione. Per attivare il profiler di java è sufficiente specificare l’opzione ‘Xrunhprof:cpu=samples,thread=y’. Esempio Data la classe: public class Test { public static void main(String[] args) { myMethod2(); myMethod(); myMethod3(); } private static void myMethod3() { Object pObject = new Object(); System.out.println("...mondo!"); } private static void myMethod() { for (int iCounter = 0; iCounter < 10000; iCounter++) { Object pObject = new Object(); } System.out.println("Ciao..."); } private static void myMethod2() { for (int iCounter = 0; iCounter < 1000000; iCounter++) { Object pObject = new Object(); } System.out.println("Ciao mondo!"); Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 9/33 FUNZIONE QUALITÀ E SICUREZZA } } Esecuzione Eseguiamo il codice abilitando il profiler con il comando java -Xrunhprof:cpu=samples,thread=y Test Il risultato della profilazione viene registrato in un file di log (java.hprof.txt) del tipo seguente: java.hprof.txt JAVA PROFILE 1.0.1, created Tue May 17 15:59:53 2005 Header for -Xhprof ASCII Output Copyright 1998 Sun Microsystems, Inc. 901 San Antonio Road, Palo Alto, California, 94303, U.S.A. All Rights Reserved. WARNING! This file format is under development, and is subject to change without notice. This file contains the following types of records: THREAD START THREAD END mark the lifetime of Java threads TRACE represents a Java stack trace. Each trace consists of a series of stack frames. Other records refer to TRACEs to identify (1) where object allocations have taken place, (2) the frames in which GC roots were found, and (3) frequently executed methods. HEAP DUMP is a complete snapshot of all live objects in the Java heap. Following distinctions are made: ROOT CLS OBJ ARR root set as determined by GC classes instances arrays SITES is a sorted list of allocation sites. This identifies the most heavily allocated object types, and the TRACE at which those allocations occurred. CPU SAMPLES is a statistical profile of program execution. The VM periodically samples all running threads, and assigns a quantum to active TRACEs in those threads. Entries in this record are TRACEs ranked by the percentage of total quanta they consumed; top-ranked TRACEs are typically hot spots in the program. CPU TIME is a profile of program execution obtained by measuring the time spent in individual methods (excluding the time spent in callees), as well as by counting the number of times each method is called. Entries in this record are TRACEs ranked by the percentage of total CPU time. The "count" field indicates the number of times each TRACE is invoked. MONITOR TIME is a profile of monitor contention obtained by measuring the time spent by a thread waiting to enter a monitor. Entries in this record are TRACEs ranked by the percentage of total monitor contention time and a brief description of the monitor. The "count" field indicates the number of times the monitor was contended at that TRACE. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 10/33 FUNZIONE QUALITÀ E SICUREZZA MONITOR DUMP is a complete snapshot of all the monitors and threads in the System. HEAP DUMP, SITES, CPU SAMPLES|TIME and MONITOR DUMP|TIME records are generated at program exit. They can also be obtained during program execution by typing Ctrl-\ (on Solaris) or by typing Ctrl-Break (on Win32). -------THREAD START (obj=2b57a50, id = 1, name="Finalizer", group="system") THREAD START (obj=2b57b58, id = 2, name="Reference Handler", group="system") THREAD START (obj=2b57c38, id = 3, name="main", group="main") THREAD START (obj=2b59cf0, id = 4, name="HPROF CPU profiler", group="system") THREAD START (obj=2b5ab58, id = 5, name="Signal Dispatcher", group="system") THREAD END (id = 3) THREAD START (obj=2b57c80, id = 6, name="DestroyJavaVM", group="main") THREAD END (id = 6) TRACE 4: (thread=3) <empty> TRACE 2: (thread=3) java.util.zip.Inflater.init(<Unknown>:Native method) java.util.zip.Inflater.<init>(<Unknown>:Unknown line) java.util.zip.ZipFile.getInflater(<Unknown>:Unknown line) java.util.zip.ZipFile.getInputStream(<Unknown>:Unknown line) TRACE 1: (thread=3) java.lang.StringCoding.<clinit>(<Unknown>:Unknown line) java.lang.String.<init>(<Unknown>:Unknown line) java.lang.String.<init>(<Unknown>:Unknown line) TRACE 5: (thread=3) test.myMethod2(test.java:39) test.main(test.java:20) TRACE 3: (thread=3) java.util.zip.Inflater.inflateBytes(<Unknown>:Native method) java.util.zip.Inflater.inflate(<Unknown>:Unknown line) java.util.zip.InflaterInputStream.read(<Unknown>:Unknown line) java.io.DataInputStream.readFully(<Unknown>:Unknown line) CPU SAMPLES BEGIN (total = 13) Tue May 17 15:59:54 2005 rank self accum count trace method 1 76.92% 76.92% 10 5 test.myMethod2 2 7.69% 84.62% 1 1 java.lang.StringCoding.<clinit> 3 7.69% 92.31% 1 2 java.util.zip.Inflater.init 4 7.69% 100.00% 1 3 java.util.zip.Inflater.inflateBytes CPU SAMPLES END La sezione interessante è l’ultima, la quale ci dice che oltre il 76 per cento dell’esecuzione del programma è stato impiegato per eseguire il metodo myMethod2 della classe test (come, in questo caso, era facile aspettarsi anche solo guardando il codice). Chi volesse procedere ad ottimizzare il programma potrebbe quindi iniziare col concentrarsi sul quel particolare metodo. 4.3 Evoluzione o riuso La progettazione del software dovrebbe sempre essere tale da garantire la facilità di evoluzione ed aumentare le possibilità di riuso. Progettare software in ottica di evoluzione o riuso significa andare oltre la semplice soluzione del problema specifico, ma piuttosto astrarre, guardare al caso generale e mettere poi in pratica tutti quegli accorgimenti che sono propri della buona progettazione OO. In generale è doveroso tener presenti le seguenti indicazioni: • Descrivere la soluzione di un problema il più possibile attraverso relazioni tra interfacce specifiche. • Nascondere la complessità e chiarire la logica funzionale. • Consentire l’applicabilità ed il corretto funzionamento dei singoli moduli in maniera il più possibile indipendente dall’architettura. • Evitare il ‘copy & paste’ del codice sorgente (come forma di riuso). Le sezioni seguenti riportano il dettaglio. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 11/33 FUNZIONE QUALITÀ E SICUREZZA 4.3.1 Descrivere la soluzione di un problema il più possibile attraverso relazioni tra interfacce specifiche Significa affrontare il problema per il suo aspetto generale, non per il caso particolare che lo ha posto. Si deve applicare questa norma rammentando che: • Le interfacce disaccoppiano la funzionalità logica dall’implementazione specifica. Concentrarsi sulla funzionalità logica e sul suo reale significato è la strada verso la più facile manutenzione e l’apertura a possibilità d’evoluzione. Se ad esempio si dovesse progettare una applicazione che gestisca la vendita online di dischi con pagamento tramite carta di credito, si dovrebbe considerare il problema nelle sue funzionalità logiche (vendita di oggetti, pagamento) piuttosto che su quelle specifiche (vendita di dischi, pagamento con carta di credito) e modellare tali funzionalità attraverso relazioni di interfacce. Esempio Date le seguenti classi: public class DiscoBean { private private private private private String m_sTitolo; Cantante m_pCantante; String m_sCasaDiProduzione; BraniCollection m_pBraniCollection; Double m_pdPrezzo; public DiscoBean(…,…,…,…,…) { ……………. } public String getTitolo() { return(m_sTitolo); } public Cantante getCantante() { return(m_pCantante); } public String getCasaDiProduzione() { return(m_sCasaDiProduzione); } public BraniCollection getBrani() { return(m_pBraniCollection); } public Double getPrezzo() { return(m_pdPrezzo); } } public class CartaDiCredito { private Intestatario m_pIntestatario; private Date m_peScadenza; public CartaDiCredito(…,…) { ……………. } Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 12/33 FUNZIONE QUALITÀ E SICUREZZA public Intestatario getIntestatario() { return(m_pIntestatario); } public Date getScadenza() { return(m_peScadenza); } public void eseguiPagamento(Double pdPrezzo) throws CdcScaduta, CdcBloccata, CdcNonValida, CdcErrore { …. …. } } Ed il codice di acquisto di un disco: … … … … … … public void acquista(DiscoBean pDiscoBean, CartaDiCredito pCartaDiCredito) { ……………. pCartaDiCredito.eseguiPagamento(pDiscoBean.getPrezzo()); ……………. } … … … … … … Si può notare come l’implementazione, stretta nei requisiti specifici forniti, soffra dal punto di vista della facilità d’evoluzione. Se infatti in seguito alla messa in produzione di questo sistema ci si chiedesse di prevedere anche i pagamenti a mezzo bonifico bancario o la vendita anche di gadget, le modifiche richieste al software sarebbero non banali come il committente tenderebbe a pensare. Il tutto sarebbe stato sicuramente più semplice se il problema fosse stato risolto inizialmente nella sua forma generale ed il software scritto di conseguenza: Esempio Date le seguenti interfacce: public interface OggettoInVendita { public Double getPrezzo(); } public interface SistemaDiPagamento { public void eseguiPagamento(Double pdPrezzo) throws PagamentoException; } e le seguenti classi: public class DiscoBean implements OggettoInVendita { private private private private String m_sTitolo; Cantante m_pCantante; String m_sCasaDiProduzione; BraniCollection m_pBraniCollection; Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 13/33 FUNZIONE QUALITÀ E SICUREZZA private Double m_pdPrezzo; public DiscoBean(…,…,…,…,…) { ……………. } public String getTitolo() { return(m_sTitolo); } public Cantante getCantante() { return(m_pCantante); } public String getCasaDiProduzione() { return(m_sCasaDiProduzione); } public BraniCollection getBrani() { return(m_pBraniCollection); } public Double getPrezzo() { return(m_pdPrezzo); } } public class CartaDiCredito implements SistemaDiPagamento { private Intestatario m_pIntestatario; private Date m_peScadenza; public CartaDiCredito (…,…) { ……………. } public Intestatario getIntestatario() { return(m_pIntestatario); } public Date getScadenza() { return(m_peScadenza); } public void eseguiPagamento(Double pdPrezzo) throws CdcScaduta, CdcBloccata, CdcNonValida, CdcErrore // le eccezioni // qui sopra sono // tutte derivate // PagamentoException { …. …. } } da Il codice di acquisto diventa: Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 14/33 FUNZIONE QUALITÀ E SICUREZZA … … … … … … public void acquista( OggettoInVendita pOggettoInVendita, SistemaDiPagamento pSistemaDiPagamento) { ……………. pSistemaDiPagamento.eseguiPagamento(pOggettoInVendita.getPrezzo()); ……………. } … … … … … … Da cui si può notare come l’implementazione, generica rispetto ai requisiti specifici forniti, non soffra più dal punto di vista della facilità d’evoluzione. Non sarebbe infatti un problema prevedere anche i pagamenti a mezzo bonifico bancario o la vendita di gadget: le modifiche richieste al software sarebbero banali e limitate a poco più della sola implementazione delle nuove classi GadgetBean (implements OggettoInVendita) e BonificoBancario (implements SistemaDiPagamento). • • Nella definizione di una classe è buona norma fornire interfacce differenti per supportare operazioni differenti e non incrementare continuamente una singola interfaccia con metodi per essa impropri. Qualora nell’ottica di generalizzare un sistema o una funzionalità si renda evidente la presenza di un qualche aspetto invariante nella codifica o nell’algoritmo, e quindi valido per tutti i possibili casi particolari, piuttosto che definire un interfaccia si potrebbe prendere in considerazione la possibilità di definire una classe astratta. 4.3.2 Nascondere la complessità e chiarire la logica funzionale Significa mediare la complessità di gestione di un determinato sistema attraverso la definizione di interfacce intermedie di semplificazione. Ciò vuol dire che da una parte occorre implementare tutti quei meccanismi utili a raccogliere la complessità di interazione con il sistema ed esportarla in maniera semplificata, dall’altra occorre anche nascondere ed impedire l’accesso diretto alle parti complesse. Si deve applicare questa norma rammentando di: • Mantenere i membri e metodi privati quanto più possibile. Le variabili membro e i metodi di una classe non definiti privati costituiscono la sua interfaccia di accesso. L’interfaccia di accesso ad una classe dovrebbe essere mantenuta il più semplice possibile. Inoltre, è da considerare come spesso se si modifica una interfaccia si corre il rischio di dover modificare anche tutte le classi che la utilizzano. Esponendo solo i metodi e le proprietà necessarie, lo sviluppatore può permettersi di modificare liberamente tutto quanto ha definito come privato senza preoccuparsi di impedire il funzionamento del codice preesistente. • Fornire metodi costruttori (Factory Method) per evidenziare funzionalità logiche diverse per una classe che accetti più possibilità di costruzione. Scrivere un Factory Method significa codificare un metodo responsabile della costruzione di un determinato oggetto ed usare questo metodo in luogo dell’operatore new (che va di conseguenza inibito). L’uso di un Factory Method può permettere di controllare il processo di istanziazione di un oggetto e di chiarire il significato dei diversi costruttori che esso implementa. Esempio Classe User per la gestione di utenti Anonimi o Autenticati: public class User { private String m_sLogin = null; private String m_sNome = null; private String m_sCognome = null; Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 15/33 FUNZIONE QUALITÀ E SICUREZZA /** * * Costruttore per User Guest. * Il costruttore è dichiarato privato * in modo che non sia accessibile dall’esterno. * */ private User() { super(); } /** * * Costruttore per User Autenticato. * Il costruttore è dichiarato privato * in modo che non sia accessibile dall’esterno. * */ private User(String sLogin, String sNome, String sCognome) { super(); … … … } /** * Primo factory della classe User: utente autenticato * * @param sNome La login dell’utente * @param sNome Il nome dell’utente * @param sCognome Il cognome dell’utente * @return User Rappresentazione dell’utente autenticato. */ public static User newAuthenticatedUser(String sLogin, String sNome, String sCognome) { User pUser = new User(sLogin, sNome, sCognome); return pUser; } /** * Secondo factory della classe User: utente anonimo. * * @return User Rappresentazione dell’utente anonimo. */ public static User newGuestUser() { User pUser = new User(); return pUser; } } Utilizzo Gestione della rappresentazione dello User: // Imposta l’utente autenticato o guest if (logonExecuted()) { m_pUserCurrent = User.newAuthenticatedUser( getLogin(), getNome(), getCognome() ); } else Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 16/33 FUNZIONE QUALITÀ E SICUREZZA { m_pUserCurrent = User.newGuestUser(); } 4.3.3 Consentire l’applicabilità ed il corretto funzionamento dei singoli moduli in maniera il più possibile indipendente dall’architettura Significa considerare che il codice che si scrive potrebbe essere usato anche in scenari applicativi diversi rispetto a quello iniziale. Ovviamente non è possibile scrivere software adatto a tutti gli scenari possibili, ma esistono alcuni scenari di utilizzo che possiamo senza dubbio definire comuni, come la possibilità di persistere (serializzare) le informazioni, di entrare a far parte efficientemente di insiemi e collezioni, di fornire una descrizione rappresentativa dello stato corrente, ecc… Si deve applicare questa norma rammentando di: • Cercare di definire le classi in “forma canonica” per gli utilizzi più comuni, ovvero implementare le interfacce Comparable, Clonable e, dove possibile, Serializable. Includere inoltre le implementazioni particolari dei metodi equals(), hashCode(), toString() e clone(). • Usare preferibilmente long al posto degli int e double al posto dei float. Gli int dovrebbero essere utilizzati solo per compatibilità con i costrutti e le classi Java (per esempio negli indici degli array). Sebbene infatti l’uso di long e double richieda una maggiore allocazione in memoria, è facile notare che in questa maniera gli overflow aritmetici diventano 4 miliardi di volte meno probabili. Similmente è opportuno usare double piuttosto che float. D’altra parte, ove possibile e specie per le variabili membro, è sempre preferibile usare oggetti appartenenti alle classi wrapper in luogo dei tipi elementari. L’utilizzo dei wrapper consente la possibilità di estendere il dominio dei valori disponibili per i tipi semplici per renderli più adeguati a rappresentare le informazioni presenti sulle basi di dati (per via del valore NULL). Questa pratica risulta particolarmente utile nell’implementazione dei Bean (Data Bean o Enterprise Java Bean). Supponiamo, per esempio, che la classe Dipendente sia utilizzata per modellare il dipendente di una ditta commerciale. In questo caso l’uso della classe wrapper Integer per indicare la percentuale sulle vendite dovuta al dipendente, permette di distinguere tra dipendenti che non hanno diritto alla percentuale (quindi con iPercentualeSulleVendite uguale a null) e i dipendenti che pur avendone diritto non hanno raggiunto la soglia minima di vendite e hanno quindi una percentuale riconosciuta pari a zero (iPercentualeSulleVendite.intValue() uguale a 0). 4.3.4 Evitare il ‘copy & paste’ del codice sorgente (come forma di riuso) Bisogna impiegare gli strumenti opportuni, come l’ereditarietà e la composizione, per garantire il riuso del codice. Si deve applicare questa norma rammentando che: • E’ sempre possibile evitare operazioni di ‘copy & paste’ del codice appartenente a funzioni o classi che si vorrebbero riusare ricorrendo alle forme di ereditarietà o a tecniche di composizione. In particolare, in tutti quei casi in cui il riuso tramite l’ereditarietà non è possibile (classi con soli metodi statici, classi final, o problematiche di ereditarietà multipla) o auspicabile (si desidera esporre solo poche funzionalità della classe base), è sempre da considerare l’uso della composizione con inoltro delle richieste (composition & request forwarding). La composizione (composition) consiste nell’includere una istanza di un’altra classe come variabile membro di un'altra, mentre il request forwarding consiste nel trasferire direttamente le chiamate ricevute da un oggetto verso un altro oggetto. Spesso l’uso di composizione e request forwarding insieme è chiamato anche Delegation (anche se la delegation vera e propria presenta anche altri aspetti). Alle volte questa sorta di minimal delegation può risultare una tecnica decisamente più sicura e più comoda rispetto anche all’ereditarietà perché forza lo sviluppatore a pensare a ciascun messaggio che si inoltra. Oltretutto, questo tipo di delegation non obbliga ad accettare tutti i metodi esposti dalla super classe. Esempio Creazione di una estensione della classe statica javax.xml.rpc.holders.StringHolder aggiungendo il metodo public boolean isNull() usando la tecnica di composition e request forwarding. public class MyStringHolder Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 17/33 FUNZIONE QUALITÀ E SICUREZZA { private StringHolder m_pStringHolder = null; public boolean isNull() { if (m_pStringHolder == null) { return true; } else { return false; } } public boolean equals(Object arg0) { return m_pStringHolder.equals(arg0); } public int hashCode() { return m_pStringHolder.hashCode(); } public String toString() { return m_pStringHolder.toString(); } } Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 18/33 FUNZIONE QUALITÀ E SICUREZZA 5 Java Server Pages Best Practices Le Java Server Pages (JSP) best practices sono buone abitudini di codifica valide nell’ambito della progettazione e della implementazione del layer di presentazione di una applicazione web-based. Ciò significa che esse riguardano essenzialmente quella parte della tecnologia J2EE relativa alla produzione di applicazioni web ma limitata alla gestione della sola interfaccia utente. 5.1 manutenibilità La corretta progettazione e codifica delle pagine JSP ha indubbiamente un notevole impatto sulla facilità di manutenzione di ogni possibile applicazione web. E’ dunque importante conoscere e rispettare tutti quei basilari accorgimenti che rendono possibile evitare inutili e dannose complicazioni e, al contrario, favoriscono una maggiore chiarezza e semplificazione. In generale, può risultare utile seguire le seguenti indicazioni: • Limitare il codice JSP alla presentazione “pura” e “pulita”. • Favorire un approccio model2 (web mvc). • Mantenere semplice ed intuitivo il modello di navigazione. • Evitare il ricorso a tag library non standard. Di seguito è riportato il dettaglio. 5.1.1 Limitare il codice JSP alla presentazione “pura” e “pulita” Essenzialmente bisogna ricordare che il codice JSP deve occuparsi solo di preparare la vista utente. In sostanza, la gestione degli eventuali dati ricevuti con le richieste web, la loro validazione o elaborazione, la logica di indirizzamento delle risposte, dovrebbero tutte essere gestite a parte e comunque tenute il più possibile al di fuori delle pagine JSP. Si deve applicare questa norma rammentando di: • Evitare di inserire nelle pagine JSP codice java finalizzato al controllo o all’elaborazione dei dati inviati dall’utente. Più in generale, occorrerebbe ricordare che le pagine JSP non dovrebbero essere mai invocate direttamente per la gestione di richieste complesse (invio di dati da parte del browser, tanto a mezzo URL, con querystring, quanto a mezzo form). Questo vuol dire che è comunque impropria anche la sola e semplice operazione di delega che una pagina JSP potrebbe fare ad altri componenti riguardo alle operazioni di validazione ed elaborazione dei dati. La gestione delle richieste utente dovrebbe essere sempre indirizzata alle Servlet (meglio ad un Controller Servlet stile model2), e le pagine JSP dovrebbero quasi esclusivamente essere da queste richiamate per rappresentare tutte le informazioni e le opzioni prodotte ed individuate per essere date in output all’utente. Esempio di gestione richiesta web semplice Richiesta semplice diretta dal client all’applicazione web (accesso al menu): HTML SUL BROWSER CLIENT HTML SUL BROWSER CLIENT VISUALIZZAZIONE GUESTBOOK JSP ENGINE SERVER WEB <A HREF=”showGuestbook.jsp”>Vai al Guestbook</A> JSP PAGE Esempio di gestione richiesta web complessa Richiesta complessa diretta dal client all’applicazione web (produzione di report): Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 19/33 FUNZIONE QUALITÀ E SICUREZZA HTML SUL BROWSER CLIENT SERVLET ENGINE SERVLET RPT SERVER WEB <A HREF=”getReport.rpt?ID=5”>Stampa il Report</A> LOGICA REPORT JSP ENGINE HTML SUL BROWSER CLIENT VISUALIZZAZIONE REPORT JSP PAGE 5.1.2 Favorire un approccio model2 (web mvc) Bisogna garantire la separazione netta tra le logiche di presentazione, validazione ed elaborazione attraverso l’uso del pattern MVC (Model-View-Controller) adattato alle applicazioni web (Model2). In sostanza rafforzare il concetto che le pagine JSP (view) devono contenere solo logica di presentazione “pura” e “semplice” e che a gestire le richieste deve essere una parte applicativa (controller) in grado di indirizzare le corrette operazioni da compiere (model) per produrre i dati della risposta che esse useranno. In genere, nell’applicare il model2 non si riscrive una sua implementazione totalmente da capo, ma ci si appoggia ad un modello progettuale noto per cui già esistono framework di larga diffusione e successo (jakarta struts, ad esempio). Si deve applicare questa norma rammentando che: • In un architettura model2 la quasi totalità delle richieste web complesse eseguibili attraverso l’applicazione dovrebbe esser gestita dal Controller Servlet. Questo vuol dire che, a parte poche e motivate eccezioni, tutte le opzioni di interazione complessa che l’utente avrà a disposizione per rapportarsi all’applicazione web (quindi tanto le ancore con querystring quanto i target delle form) non dovrebbero essere gestite con puntamenti diretti a JSP, ma al contrario dovrebbero essere indirizzate ad una unica servlet (il Controller) in grado di localizzare ed eseguire (di solito attraverso opportuna configurazione di un file xml che mappi le possibili URI di richiesta ad una sequenza di passi) sia i componenti utili a gestire le eventuali operazioni da compiere per produrre la risposta (model), che le pagine JSP necessarie a visualizzare i dati prodotti (view). Esempio di model2 secondo Jakarta Struts Richiesta complessa diretta dal client all’applicazione web (logon di un utente al sistema): Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 20/33 FUNZIONE QUALITÀ E SICUREZZA Esempio di file di configurazione XML per il Controller Servlet di Jakarta Struts necessario a gestire la richiesta utente di logon al sistema: <action-mappings> <action path="/logon" actionClass="it.aci.struts.example.LogonAction" formAttribute="logonForm" formClass=" it.aci.struts.example.LogonForm" inputForm="/logon.jsp"> <!-Possibili valori logici di ritorno usabili dalla azione LogonAction per indicare il risultato della operazione compiuta. Il risultato logico della azione viene usato dal controller servlet per indirizzare l’opportuna pagina JSP di risposta. --> <forward name="success" path="/mainMenu.jsp"/> <forward name="failure" path="/logon.jsp"/> </action> </action-mappings> 5.1.3 Mantenere semplice ed intuitivo il modello di navigazione Significa tenere presente che le scelte relative ai percorsi di navigazione interni ad una applicazione web hanno un forte impatto sulla complessità di tutta la logica di presentazione e si scontrano spesso con i limiti naturali del sistema web. In generale, occorrerebbe evitare di orientarsi verso l’implementazione di percorsi di navigazione troppo ramificati, preferendo invece una operatività semplice ed essenziale. Si deve applicare questa norma rammentando che: • Una applicazione web è sempre una applicazione Thin Client, ovvero limitata nelle funzionalità esprimibili attraverso la sua interfaccia utente (costituita di norma dal web browser). Pensare percorsi di navigazione complessi entro i quali costringere l’interazione utente comporta spesso, se non sempre, il dover gestire parte della logica necessaria sul server con filter servlets e parte sul client attraverso corposi codici javascript, aumentando conseguentemente il grado di difficoltà nella validazione delle richieste utente. Inoltre, una logica di navigazione complessa s’accompagna di frequente ad uno sforzo aggiuntivo di codifica per limitare o impedire tutte quelle interazioni ed effetti indesiderati che la normale operatività dell’utente sul proprio browser (ad esempio l’uso dei canonici tasti back e forward) potrebbe altrimenti comportare. In sostanza, nel progettare ed implementare il layer di presentazione, è sempre opportuno far riferimento al normale comportamento previsto e conosciuto per la navigazione web classica, ovvero senza costrizioni di percorso, con funzionalità di back & forward implicite, con possibilità di abbandono dell’applicazione da parte dell’utente in qualsiasi momento e senza preavviso. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 21/33 FUNZIONE QUALITÀ E SICUREZZA 5.1.4 Evitare il ricorso a tag libraries non standard Bisogna cercare di attenersi il più possibile all’uso delle librerie di tag standard di java (jstl) e del framework che eventualmente si decide di usare (ad esempio quelle di jakarta struts). L’introduzione nelle pagine JSP di tag customizzati propri o non largamente diffusi può forse da un lato comportare la semplificazione di alcuni procedimenti, ma dall’altro rende sicuramente la comprensione del codice fortemente legata ad una conoscenza specifica non comune, con complicazioni anche sul fronte dei futuri interventi di manutenzione. Si deve applicare questa norma rammentando che: • Esiste una libreria standard di tag JSP per java (java standard tag library – jstl) che fornisce tag per tutte le più comuni problematiche nello sviluppo di pagine JSP. JSTL fornisce tag organizzati nelle categorie di Iteration, Conditionals, Expression language, Text inclusion, Text formatting, XML manipulation, Database access e General functions; la conoscenza e l’uso di jstl non solo velocizza lo sviluppo del layer di presentazione con JSP, ma mette anche al riparo dalla pericolosa tentazione di impiegare del lavoro per scrivere una propria particolare libreria di tag che, nella maggioranza dei casi, si rivelerebbe una copia molto ridotta di quella standard. 5.2 Prestazioni La logica di presentazione è quella sicuramente più esposta alla percezione da parte dell’utente di ogni minimo problema legato alle prestazioni. Scrivere un front-end JSP performante è pertanto una cosa di fondamentale importanza nella costruzione di applicazioni web. In generale, può risultare utile osservare le seguenti indicazioni: • Limitare i roundtrip sul server. • Fare un uso accorto della bufferizzazione dei dati delle risposte. • Tenere basso il numero di informazioni cui accedere o generare dinamicamente ad ogni richiesta utente. Di seguito è riportato il dettaglio. 5.2.1 Limitare i roundtrip sul server Bisogna implementare la logica delle JSP in modo da evitare inutili chiamate aggiuntive al server web. Un caso tipico di questo comportamento si ha quando, per vari motivi, si rimbalzano continuamente i dati forniti dall’utente o prodotti dall’applicazione in sequenze consecutive di richiesta/risposta tra client e server per gestire quella che, nella realtà, sarebbe una unica operazione logica. Si deve applicare questa norma rammentando che: • Laddove si renda indispensabile ricorrere all’uso diretto da parte del codice JSP di funzioni di redirezione (che sono proprie del controller in una architettura mvc/model2), quali, ad esempio, le “forward” e le “redirect”, occorre sempre preferire l’uso di quelle prestazionalmente meno onerose. Ad esempio, proprio le funzioni di forward e redirect, pur simili nella finalità, sono nella soluzione implementativa radicalmente differenti dal punto di vista logico e prestazionale. Quando da una pagina JSP si effettua una chiamata forward, infatti, la pagina target è invocata dal server web attraverso l’esecuzione di un metodo interno, per cui il thread che sta già gestendo la pagina non si interrompe, ma al contrario continua a processare di fatto la stessa richiesta. Il “salto” alla pagina target, ovvero il cambio di contesto operativo, avviene tutto esclusivamente sul server ed il client non è affatto consapevole di questa complessità. Viceversa, quando da una pagina JSP si effettua una chiamata Redirect, l’esecuzione sul server termina immediatamente e si comunica invece al client che per continuare con l’operazione è necessario effettuare una nuova richiesta direttamente alla pagina target. E’ chiaro come nel secondo caso l’operazione di cambiamento di contesto sia totalmente a carico del chiamante che così, ovviamente, non solo viene a conoscenza dei meccanismi interni al disbrigo della funzionalità richiesta, ma ingiustificatamente aumenta il numero di interazioni col server necessarie a gestire quella che nella sostanza è una unica operazione logica. Inoltre, è da notare come lo spezzare una funzionalità logica su due o più richieste http distinte significhi essenzialmente dover in qualche maniera provvedere, data la natura stateless del protocollo http, a condividere le eventuali informazioni necessarie al disbrigo di tale funzionalità tra le diverse invocazioni di pagine (ricorrendo magari o all’uso di querystring nei target di redirezione o, peggio ancora, all’uso di variabili temporanee a visibilità -scope- di sessione). Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 22/33 FUNZIONE QUALITÀ E SICUREZZA Schema di funzionamento delle principali chiamate di redirezione Esecuzione di una richiesta web gestita con una chiamata “forward” ed una richiesta web gestita con una chiamata “redirect”: Need for request data sharing JSP JSP <jsp:forward… • JSP JSP <% … sendRedirect … %> 1 richiesta 2 richieste Client Browser Client Browser Quando la funzione di redirezione sia usata non per gestire un particolare flusso di elaborazione di una richiesta web (il verificarsi di un errore o di una condizione imprevista), ma al contrario nella sua esecuzione naturale e propria, è il caso di chiedersi se non sia più giusto ricorrere all’uso di una chiamata di “include” della pagina target piuttosto che ad una “forward” o ad una “redirect” ad essa. Nel ricorrere ad una chiamata di include in una JSP occorrerebbe comunque, se possibile, cercare di prediligere la modalità di inclusione eseguibile con la apposita direttiva (<%@ include file="targetPage.jsp" %>) e non quella eseguibile con l’apposito tag (<jsp:include page="targetPage.jsp" flush="true" />); questo perché mentre la prima viene eseguita una sola volta alla compilazione della pagina JSP, la seconda aggiunge l’overhead di una esecuzione dell’operazione di inclusione ad ogni richiesta alla pagina. 5.2.2 Fare un uso accorto della bufferizzazione dei dati delle risposte Occorre ricordare che in una comunicazione web l’invio dei dati dal server al client non deve necessariamente avvenire sempre tutto alla fine, quando cioè essi siano stati interamente prodotti, ma al contrario può essere anche eseguito man mano che si procede nella loro produzione. Si deve applicare questa norma rammentando che: • E’ sempre opportuno impostare, attraverso l’apposita direttiva JSP, la dimensione massima del buffered output contenente la risposta (oggetto implicito out) generata dalla JSP per il client. L’impostazione della dimensione massima disponibile per il buffered output regola il comportamento del flushing automatico sulla rete dei dati prodotti dalla JSP durante l’elaborazione di una richiesta (quando il buffer è pieno avviene un flush automatico ed il contenuto disponibile viene inviato al client). E’ di conseguenza importante tarare la dimensione di tale buffer in ragione delle massime dimensioni dei dati che si intendono trattare come unica unità inviabile (flush) o cancellabile (clear) all’interno della elaborazione della singola JSP. Esempio di impostazione della dimensione del buffered output in una pagina JSP <!— imposta un buffer a 12 KB. --> <%@ page session="false" buffer="12kb" %> • E’ possibile richiamare esplicitamente il flushing del buffered output da JSP attraverso il metodo flush dell’oggetto implicito out. Richiamare il metodo flush all’interno di una pagina JSP può essere utile nel momento in cui, ancora non raggiunta la piena dimensione del buffer, si stanno per Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 23/33 FUNZIONE QUALITÀ E SICUREZZA compiere operazioni di particolare lentezza e si desidera mascherare all’utente il tempo di attesa necessario cominciando ad inviare i dati validi prodotti in modo che il suo browser possa cominciare a mostrargli qualcosa. Esempio di chiamata a flush in una pagina JSP <%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %> <html> <head> <title>Flush example</title> </head> <body> Here I am. I’m processing your request.<br/> Be patient.<br/> Result will arrive…..<p/> <% out.flush() %> <c:forEach var="i" begin="1" end="1000" step="1"> processing number: <c:out value="${i}" /> <br /> </c:forEach> <p/> ….Done! </body> </html> 5.2.3 Tenere basso il numero di informazioni cui accedere o generare dinamicamente ad ogni richiesta utente Bisogna mettere in atto tutti gli accorgimenti necessari a limitare il carico d’elaborazione legato al processing del contenuto delle pagine JSP. L’elaborazione di ogni richiesta ad una pagina JSP comporta sempre la fusione di contenuto statico e dinamico per generare la risposta da fornire al client. Massimizzare, ove possibile, la parte gestita staticamente rispetto a quella gestita dinamicamente comporta un notevole guadagno prestazionale. Si deve applicare questa norma rammentando che: • E’ molto spesso possibile eseguire un caching del contenuto statico generato dinamicamente. E’ noto che parte del contenuto logicamente statico di ogni pagina JSP viene prodotto dinamicamente ad ogni esecuzione della JSP stessa. Ovviamente, maggiori sono le operazioni che una JSP ripete ad ogni esecuzione, maggiore è il suo tempo d’elaborazione totale. Occorre cercare di individuare quali parti di codice statico e dinamico generano, dopo la prima esecuzione, sempre lo stesso output, cioè producono in definitiva contenuto statico, e fare in modo di eseguirne un caching (magari sfruttando il metodo init delle JSP) così da velocizzare ogni successiva richiesta. Esempio di caching di contenuto statico generato dinamicamente in una JSP Al seguente codice: <% out.print("<html>”); out.print("<head><title>Hello world</title></head>”); out.print(“<body>”); // Here it will be created some dynamic data ……….. // End of dynamic data generation out.print(“</body>”); Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 24/33 FUNZIONE QUALITÀ E SICUREZZA out.print("</html>”); %> Sarebbe preferibile: <%! char[] header; char[] navbar; char[] footer; char[] otherStaticData; public void jspInit() { //create all the static data here // Note that it would have been better // to initialize the StringBuffer with // some size to improve performance StringBuffer pStringBuffer = new StringBuffer(); pStringBuffer.append("<html>”); pStringBuffer.append("<head><title>Hello world</title></head>”); pStringBuffer.append(“<body>”); header = pStringBuffer.toString().toCharArray(); // here it can be done the same for navbar if its data is static // here it can be done the same for footer if its data is static } // end jspInit() method %> <% out.print(header); out.print(navbar); // Here it will be created some dynamic data ……….. // End of dynamic data generation out.print(footer); %> 5.3 Sicurezza Il front-end di una applicazione web è la parte più direttamente interessata alle tematiche di sicurezza del codice. Per quanto riguarda le pagine JSP, lasciando da parte la validazione ed il controllo dei dati ricevuti in ingresso dall’utente (che, al limite, può avvenire sul client tramite codice javascript, ma che sul server dovrebbe comunque essere operato ad un livello applicativo diverso), occorre prestare particolare attenzione alla corretta costruzione delle risposte. Infatti, una gestione leggera ed inaccurata della parte di preparazione delle viste utente può comportare dei rischi di attacchi XSS (cross site scripting) da parte di utenti malintenzionati. In generale è doveroso tener presente la seguente indicazione: • Eseguire sempre un “escaping” dell’output costruito dinamicamente. Eccone il dettaglio: Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 25/33 FUNZIONE QUALITÀ E SICUREZZA 5.3.1 Eseguire sempre un “escaping” dell’output costruito dinamicamente Occorre ricordare che ogni linguaggio ha le sue parole riservate e che, di conseguenza, anche l’HTML non sfugge a questa regola. In sostanza, ogni pagina JSP generando di norma codice HTML per l’output verso l’utente dovrebbe preoccuparsi di rendere tale output conforme alle regole del linguaggio (escaping dei caratteri non ASCII, dei simboli di contenimento dei tag ‘<’ e ‘>’, ecc…) e non semplicemente presumere tale conformità. Si deve applicare questa norma rammentando di: • Eseguire sempre l’escaping di quanto prodotto dinamicamente da JSP come contenuto dell’HTML da restituire al client. Lo standard JSP, purtroppo, non mette a disposizione funzioni specifiche per l’escaping del contenuto generato (si limita a fornire il supporto all’escaping delle URL) così occorre provvedere manualmente a definire una funzione comune per effettuare tale operazione. Esempio di costruzione dinamica con JSP di contenuto web non sicuro (privo di escaping) Si Consideri la seguente pagina JSP processata al termine dell’invio dei dati per un guestbook: <html> <head> <title>grazie per aver firmato il nostro guestbook</title> </head> <body> Dati inseriti: <p/> Nome: <%= request.getParameter(“nome”)%><br/> Cognome: <%= request.getParameter(“cognome”)%><br/> Commento: <%= request.getParameter(“commento”)%><br/> </body> </html> Se un utente fornisse in ingresso i seguenti dati: Campo Nome Cognome Commento Valore [blank] [blank] <div id=’fake’ style=’position:absolute;left:0;top:0’> <table width=’800’ height=’600’> <tr> <td bgcolor=’white’> sostituzione di pagina </td> </tr> </table> </div> Otterrebbe di cambiare dinamicamente l’aspetto della pagina di risposta. Esempio di costruzione dinamica con JSP di contenuto web sicuro Si Consideri la seguente pagina JSP processata al termine dell’invio dei dati per un guestbook: <%@ include file="htmlEscape.jsp" %> <html> <head> <title>grazie per aver firmato il nostro guestbook</title> </head> <body> Dati inseriti: <p/> Nome: <%= forHTML(request.getParameter(“nome”)) %><br/> Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 26/33 FUNZIONE QUALITÀ E SICUREZZA Cognome: <%= forHTML(request.getParameter(“cognome”)) %><br/> Commento: <%= forHTML(request.getParameter(“commento”)) %><br/> </body> </html> dato il codice della pagina inclusa: <%@ page import="java.util.*" %> <%@ page import="java.text.*" %> <%! public String forHTML(String sData) { StringBuffer pStringBufferResult = new StringBuffer(""); StringCharacterIterator pStringCharacterIterator = null; if (sData != null) { pStringCharacterIterator = new StringCharacterIterator(sData); char character = pStringCharacterIterator.current(); while (character != StringCharacterIterator.DONE ) { if (character == '<') { pStringBufferResult.append("&lt;"); } else if (character == '>') { pStringBufferResult.append("&gt;"); } else if (character == '\"') { pStringBufferResult.append("&quot;"); } else if (character == '\'') { pStringBufferResult.append("&#039;"); } else if (character == '\\') { pStringBufferResult.append("&#092;"); } else if (character == '&') { pStringBufferResult.append("&amp;"); } else { //the char is not a special one //add it to the pStringBufferResult as is pStringBufferResult.append(character); } character = pStringCharacterIterator.next(); } } return pStringBufferResult.toString(); } %> Se un utente fornisse in ingresso i seguenti dati: Campo Nome Cognome Guida Metodologica Java Best Pratices SQC609005 VER. 2 Valore [blank] [blank] Pag. 27/33 FUNZIONE QUALITÀ E SICUREZZA Commento <div id=’fake’ style=’position:absolute;left:0;top:0’> <table width=’800’ height=’600’> <tr> <td bgcolor=’white’> sostituzione di pagina </td> </tr> </table> </div> La pagina di risposta non subirebbe alcuna modifica nell’aspetto. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 28/33 FUNZIONE QUALITÀ E SICUREZZA 6 Servlets Best Practices Le servlets best practices sono buone abitudini di codifica valide nella progettazione e nella implementazione del layer di controllo, validazione e dispatching delle richieste utente verso una applicazione web. Ciò significa che esse riguardano essenzialmente quella parte della tecnologia J2EE relativa alla produzione di applicazioni web ma limitata alla gestione dell’interazione dell’utente con il sistema. 6.1 Manutenibilità Le servlet hanno il loro campo di applicazione nella gestione della logica di interazione dell’utente con l’applicazione web. Di conseguenza esse dovrebbero limitarsi a validare le richieste utente individuandone i gestori per l’elaborazione e la produzione della risposta e non dovrebbero esse stesse contenere tali logiche. Valgono, in questo senso, le seguenti indicazioni: • Usare le servlet il meno possibile (appoggiarsi al model2) • Evitare di eseguire elaborazioni o generare output direttamente nelle servlet. • Usare i Servlet Filters per concentrarvi le logiche di validazione ed utilità orizzontali e non specifiche alla singola richiesta utente. Di seguito è riportato il dettaglio delle indicazioni. 6.1.1 Usare le servlet il meno possibile (appoggiarsi al model2) Bisogna essenzialmente dar spazio all’applicazione del pattern MVC in ambiente web che rende inutile, di fatto, la codifica delle servlet. Si deve applicare questa norma rammentando di: • Usare un framework per l’applicazione del modello web MVC (model2) come jakarta struts. L’uso di un framework “mvc based” consente di disaccoppiare fortemente le richieste utente dai loro gestori e incanala la progettazione e lo sviluppo di una applicazione web verso la netta separazione tra le logiche di validazione, elaborazione e presentazione. 6.1.2 Evitare di eseguire elaborazioni o generare output direttamente nelle servlet Occorre mantenere, anche al di fuori di un modello MVC, una netta separazione dei ruoli tra la logica di validazione e controllo, quella di elaborazione e quella di presentazione. Si deve applicare questa norma rammentando che: • L’oggetto HttpRequest dovrebbe raramente essere usato dalle servlet per controllare gli eventuali parametri inviati dall’utente per il disbrigo di una determinata richiesta. Entrare nel merito del contenuto dell’oggetto HttpRequest nella servlet significherebbe spostarvi impropriamente una parte del codice di elaborazione che dovrebbe essere a carico della specifica azione richiesta. • L’oggetto HttpResponse non dovrebbe mai essere usato dalle servlet per scrivervi contenuto fruibile all’utente. 6.1.3 Usare i Servlet Filters per concentrarvi logiche di validazione ed utilità orizzontali e non specifiche alla singola richiesta utente Bisogna consentire l’interposizione trasparente di servizi comuni ad ogni richiesta utente quali ad esempio il controllo sull’autorizzazione, la compressione dell’output generato, il controllo della cache lato client, ecc. Si deve applicare questa norma rammentando che: • I java servlet filters sono basati sul pattern del ‘chain of responsability’ per cui occorre, nella loro implementazione, fare particolare attenzione all’ordine d’esecuzione nella catena. In effetti, quando un servlet filter decide di bloccare l’esecuzione di una richiesta utente perché magari non conforme a determinate regole di validazione che esso osserva, si impedisce automaticamente a tutti i filtri più in basso nella catena di eseguire per quella richiesta il proprio lavoro (si pensi, ad esempio, ad un filtro interessato al tracciamento dei parametri forniti in post ad ogni richiesta: esso dovrebbe poter tracciare tutte le richieste anche, o soprattutto, se ritenute non valide da altri filtri!). Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 29/33 FUNZIONE QUALITÀ E SICUREZZA • Le logiche di servizio ottimali da implementare tramite servlet filters sono tutte quelle di supporto alla generica applicazione web. Implementare le logiche comuni attraverso filter servlets porta con sé l’indubbio vantaggio di evitare di sporcare il codice di validazione, elaborazione e produzione dell’output per le richieste utente con chiamate a funzioni che, in quanto orizzontali all’applicazione, sono ripetitive e costanti. Esempio di logica per un servlet filter Estratto di un servlet filter per il controllo dell’autorizzazione utente verso la risorsa o l’operazione richiesta: .... public class SecurityFilter implements Filter { public void doFilter(ServletRequest pServletRequest, ServletResponse pServletResponse, FilterChain pFilterChain) throws IOException, ServletException { ... // extract session and uri ... //Get the user's security profile of the user from the session //If the session is invalid, redirect to some login/invalid page. SecurityHelper pSecurityHelperForUser = pSession.getAttribute(SECURITY_PROFILE); ... //Compare the current URI with the access associated with the access //to that URI for the role using the pSecurityHelperForUser. bAccessAllowed = pSecurityHelperForUser.isAccessAllowed(sRequestUri); if(bAccessAllowed) { //If the user is allowed access to the URI, let the flow proceed as normal pFilterChain.doFilter(pServletRequest, pServletResponse); } else { //if the user is not allowed access, you can redirect him to an error page. response.sendRedirect("AccessDenied.jsp"); } } ... } 6.2 Prestazioni Le servlet costituiscono la parte di logica applicativa immediatamente legata all’interazione dell’utente con l’applicazione web. Normalmente, quando un utente interagisce con una applicazione web passa attraverso l’esecuzione di una servlet (il controller servlet nel modello web mvc). Questo da solo può chiarire l’importanza delle considerazioni relative alle prestazioni nella progettazione e nello sviluppo delle servlet. Di conseguenza, nella realizzazione delle servlet, occorre tenere presenti le seguenti indicazioni: • Mantenere il più possibile un modello stateless e quindi thread safe (senza necessità di sincronizzazione). • Usare i metodi di inizializzazione per preparare ed effettuare il caching di risorse costose. • Usare con moderazione il tracing. Di seguito è riportato il dettaglio delle indicazioni. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 30/33 FUNZIONE QUALITÀ E SICUREZZA 6.2.1 Mantenere il più possibile un modello stateless e quindi thread safe (senza necessità di sincronizzazione) Occorre tenere a mente che le servlet sono di default caricate in memoria come singleton, ovvero con una unica istanza atta a gestire parallelamente tutte le richieste utente (anche contemporanee) in modo da realizzare un comportamento che consente un notevole guadagno prestazionale per il web server. Ciò non implica tuttavia che le servlet siano automaticamente thread safe, tutt’altro, ma indica invece la strada da seguire nella loro implementazione: aderire ad un modello di programmazione ‘stateless’, ovvero privo di riferimenti specifici alla conservazione di informazioni di stato per le singole richieste. Si deve applicare questa norma rammentando di: • Mantenere le informazioni di stato per le richieste utente attraverso l’uso dell’apposito oggetto di gestione della sessione (HttpSession). Si dovrebbe in pratica sempre usare l’oggetto HttpSession per mantenere lo stato tra differenti richieste utente. Occorre a tal proposito far anche notare che le specifiche J2EE raccomandano di mantenere in sessione solo oggetti serializzabili. Questa specifica ha la sua motivazione nel voler permettere al servlet container di serializzare le sessioni per gestire servizi come il load balancing o lo swapping in memoria. 6.2.2 Usare i metodi di inizializzazione per preparare ed effettuare il caching di risorse costose Bisogna richiamare il principio per cui si dovrebbe evitare di ripetere inutilmente operazioni particolarmente onerose. Si deve applicare questa norma rammentando di: • Effettuare, laddove possibile, il caching del prodotto di operazioni particolarmente costose ed evitarne la ri-esecuzione. La classe HttpServlet espone un metodo, Init (), per effettuare proprio le operazioni che necessitano di essere svolte una sola volta. Poiché il metodo Init () è invocato automaticamente quando la servlet è istanziata per la prima ed unica volta (si ricorda a tal proposito che essa è gestita, di norma, dal container come un singleton), esso è indubbiamente il posto ideale per eseguire operazioni costose che possono essere svolte anche durante la fase di inizializzazione. I risultati di tali operazioni possono essere mantenuti in cache all’interno di variabili membro della servlet stessa e gestite da questo momento in poi come variabili a sola lettura (read-only), ovvero senza bisogno di strategie di sincronizzazione. 6.2.3 Usare con moderazione il tracing L’attività di tracciamento del flusso applicativo in appositi file di log utili a ricostruire una determinata catena di eventi, più o meno a scopo bug fixing, o allo scopo di rintracciare determinati tentativi di intrusione in un sistema, può comportare un evidente rallentamento nei tempi di risposta di una qualsiasi applicazione web. In generale, quanta più stretta è la cadenza del tracciamento e quanto più ampia la mole dei dati tracciati, tanto maggiore è il ritardo che tale tracciamento causa all’applicazione. Usare il tracing con moderazione vuol dire appunto tenere a mente tutte queste considerazioni. Si deve applicare questa norma rammentando che: • Le applicazioni web devono essere applicazioni il più possibile veloci; per questo occorre minimizzare tutte le operazioni che possono rallentarne l’esecuzione. Il tracing applicativo, essendo fatto essenzialmente con operazioni di scrittura su disco, ha un notevole impatto negativo sulle performance dell’intero sistema. D’altra parte, un buon tracciamento degli eventi può risultare molto utile in fase di bug detection o intrusion detection. Per tale ragione è consigliabile prevedere nella gestione dei tracciamenti la configurabilità di diversi livelli di dettaglio (e legare magari il tipo di log ad una specifica categoria di eventi – info, warning, error, fatal, ecc…). Esistono, per java, diverse librerie di tracing che consentono di regolare i log al livello richiesto e sono ormai considerate uno standard, per cui se ne consiglia l’utilizzo (ad esempio log4j). 7 Enterprise JavaBeans Best Practices Enterprise JavaBeans best practices sono buone abitudini di codifica valide nella progettazione e nella implementazione del layer di elaborazione e memorizzazione di una applicazione di livello enterprise. Ciò significa che esse riguardano essenzialmente quella parte della tecnologia J2EE relativa alla gestione della logica di business e di persistenza delle diverse tipologie di applicazione di una certa complessità (web, web services, ecc…). Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 31/33 FUNZIONE QUALITÀ E SICUREZZA 7.1 Prestazioni Gli Enterprise JavaBeans sono il cuore di una qualsiasi applicazione di livello enterprise. In essi è concentrato, di norma, tutto l’insieme delle operazioni più onerose e critiche sia dal punto di vista della mole dei dati trattati che dal punto di vista delle funzionalità esposte. Di conseguenza l’attenzione alle problematiche relative alle prestazioni deve essere massima per questi componenti. Si possono, quindi, rivelare utili le seguenti indicazioni: • Minimizzare la durata delle transazioni relative alla logica di persistenza. • Velocizzare la localizzazione e l’accesso alle funzionalità remote. • Facilitare una gestione dinamica della memoria occupata. Di seguito è riportato il dettaglio delle indicazioni. 7.1.1 Minimizzare la durata delle transazioni relative alla logica di persistenza Occorre essenzialmente cercare di condurre nel più breve tempo possibile tutte le operazioni che coinvolgono e bloccano i dati. La riduzione del tempo di impegno esclusivo delle informazioni persistibili (realizzata da un RDBMS attraverso i noti meccanismi di lock ad esempio) concorre ad evitare race conditions e a riduce la possibilità di stallo o deadlock. Si deve applicare questa norma rammentando di: • Accedere agli EJB di entità (entity beans) solo attraverso stateless session beans locali. È buona norma evitare di accedere agli entity EJB direttamente dall’esterno. Questi, anzi, dovrebbero essere progettati per prevedere le sole interfacce di accesso locali (LocalHome) e mai quelle remote (RemoteHome). L’incapsulamento della logica di accesso agli entity beans all’interno di EJB stateless di sessione non solo introduce un ottimo livello di indirezione, ma concorre anche a ridurre sensibilmente il numero di remote method calls. Infatti quando un client volesse accedere direttamente ad un EJB di entità, si troverebbe a dover invocare tutti i metodi getter/setter di suo interesse realizzando così una cospicua serie di chiamate remote (che oltretutto allungherebbe il tempo tra le necessarie operazioni di EjbLoad e EjbStore). Al contrario usare un Ejb di sessione stateless consentirebbe di eseguire una unica chiamata remota verso di esso, passandogli in argomento un data bean rappresentativo delle informazioni da impostare sull’Ejb di entità, e gli lascerebbe il compito di invocare, questa volta in locale, la catena di metodi getter/setter ritenuta opportuna. 7.1.2 Velocizzare la localizzazione e l’accesso alle funzionalità remote Bisogna mettere in atto quelle soluzioni utili a non ripetere l’accesso ai servizi di naming per la localizzazione e l’accesso ai componenti EJB che si intende utilizzare. Si deve applicare questa norma rammentando di: • Riusare le EJB home e minimizzare le lookup. Una EJB home è sempre ottenuta dall’application server attraverso una JNDI naming lookup. Questa è un’operazione prestazionalmente molto costosa che vale la pena evitare con una accorta strategia di caching. In questo senso, si potrebbe pensare ad implementare una classe EJBHomeLocatorAndCaching usando il pattern singleton: tale classe avrebbe così il duplice compito di incapsulare la logica di localizzazione ed istanziazione dell’EJB e di garantirne un accesso veloce. • Progettare EJB coarse-graine. Poiché ogni metodo invocato su un EJB remoto è, per l’appunto, una chiamata remota, l’overhead per una interazione fine-graine è sicuramente eccessivo. Per minimizzare tale overhead un EJB dovrebbe rappresentare oggetti con un alto livello di astrazione. Per esempio, mentre la classe CartaDiCircolazione è una buona candidata per diventare un EJB, non lo sono le classi KiloWatt o anche DatiTecnici. Un simile ragionamento può essere inoltre applicato alla definizione dei metodi; progettare un EJB con una interfaccia che espone pochi metodi a grana grossa riduce senz’altro il numero di interazioni tra client ed EJB container e quindi migliora le prestazioni dell’intero sistema. • Accorpare la logica di accesso ad un sistema di classi complesso (siano esse EJB o semplici componenti) utilizzando il pattern facade. Questa tecnica permette di concentrare in un solo EJB remoto un wrapping di un sistema complesso composto da diversi componenti software (EJB o no). L’EJB facade esporrebbe, in luogo della complessità di tutte le singole funzioni dei vari componenti che gestisce, pochi utili metodi, limitando così le chiamate remote a singole chiamate verso il facade e trasformando in chiamate locali tutta la logica complessa di interazione con gli altri componenti. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 32/33 FUNZIONE QUALITÀ E SICUREZZA Esempio di session facade Estratto di un EJB session facade che da accesso ad un sottosistema di EJB: public class Soggetto implements SessionBean { public void setAnni(Integer piAnni) throws EJBException { … } } public class Veicolo implements SessionBean { public void setTarga(String sTarga) throws EJBException { … } } public class SessionFacade implements SessionBean { private Soggetto pSoggetto; private Veicolo pVeicolo; … public void setAnniETarga (String sTarga, Integer piAnni) throws EJBException { pVeicolo.setTarga(sTarga); pSoggetto.setAnni(piAnni); } } 7.1.3 Facilitare una gestione dinamica della memoria occupata Si deve cercare di mantenere il più possibile basso il livello di occupazione della memoria causato da EJB stateful (siano essi stateful session beans o entity beans) garantendo quindi la possibilità di ricorrere a meccanismi come la passivazione e/o la rimozione. Si deve applicare questa norma rammentando di: • Rimuovere gli stateful session bean quando non servono più. Può sembrare una banalità, ma gli stateful session bean rimangono nell’EJB container fin quando non sono esplicitamente rimossi dal client o non scade un timeout (solitamente fissato intorno ai 20 minuti per linearità con la gestione delle sessioni web). Ciò vuol dire che se un client termina la sua sessione di lavoro senza rimuovere esplicitamente l’EJB, questo viene prima passivato (ovvero congelato in uno stato di attesa e messo da parte) e quindi, allo scadere del timeout, rimosso. Queste operazioni costituiscono un netto overhead per l’Application Server che può determinare pesanti cali in termini di prestazioni globali del sistema. Passivare un EJB, del resto, significa proprio, in termini pratici, serializzarlo e salvarlo su disco, quindi una operazione decisamente poco leggera. La rimozione esplicita di un EJB di sessione evita sempre di incorrere in inutili operazioni di IO e contribuisce a minimizzare l’overhead richiesto all’EJB container per la gestione degli EJB. Guida Metodologica Java Best Pratices SQC609005 VER. 2 Pag. 33/33