Guida Metodologica Java Best Pratices

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("<");
}
else if (character == '>')
{
pStringBufferResult.append(">");
}
else if (character == '\"')
{
pStringBufferResult.append(""");
}
else if (character == '\'')
{
pStringBufferResult.append("'");
}
else if (character == '\\')
{
pStringBufferResult.append("\");
}
else if (character == '&')
{
pStringBufferResult.append("&");
}
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