In questo articolo affrontiamo l‘argomento della gestione delle eccezioni nelle applicazioni enterprise: ovviamente non si tratta di un articolo di base sulle eccezioni, ma piuttosto di un tentativo di fornire spunti e suggerimenti per un corretto approccio al problema.
Introduzione
In questo articolo affrontiamo un argomento spesso piuttosto trascurato nella realizzazione di applicazioni Java enterprise n-layered: la gestione delle eccezioni. Molto dello sforzo di definizione architetturale e di disegno delle applicazioni è dedicato alla scelta dell’architettura applicativa, delle tecnologie e dei framework da utilizzare nei diversi layer e nella definizione dei vari componenti che implementano i diversi livelli applicativi. Anche gli articoli precedenti della serie si sono soffermati su questi aspetti. Molto spesso però non si affronta con la medesima attenzione l’argomento della gestione delle eccezioni che viene trascurato in fase di disegno e lasciato un po’ al caso e alla sensibilità del singolo sviluppatore. Di seguito cercheremo quindi di fornire alcuni spunti sia per far comprendere come la gestione delle eccezioni sia un aspetto molto importante per la qualità del software da realizzare, sia per suggerire alcune modalità pratiche di gestione.
Aspetti di base
Non è nello scopo del presente articolo affrontare in dettaglio i concetti di base della gestione delle eccezioni in Java, che dovrebbero essere ben conosciuti da qualsiasi progettista o sviluppatore con un minimo di esperienza del linguaggio. In maniera estremamente sintetica, è sufficiente ricordare che le eccezioni sono il meccanismo con il quale in Java viene segnalata una situazione di errore verificatasi nell’esecuzione del codice. Le eccezioni in Java si dividono in due categorie principali, le checked e le unchecked.
Le eccezioni di tipo checked devono essere obbligatoriamente gestite dove vengono generate, nel senso che il codice nel quale una eccezione checked può essere generata deve essere inserito in un blocco try/catch, oppure l’eccezione deve essere rilanciata al metodo chiamante mediante la dichiarazione della clausola throws nel metodo.
Le eccezioni di tipo unchecked invece non devono essere gestite obbligatoriamente in un blocco try/catch e non necessitano della definizione della clausola throws nel metodo in cui vengono generate. In [1] è possibile trovare tutti i dettagli necessari alla conoscenza dei meccanismi che il linguaggio fornisce per la gestione delle eccezioni.
Affrontando il problema della gestione delle eccezioni per una applicazione a n-livelli, non solo bisogna conoscere questi meccanismi di base ma bisogna anche pensare a una gestione che sia coerente con i principi di base che portano alla costruzione di una applicazione ben strutturata.
Unire questi principi architetturali con le regole di base per la gestione corretta delle eccezioni non è sempre così facile ed immediato e le soluzioni possono essere diverse in base all’approccio che si preferisce da un punto di vista architetturale.
Il principio di base che secondo me però è sempre valido è che, se non è possibile fare nulla per gestire in qualche modo una eccezione… allora è meglio non gestirla per nulla. Non è raro vedere codice applicativo contenente un mare di righe per la gestione di eccezioni assolutamente irrecuperabili. In genere nei libri sul linguaggio Java si consiglia di utilizzare eccezioni checked per tutte le situazioni in qualche modo recuperabili ed eccezioni unchecked per le altre. In realtà la distinzione non è così netta e spesso non viene sottolineato come da un punto di vista funzionale i due tipi di eccezioni sono assolutamente equivalenti nel senso che anche una eccezione unchecked può essere gestita.
Il mio consiglio in questo caso è di utilizzare sempre eccezioni unchecked e di decidere se gestirle o meno a seconda della situazione. Le eccezioni unchecked consentono di scrivere codice privo di inutili blocchi try/catch e permettono la definizione di metodi nei quali non è necessario dichiarare una marea di eccezioni, cosa molto utile nella definizione di interfacce poiche’ preserva la struttura del metodo in caso di cambiamento di implementazione.
Si potrebbe giustamente obiettare che, nella scrittura di un’applicazione, abbiamo sempre a che fare con librerie di terze parti e che quindi potremmo sempre essere nella situazione di utilizzare un metodo di una classe scritta da qualcun altro che rilancia una eccezione di tipo checked. In questo caso non abbiamo scelta, ovviamente, ma possiamo ricorrere alla tecnica del wrapping della eccezione per fare in modo da non dover inserire blocchi try/catch o clausole throws.
Effettuare il wrapping di una eccezione significa semplicemente catturare una eccezione e incapsularla in un’altra di differente tipo come nel semplice codice seguente:
try{ object.method(); } catch (Exception checkedException) { throw new MyException("exception", checkedException); }
In questo caso l’eccezione checked rilanciata dal metodo method dell’oggetto object viene wrappata in una eccezione unchecked MyException che viene rilanciata dal metodo. Il metodo chiamante non è costretto ad inserire blocchi try/catch o clausole throws, ma può decidere se gestire o meno l’eccezione.
Applicazioni a n livelli
Arrivati a questo punto della serie sappiamo cosa si intenda per applicazione a n-livelli e dovremmo avere ben chiari i principi architetturali alla base di una buona progettazione di applicazioni enterprise complesse. Uno di questi principi sui quali ci siamo soffermati spesso è quello della separazione dei livelli applicativi.
Abbiamo sempre insistito sul fatto che una buona applicazione dovrebbe essere strutturata in modo che un layer venga esposto al layer adiacente esclusivamente mediante interfacce , ovvero il layer adiacente deve essere assolutamente indipendente dall’implementazione dell’altro layer e non deve avere alcuna conseguenza dal cambiamento di quest’ultimo. Questo principio dovrebbe rimanere valido anche nel caso delle eccezioni anche se a volte ciò contrasta con la semplicità di scrittura dell’applicazione. Detto in maniera generale ogni layer dovrebbe gestire le proprie eccezioni al suo interno, e non propagarle agli altri strati applicativi.
Un approccio quindi potrebbe essere il seguente, facendo sempre l’ipotesi di una applicazione a tre livelli presentation-service-persistance implementata con Spring, Hibernate e un web framework come Struts o JSF.
Lo strato di persistenza ingloba in wrapping le sue eccezioni in una DAOException che è l’unica che comunica al layer di business:
public Object executeMethod() throws DAOException { try { HibernateTemplate hTemplate = getHibernateTemplate(); hTemplate.executeMethod(); } catch (DataAccessException dae) { throw new DAOException("executeMethod() " + dae.getMessage(), dae); } return object; }
Il layer di business segue lo stesso approccio facendo il wrapping delle eccezioni in una ServiceException:
public Object executeMethod() throws ServicesException { Object object = null; try { object = dao. executeMethod(); } catch (DAOException de) { throw new ServicesException("Exception ", de); } return object; }
Ovviamente questa è una semplificazione in quanto in ogni layer si potrebbe definire una gerarchia di eccezioni per consentire di distinguere tra le varie situazioni di errore. In questo caso DAOException e ServiceException sarebbero le eccezioni in cima alla gerarchia di ciascun layer, e avrebbero una serie di altre eccezioni gerarchicamente definite sotto di esse.
A questo punto l’eccezione dopo aver viaggiato tra i vari layer arriva al layer di presentation che deve obbligatoriamente gestirla.
Infatti la cosa da evitare sempre è di mostrare all’utente uno stacktrace conseguente a una eccezione, sia perche’ l’utente non è in grado di comprendere l’informazione visualizzata, sia perche’ questo dà l’impressione di una applicazione poco solida e di scarsa qualità.
A livello di presentation la scelta migliore è gestire in un punto univoco mediante un’opportuna classe handler tutte le eccezioni presentando all’utente opportuni messaggi di errore.
In Struts questo è molto semplice in quanto può essere fatto esternamente al codice sfruttando gli handler forniti dal framework. In [2] è presentato un esempio di gestione dichiarativa delle eccezioni con Struts.
In base a quanto detto quindi si definisce per ogni layer una gerarchia di eccezioni di tipo unchecked e le eccezioni vengono gestite a seconda delle situazioni nel singolo layer ma per l’esterno vengono definite interfacce che dichiarano solo l’eccezione base della gerarchia. Ciò consente di arricchire in ogni layer la gestione delle eccezioni a piacimento, wrappando anche eccezioni checked rilanciate da classi di librerie di terze parti utilizzate nel layer. Il layer adiacente in ogni caso è indipendente dalla aggiunta di nuove eccezioni nel layer sottostante o dall’utilizzo di librerie che dichiarano eccezioni checked ma può in ogni caso gestirle, se ne ha in qualche modo bisogno, effettuando l’unwrapping.
In questo modo, le interfacce dei vari layer rimangono stabili: non è necessario inserire inutili blocchi try/catch ne’ effettuare l’unwrapping delle eccezioni quando non è necessario gestirle. Questo approccio è valido sia quando si ha il controllo di tutti i layer applicativi, sia quando il layer applicativo deve essere utilizzato da un layer sviluppato da altri.
Riassumendo: siamo partiti da due semplici principi, uno riguardante le eccezioni e uno riguardante le applicazioni ad n-livelli:
- È inutile gestire una eccezione per la quale non possiamo fare nulla
- I livelli applicativi devono essere indipendenti l’uno dall’altro e comunicare solo attraverso interfacce
A partire da questi due semplici principi e tenendo presente i concetti di base della gestione delle eccezioni in Java abbiamo suggerito l’utilizzo di eccezioni unchecked e della tecnica del wrapping delle eccezioni checked.
Da ciò abbiamo enunciato gli elementi di base per la gestione delle eccezioni nei layer applicativi:
- Definire una gerarchia di eccezioni unckeched in ogni layer
- Wrappare con una eccezione della gerarchia le eccezioni checked generate nel layer
- Gestire all’interno del layer quando ciò è necessario le eccezioni
- Propagare al layer adiacente l’eccezione base della gerarchia definendola nell’interfaccia
- Gestire l’eccezione in un altro layer se necessario effettuando l’unwrapping della stessa
- Gestire sempre le eccezioni nel layer di presentation con un opportuno handler non propagandola mai all’utente finale
Questi sono principi di base che possono fungere da guida nella implementazione della gestione delle eccezioni in una applicazione ad n-livelli. Chiaramente questo non è l’unico approccio ma è in linea con i principi architetturali seguiti fino ad ora nello sviluppo della serie.
Va sempre tenuto presente che saranno sempre presenti situazioni particolari che richiedono soluzioni ad-hoc che magari si allontanano molto da quanto detto. Però, enunciare principi di base da seguire è sempre una cosa utile e necessaria per un corretto approccio alla realizzazione di applicazioni complesse.
Conclusioni
Nel presente articolo abbiamo trattato l’argomento della gestione delle eccezioni nelle applicazioni Java enterprise a n-livelli. Non abbiamo trattato nello specifico la gestione delle eccezioni di singoli framework sia perche’ sarebbe impossibile in un solo articolo sia perche’ abbiamo preferito enunciare principi di base validi indipendentemente dalla implementazione scelta per un singolo layer applicativo. L’articolo può essere preso come spunto per linee guida da seguire: l’enunciazione di questi principi è vista nell’ottica di mantenere fede alla regola di base della separazione dei livelli applicativi senza però trascurare un approccio semplice e non ridondante nella trattazione delle eccezioni.
Riferimenti
[1] AA.VV., “The Java Tutorials. Trail: Essential Classes. Exceptions”, 1995-2008, Sun Microsystems
http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html
[2] Alfredo Larotonda, “Sviluppare applicazioni J2EE con Jakarta Struts – VI parte: la gestione delle eccezioni”, MokaByte 86, Giugno 2004