14. Applicazione web MVC in un'architettura a 3 livelli – Esempio 1
14.1. Introduzione
Fino a questo punto, ci siamo limitati a esempi destinati a scopi didattici. Per questo motivo, dovevano essere semplici. Ora presentiamo un'applicazione di base che è comunque più ricca di funzionalità rispetto a qualsiasi altra presentata finora. Sarà unica in quanto utilizza i tre livelli di un'architettura a 3 livelli:

Si invitano i lettori a rivedere i principi di un'applicazione web MVC in un'architettura a tre livelli nella Sezione 4, qualora li avessero dimenticati.
L'applicazione web che scriveremo ci permetterà di gestire un gruppo di persone utilizzando quattro operazioni:
- elenco delle persone nel gruppo
- aggiungere una persona al gruppo
- modifica di una persona nel gruppo
- rimuovere una persona dal gruppo
Queste sono le quattro operazioni di base su una tabella di database. Scriveremo due versioni di questa applicazione:
- Nella versione 1, il livello [DAO] non utilizzerà un database. I membri del gruppo saranno memorizzati in un semplice oggetto [ArrayList] gestito internamente dal livello [DAO]. Ciò consentirà al lettore di testare l'applicazione senza i vincoli di un database.
- Nella versione 2, inseriremo il gruppo di persone in una tabella del database. Dimostreremo che ciò può essere fatto senza influire sul livello web della versione 1, che rimarrà invariato.
Le seguenti schermate mostrano le pagine che l'applicazione scambia con l'utente.



![]() |
![]() |
14.2. Il progetto Eclipse
Il progetto dell'applicazione si chiama [people-01]:

Questo progetto copre i tre livelli dell'architettura a tre livelli dell'applicazione:
![]() |
- il livello [dao] è contenuto nel pacchetto [istia.st.mvc.personnes.dao]
- il livello [business] o [service] è contenuto nel pacchetto [istia.st.mvc.personnes.service]
- il livello [web] o [ui] è contenuto nel pacchetto [istia.st.mvc.personnes.web]
- il pacchetto [istia.st.mvc.personnes.entities] contiene oggetti condivisi tra diversi livelli
- il pacchetto [istia.st.mvc.people.tests] contiene i test JUnit per i livelli [DAO] e [service]
Esploreremo i tre livelli [dao], [service] e [web] uno dopo l’altro. Poiché scrivere richiederebbe troppo tempo e leggere potrebbe risultare troppo noioso, a volte potremmo passare rapidamente attraverso le spiegazioni, tranne quando il materiale presentato è nuovo.
14.3. Rappresentazione di una persona
L'applicazione gestisce un gruppo di persone. Le schermate nella Sezione 14.1 mostravano alcune delle caratteristiche di una persona. Formalmente, queste sono rappresentate da una classe [Person]:
![]()
La classe [Person] è la seguente:
- Una persona è identificata dalle seguenti informazioni:
- id: un identificatore univoco per una persona
- last_name: il cognome della persona
- firstName: il suo nome
- dateOfBirth: la data di nascita
- maritalStatus: se è sposata o meno
- nbChildren: il numero di figli
- L'attributo [version] è un attributo aggiunto artificialmente ai fini dell'applicazione. Da una prospettiva orientata agli oggetti, sarebbe stato probabilmente preferibile aggiungere questo attributo a una classe derivata da [Person]. La sua necessità diventa evidente quando si considerano i casi d'uso dell'applicazione web. Uno di questi casi d'uso è il seguente:
Al momento T1, l'utente U1 inizia a modificare una persona P. A questo punto, il numero di figli è 0. U1 cambia questo numero in 1, ma prima di convalidare la modifica, l'utente U2 inizia a modificare la stessa persona P. Poiché U1 non ha ancora convalidato la propria modifica, U2 vede il numero di figli come 0. U2 cambia il nome della persona P in maiuscolo. Quindi U1 e U2 salvano le loro modifiche in quell'ordine. La modifica di U2 avrà la precedenza: il nome sarà in maiuscolo e il numero di figli rimarrà a zero, anche se U1 crede di averlo cambiato in 1.
Il concetto di versione di una persona ci aiuta a risolvere questo problema. Rivediamo lo stesso caso d'uso:
Al momento T1, un utente U1 inizia a modificare una persona P. In questo momento, il numero di figli è 0 e la versione è V1. Cambia il numero di figli in 1, ma prima di confermare la modifica, un utente U2 entra in modalità di modifica per la stessa persona P. Poiché U1 non ha ancora confermato la modifica, U2 vede il numero di figli come 0 e la versione come V1. U2 cambia il nome della persona P in maiuscolo. Quindi U1 e U2 salvano le loro modifiche in quell'ordine. Prima di salvare una modifica, verifichiamo che l'utente che modifica la persona P abbia la stessa versione di quella attualmente salvata della persona P. Questo sarà il caso dell'utente U1. La sua modifica viene quindi accettata, e cambiamo quindi la versione della persona modificata da V1 a V2 per indicare che la persona ha subito una modifica. Nel convalidare la modifica di U2, noteremo che ha la versione V1 della persona P, mentre la versione attuale è V2. Possiamo quindi informare l'utente U2 che qualcun altro ha agito prima di lui e che deve partire dalla nuova versione della persona P. Lo farà, recupererà una versione V2 della persona P che ora ha un figlio, scriverà il nome in maiuscolo e convaliderà. La sua modifica sarà accettata se la persona P registrata ha ancora la versione V2. In definitiva, le modifiche apportate da U1 e U2 saranno prese in considerazione, mentre nel caso d'uso senza versioni, una delle modifiche andava persa.
- righe 32–40: un costruttore in grado di inizializzare i campi di una persona. Il campo [version] è omesso.
- righe 43–51: un costruttore che crea una copia della persona che gli viene passata come parametro. Ora abbiamo due oggetti con contenuto identico ma a cui fanno riferimento due puntatori diversi.
- Riga 55: il metodo [toString] viene ridefinito per restituire una stringa che rappresenta lo stato della persona
14.4. Il livello [DAO]
Il livello [DAO] è costituito dalle seguenti classi e interfacce:
![]()
- [IDao] è l'interfaccia presentata dal livello [dao]
- [DaoImpl] è un'implementazione di questa interfaccia in cui il gruppo di persone è incapsulato in un oggetto [ArrayList]
- [DaoException] è un tipo di eccezione non controllata generata dal livello [dao]
L'interfaccia [ IDao] è la seguente:
- L'interfaccia dispone di quattro metodi per le quattro operazioni che vogliamo eseguire sul gruppo di persone:
- getAll: per recuperare un insieme di persone
- getOne: per recuperare una persona con un ID specifico
- saveOne: per aggiungere una persona (id=-1) o modificare una persona esistente (id ≠ -1)
- deleteOne: per eliminare una persona con un ID specifico
Il livello [DAO] può generare delle eccezioni. Queste saranno di tipo [ DaoException]:
- Riga 3: La classe [DaoException], che deriva da [RuntimeException], è un tipo di eccezione non gestita: il compilatore non richiede di:
- gestire questo tipo di eccezione con un blocco try/catch quando si chiama un metodo che potrebbe generarla
- includere la parola chiave "throws DaoException" nella firma di un metodo che potrebbe generare l'eccezione
Questa tecnica ci evita di dover firmare i metodi dell'interfaccia [IDao] con eccezioni di un tipo specifico. Qualsiasi implementazione che generi eccezioni non controllate sarà quindi accettabile, garantendo così flessibilità all'architettura.
- Riga 6: un codice di errore. Il livello [dao] genererà varie eccezioni identificate da diversi codici di errore. Ciò consentirà al livello responsabile della gestione dell'eccezione di determinare l'esatta origine dell'errore e di intraprendere l'azione appropriata. Esistono altri modi per ottenere lo stesso risultato. Uno di questi consiste nel creare un tipo di eccezione per ogni possibile tipo di errore, ad esempio MissingLastNameException, MissingFirstNameException, IncorrectAgeException, ...
- righe 13–16: il costruttore che consente di creare un'eccezione identificata da un codice di errore e un messaggio di errore.
- Righe 8–10: il metodo che consente al gestore delle eccezioni di recuperare il codice di errore.
La classe [ DaoImpl] implementa l'interfaccia [IDao]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Ci limiteremo a illustrare questo codice. Tuttavia, ci soffermeremo un po' sulle parti più complesse.
- Riga 13: l'oggetto [ArrayList] che conterrà il gruppo di persone
- riga 16: l'ID dell'ultima persona aggiunta. Ogni volta che viene aggiunta una nuova persona, questo ID verrà incrementato di 1.
La classe [DaoImpl] verrà istanziata come singola istanza. Questo è noto come singleton. Un'applicazione web serve i propri utenti simultaneamente. In qualsiasi momento, ci sono più thread in esecuzione sul server web. Questi thread condividono i singleton:
- quello del livello [dao]
- quello nel livello [service]
- quelli dei vari controller, validatori di dati, ecc., nel livello web
Se un singleton ha campi privati, dovresti chiederti immediatamente perché li ha. Sono giustificati? Infatti, saranno condivisi tra thread diversi. Se sono di sola lettura, questo non è un problema se possono essere inizializzati in un momento in cui sei sicuro che ci sia un solo thread attivo. In genere sappiamo come identificare questo momento. È quando l'applicazione web si avvia ma non ha ancora iniziato a servire i clienti. Se sono in lettura/scrittura, allora deve essere implementata la sincronizzazione dell'accesso ai campi; altrimenti, il disastro è inevitabile. Illustreremo questo problema quando testeremo il livello [dao].
- La classe [DaoImpl] non ha un costruttore. Pertanto, verrà utilizzato il suo costruttore predefinito.
- Righe 19–38: il metodo [init] verrà chiamato quando viene istanziato il singleton del livello [dao]. Crea un elenco di tre persone.
- Righe 41–43: Implementa il metodo [getAll] dell'interfaccia [IDao]. Restituisce un riferimento all'elenco delle persone.
- Righe 46–55: Implementa il metodo [getOne] dell'interfaccia [IDao]. Il suo parametro è l'ID della persona ricercata.
Per recuperarlo, chiamiamo un metodo privato [getPosition] nelle righe 113–126. Questo metodo restituisce la posizione nell'elenco della persona ricercata, oppure -1 se la persona non è stata trovata.
Se la persona viene trovata, il metodo [getOne] restituisce un riferimento (riga 51) a una copia di quella persona, non alla persona stessa. Infatti, quando un utente desidera modificare una persona, le informazioni su quella persona vengono richieste al livello [dao] e trasmesse al livello [web] per la modifica, sotto forma di un riferimento a un oggetto [Person]. Questo riferimento funge da contenitore di input nel modulo di modifica. Quando l'utente invia le proprie modifiche nel livello web, il contenuto del contenitore di input verrà modificato. Se il contenitore è un riferimento alla persona effettiva nell'[ArrayList] del livello [dao], allora quella persona viene modificata anche se le modifiche non sono state presentate ai livelli [service] e [dao]. Quest'ultimo è l'unico livello autorizzato a gestire l'elenco delle persone. Pertanto, il livello web deve lavorare su una copia della persona da modificare. In questo caso, il livello [dao] fornisce questa copia.
Se la persona ricercata non viene trovata, viene generata una [DaoException] con codice di errore 2 (riga 53).
- righe 94–104: implementa il metodo [deleteOne] dell'interfaccia [IDao]. Il suo parametro è l'ID della persona da eliminare. Se la persona da eliminare non esiste, viene generata un'eccezione [DaoException] con codice di errore 2.
- Righe 58–91: implementa il metodo [saveOne] dell'interfaccia [IDao]. Il suo parametro è un oggetto [Person]. Se questo oggetto ha un id pari a -1, allora si tratta di una nuova persona che viene aggiunta. Altrimenti, modifica la persona nell'elenco con quell'id utilizzando i valori nel parametro.
- Riga 60: La validità del parametro [Person] viene verificata da un metodo privato [check] definito alle righe 129–155. Questo metodo esegue controlli di base sui valori dei vari campi di [Person]. Ogni volta che viene rilevata un'anomalia, viene generata una [DaoException] con un codice di errore specifico. Poiché il metodo [saveOne] non gestisce questa eccezione, essa verrà propagata al metodo chiamante.
- Riga 62: Se il parametro [Person] ha un id pari a -1, si tratta di un'aggiunta. L'oggetto [Person] viene aggiunto all'elenco interno delle persone (riga 66), con il primo id disponibile (riga 64) e un numero di versione pari a 1 (riga 65).
- Se il parametro [Person] ha un [id] diverso da -1, ciò comporta la modifica della persona nell'elenco interno con quell'[id]. Innanzitutto, verifichiamo (righe 70–75) che la persona da modificare esista. Se così non fosse, generiamo una [DaoException] con codice di errore 2.
- Se la persona esiste, verifichiamo che la sua versione attuale corrisponda a quella del parametro [Person], che contiene le modifiche da applicare all'originale. Se così non fosse, significa che l'utente che sta tentando di modificare la persona non dispone della versione più recente. Lo informiamo di ciò generando un'eccezione [DaoException] con codice di errore 3 (righe 79–80).
- Se tutto va bene, le modifiche vengono apportate al record originale della persona (righe 85–90)
È chiaro che questo metodo deve essere sincronizzato. Ad esempio, tra il momento in cui verifichiamo che la persona da modificare sia effettivamente presente e il momento in cui viene apportata la modifica, la persona potrebbe essere stata rimossa dall'elenco da qualcun altro. Il metodo dovrebbe quindi essere dichiarato [synchronized] per garantire che venga eseguito da un solo thread alla volta. Lo stesso vale per gli altri metodi dell'interfaccia [IDao]. Non lo facciamo, preferendo spostare questa sincronizzazione al livello [service]. Per evidenziare i problemi di sincronizzazione, durante il test del livello [dao] metteremo in pausa l'esecuzione di [saveOne] per 10 ms (riga 83) tra il momento in cui sappiamo di poter effettuare la modifica e il momento in cui la effettuiamo effettivamente. Il thread che esegue [saveOne] perderà quindi la CPU a favore di un altro thread. Questo aumenta le nostre possibilità di vedere conflitti di accesso nell'elenco delle persone.
14.5. Test del livello [DAO]
Viene scritto un test JUnit per il livello [dao]:
![]() | ![]() |
[TestDao] è il test JUnit. Per evidenziare i problemi di accesso concorrente all'elenco delle persone, vengono creati thread di tipo [ThreadDaoMajEnfants]. Essi hanno il compito di aumentare di 1 il numero di figli di una data persona.
[TestDao] contiene cinque test, da [test1] a [test5]. Qui ne presentiamo solo due; invitiamo i lettori a esplorare gli altri nel codice sorgente associato a questo articolo.
- riga 9: riferimento all'implementazione del livello [dao] sottoposto a test
- righe 12–15: il costruttore del test JUnit. Crea un'istanza di tipo [DaoImpl] dal livello [dao] da testare e la inizializza.
Il metodo [test1] verifica i quattro metodi dell'interfaccia [IDao] come segue:
- Riga 3: Richiediamo l'elenco delle persone
- riga 6: lo visualizziamo
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]
Il test aggiunge quindi una persona, la modifica e la elimina. Vengono così utilizzati i quattro metodi dell'interfaccia [IDao].
- Righe 8–10: viene aggiunta una nuova persona (id=-1).
- Riga 11: Recuperiamo l'ID della persona aggiunta perché l'operazione di aggiunta gliene ha assegnato uno. Prima di allora, non ne aveva uno.
- Righe 13–14: Chiediamo al livello [dao] una copia della persona appena aggiunta. Tieni presente che se la persona richiesta non viene trovata, il livello [dao] genera un'eccezione. Ciò causerà un crash alla riga 13. Avremmo potuto gestire questo caso in modo più pulito. Alla riga 14, controlliamo il nome della persona recuperata.
- Righe 16–17: Modifichiamo questo nome e chiediamo al livello [DAO] di salvare le modifiche.
- Righe 19–20: Chiediamo al livello [DAO] una copia della persona appena aggiunta e verifichiamo il suo nuovo nome.
- Riga 22: Elimina la persona aggiunta all'inizio del test.
- Righe 23–34: Richiediamo una copia della persona appena eliminata dal livello [dao]. Dovresti ricevere un [DaoException] con codice 2.
- Righe 36–37: L'elenco delle persone viene richiesto nuovamente. Dovremmo ottenere lo stesso elenco dell'inizio del test.
Il metodo [test4] mira a evidenziare i problemi relativi all'accesso simultaneo ai metodi del livello [dao]. Ricordiamo che questi metodi non sono stati sincronizzati. Il codice del test è il seguente:
- righe 3–6: aggiungiamo alla lista una persona P senza figli. Registriamo il suo [id] (riga 6).
- righe 7–13: avviamo N thread. Ciascuno di essi incrementerà di 1 il numero di figli della persona P. Alla fine, la persona P dovrebbe avere N figli.
- righe 15–17: il metodo [test4] che ha avviato i N thread attende che questi completino il loro lavoro prima di verificare il nuovo numero di figli della persona P.
- righe 18–21: recuperiamo la persona P e verifichiamo che il numero dei suoi figli sia N.
- Righe 22–35: La persona P viene rimossa e verifichiamo che non sia più presente nell'elenco.
Alla riga 11, vediamo che i thread sono di tipo [ThreadDaoMajEnfants]. Il costruttore di questo tipo ha tre parametri:
- il nome assegnato al thread, utilizzato per tracciarlo tramite i log
- un riferimento al livello [dao] in modo che il thread possa accedervi
- l'ID della persona su cui il thread dovrebbe lavorare
Il tipo [ThreadDaoMajEnfants] è il seguente:
- riga 9: [ThreadDaoMajEnfants] è effettivamente un thread
- righe 18–22: il costruttore che inizializza il thread con tre informazioni
- il nome [name] assegnato al thread
- un riferimento [dao] al livello [dao]. Si noti che, ancora una volta, stiamo lavorando con il tipo di interfaccia [IDao] e non con il tipo di implementazione [DaoImpl].
- l'identificatore [id] della persona su cui il thread deve lavorare
Quando [test4] avvia un thread [ThreadDaoMajEnfants] (riga 12 di test4), viene eseguito il suo metodo [run] (riga 25):
- righe 78–81: il metodo privato [suivi] consente la registrazione su schermo. Il metodo [run] lo utilizza per tracciare l'esecuzione del thread.
- Il thread tenta di incrementare di 1 il numero di figli della persona P con identificatore [id]. Questo aggiornamento potrebbe richiedere più tentativi. Consideriamo due thread [TH1] e [TH2]. [TH1] richiede una copia della persona P dal livello [dao]. La ottiene e rileva che ha la versione V1. [TH1] viene interrotto. [TH2], che lo stava seguendo, fa la stessa cosa e ottiene la stessa versione V1 della persona P. [TH2] viene interrotto. [TH2] riprende il controllo, incrementa il numero di figli per P e salva le modifiche. Sappiamo che queste modifiche sono ora salvate e che la versione di P passerà a V2. [TH1] ha terminato il suo lavoro. [TH2] riprende il controllo e fa lo stesso. Il suo aggiornamento a P verrà rifiutato perché detiene una copia di P nella versione V1, mentre il P originale è ora nella versione V2. [TH2] deve quindi ripetere l'intero ciclo [lettura -> aggiornamento -> salvataggio]. Questo è il motivo per cui troviamo il ciclo nelle righe 32–72. In questo ciclo, il thread:
- richiede una copia della persona P da modificare (riga 34)
- attende 10 ms (riga 43). Questo è artificiale e mira a interrompere il thread tra la lettura della persona P e il suo effettivo aggiornamento nell'elenco delle persone, al fine di aumentare la probabilità di conflitti.
- incrementa il numero di figli di P (riga 54) e salva P (riga 56). Se il thread non possiede la versione corretta di P, verrà generata un'eccezione dal livello [dao]. Recuperiamo quindi il codice dell'eccezione (riga 61) per verificare che si tratti effettivamente del codice 3 (versione errata di P). Se così non fosse, l'eccezione viene rilanciata al metodo chiamante, in definitiva il metodo di test [test4]. Se abbiamo l'eccezione codice 3, allora riavviamo il ciclo [lettura -> aggiornamento -> salvataggio]. Se non c'è alcuna eccezione, allora l'aggiornamento è stato completato e il lavoro del thread è terminato.
Cosa mostrano i test?
Nella prima configurazione testata:
- commentiamo l'istruzione wait nel metodo [saveOne] di [DaoImpl] (riga 83, sezione 14.4).
- il metodo [test4] crea 100 thread (riga 8, sezione 14.5).
Si ottengono i seguenti risultati:

Tutti e cinque i test hanno avuto esito positivo.
Nella seconda configurazione testata:
- l'istruzione wait nel metodo [saveOne] di [DaoImpl] è stata rimossa dal commento (riga 83, sezione 14.4).
- il metodo [test4] crea 2 thread (riga 8, sezione 14.5).
Si ottengono i seguenti risultati:
![]() | ![]() |
Il test [test4] ha dato esito negativo. Abbiamo creato due thread, ciascuno con il compito di incrementare di 1 il numero di figli di una persona P che inizialmente ne aveva 0. Ci aspettavamo quindi di avere 2 figli dopo l'esecuzione dei due thread, ma ne abbiamo solo uno.
Esaminiamo i log di schermo di [test4] per capire cosa è successo:
- Riga 1: il thread n. 0 inizia il suo lavoro
- riga 2: ha recuperato una copia della persona P e rileva che il numero di figli è 0
- riga 3: incontra il [Thread.sleep(10)] nel suo metodo [run] e quindi si mette in pausa al tempo [1145536368171] (ms)
- riga 4: il thread n. 1 prende quindi il controllo del processore e inizia il suo lavoro
- riga 5: ha recuperato una copia della persona P e rileva che il numero di figli è 0
- Riga 6: incontra il [Thread.sleep(10)] nel suo metodo [run] e quindi si mette in pausa
- Riga 7: il thread 0 riacquista la CPU al tempo [1145536368187] (ms), ovvero 16 ms dopo averla persa.
- riga 8: lo stesso vale per il thread n. 1
- riga 9: il thread n. 0 si è aggiornato e ha impostato il numero di figli su 1
- riga 10: il thread n. 1 ha fatto lo stesso
La domanda è: perché il thread n. 1 è stato in grado di eseguire il proprio aggiornamento quando, normalmente, non possedeva più la versione corretta della persona P, che era stata appena aggiornata dal thread n. 0?
Innanzitutto, possiamo osservare un'anomalia tra le righe 7 e 8: sembra che il thread #0 abbia perso la CPU tra queste due righe a favore del thread #1. Cosa stava facendo in quel momento? Stava eseguendo il metodo [saveOne] del livello [dao]. Questo metodo ha la seguente struttura (vedi sezione 14.4):
- Il thread #0 ha eseguito [saveOne] ed è passato alla riga 8, dove è stato costretto a rilasciare il processore. Nel frattempo, ha letto la versione della persona P, che era 1 perché la persona P non era ancora stata aggiornata.
- Poiché la CPU si è liberata, il thread #1 l'ha acquisita. A sua volta, ha eseguito [saveOne] e ha raggiunto la riga 8, dove è stato costretto a rilasciare la CPU. Nel frattempo, ha letto la versione della persona P, che era 1 perché la persona P non era ancora stata aggiornata.
- Poiché il processore si è liberato, il thread #0 lo ha acquisito. A partire dalla riga 9, ha eseguito il suo aggiornamento e ha impostato il numero di figli su 1. Quindi il metodo [run] del thread #0 è terminato e il thread ha visualizzato il log indicando che aveva impostato il numero di figli su 1 (riga 9).
- Poiché il processore si è liberato, il thread #1 lo ha ereditato. A partire dalla riga 9, ha eseguito il proprio aggiornamento e ha impostato il numero di figli su 1. Perché 1? Perché contiene una copia di P con il numero di figli impostato su 0. Ciò è indicato dal log (riga 5). Quindi il metodo [run] del thread n. 1 si è concluso e il thread ha visualizzato il log indicando di aver impostato il numero di figli su 1 (riga 10).
Da dove deriva il problema? Deriva dal fatto che il thread #0 non ha avuto il tempo di confermare la sua modifica e quindi di aggiornare la versione della persona P prima che il thread #1 tentasse di leggere quella versione per verificare se la persona P fosse cambiata. Questo scenario è improbabile ma non impossibile. Abbiamo dovuto forzare il thread #0 a cedere la CPU per far sì che apparisse con solo due thread. Senza questa soluzione alternativa, la configurazione precedente non era riuscita a riprodurre lo stesso scenario con 100 thread. Il test [test4] aveva avuto esito positivo.
Qual è la soluzione? Ce ne sono senza dubbio diverse. Una di queste, facile da implementare, consiste nel sincronizzare il metodo [saveOne]:
public synchronized void saveOne(Personne personne)
La parola chiave [synchronized] garantisce che solo un thread alla volta possa eseguire il metodo. Pertanto, al thread n. 1 sarà consentito eseguire [saveOne] solo dopo che il thread n. 0 lo avrà terminato. Possiamo quindi essere certi che la versione della persona P sarà stata modificata nel momento in cui il thread n. 1 entrerà in [saveOne]. Il suo aggiornamento verrà quindi rifiutato perché non avrà la versione corretta di P.
Questi sono i quattro metodi del livello [dao] che dovrebbero essere sincronizzati. Tuttavia, decidiamo di mantenere questo livello come descritto e di spostare la sincronizzazione al livello [service]. Ci sono diverse ragioni per questo:
- Partiamo dal presupposto che l'accesso al livello [dao] avvenga sempre attraverso un livello [service]. Questo è il caso della nostra applicazione web.
- Potrebbe anche essere necessario sincronizzare l'accesso ai metodi del livello [service] per motivi diversi da quelli che ci porterebbero a sincronizzare quelli del livello [dao]. In questo caso, non c'è bisogno di sincronizzare i metodi del livello [dao]. Se siamo certi che:
- tutti gli accessi al livello [DAO] passano attraverso il livello [service]
- solo un thread alla volta utilizza il livello [service]
allora possiamo essere certi che i metodi del livello [DAO] non saranno eseguiti da due thread contemporaneamente.
Esploreremo ora il livello [service].
14.6. Il livello [service]
Il livello [service] è costituito dalle seguenti classi e interfacce:
![]()
- [IService] è l'interfaccia esposta dal livello [dao]
- [ServiceImpl] è un'implementazione di questa interfaccia
L'interfaccia [IService] è la seguente:
È identico all'interfaccia [IDao].
L'implementazione [ServiceImpl] dell'interfaccia [IService] è la seguente:
- righe 10–19: l'attributo [IDao dao] è un riferimento al livello [dao]. Verrà inizializzato da Spring IoC.
- righe 22–24: implementazione del metodo [getAll] dell'interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
- righe 27–29: implementazione del metodo [getOne] dell'interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
- Righe 32–34: implementazione del metodo [saveOne] dell'interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
- Righe 37–39: implementazione del metodo [deleteOne] dell'interfaccia [IService]. Il metodo si limita a delegare la richiesta al livello [dao].
- Tutti i metodi sono sincronizzati (utilizzando la parola chiave `synchronized`), garantendo che solo un thread alla volta possa utilizzare il livello [service] e, di conseguenza, il livello [dao].
14.7. Test per il livello [service]
Viene scritto un test JUnit per il livello [service]:
![]() | ![]() |
[TestService] è il test JUnit. I test eseguiti sono esattamente gli stessi di quelli eseguiti per il livello [dao]. Lo scheletro di [TestService] è il seguente:
- Riga 9: il livello [service] sottoposto a test è di tipo [ServiceImpl].
- righe 11–15: il costruttore del test JUnit crea un'istanza del livello [service] da testare (riga 12), crea un'istanza del livello [dao] (riga 13) e indica al livello [service] di utilizzare questo livello [dao] (riga 14).
Il metodo [test1] verifica i quattro metodi dell'interfaccia [IService] allo stesso modo del metodo di test del livello [dao] con lo stesso nome. L'unica differenza è che accede al livello [service] (righe 25, 32, 35) anziché al livello [dao].
Il metodo [test4] mira a evidenziare i problemi relativi all'accesso simultaneo ai metodi del livello [service]. È, ancora una volta, identico al metodo di test [test4] del livello [dao]. Tuttavia, vi sono alcuni dettagli che differiscono:
- ci rivolgiamo al livello [service] anziché al livello [dao] (riga 55)
- passiamo un riferimento al livello [service] ai thread anziché al livello [dao] (riga 61)
Anche il tipo [ThreadServiceMajEnfants] è quasi identico al tipo [ThreadDaoMajEnfants], con l'eccezione che opera con il livello [service] anziché con il livello [dao]:
- Riga 12: Il thread opera con il livello [service]
Stiamo eseguendo i test con la configurazione che ha causato problemi a livello [dao]:
- rimuoviamo il commento dall'istruzione wait nel metodo [saveOne] di [DaoImpl] (riga 83, sezione 14.4).
- Il metodo [test4] crea 100 thread (riga 65, sezione 14.7).
I risultati ottenuti sono i seguenti:
![]() |
È stata la sincronizzazione dei metodi nel livello [service] a consentire il successo del test [test4].
14.8. Il livello [web]
Rivediamo l'architettura a tre livelli della nostra applicazione:
![]() |
Il livello [web] fornirà all'utente delle schermate che gli consentiranno di gestire il gruppo di persone:
- elenco delle persone nel gruppo
- aggiungere una persona al gruppo
- modifica di una persona nel gruppo
- rimuovere una persona dal gruppo
Per farlo, si affiderà al livello [service], che a sua volta richiamerà il livello [DAO]. Abbiamo già presentato le schermate gestite dal livello [web] (sezione 14.1). Per descrivere il livello web, presenteremo di seguito i seguenti elementi:
- la sua configurazione
- le sue viste
- il suo controller
- alcuni test
14.8.1. Configurazione dell'applicazione web
Il progetto Eclipse per l'applicazione è il seguente:

- Nel pacchetto [istia.st.mvc.personnes.web] si trova il controller [Application].
- Le pagine JSP/JSTL si trovano in [WEB-INF/views].
- La cartella [lib] contiene le librerie di terze parti richieste dall'applicazione. Sono visibili nella cartella [Web App Libraries].
[web.xml]
Il file [web.xml] è il file utilizzato dal server web per caricare l'applicazione. Il suo contenuto è il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>mvc-personnes-01</display-name>
<!-- ServletPersonne -->
<servlet>
<servlet-name>personnes</servlet-name>
<servlet-class>
istia.st.mvc.personnes.web.Application
</servlet-class>
<init-param>
<param-name>urlEdit</param-name>
<param-value>/WEB-INF/vues/edit.jsp</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/WEB-INF/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlList</param-name>
<param-value>/WEB-INF/vues/list.jsp</param-value>
</init-param>
</servlet>
<!-- Mapping ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- welcome files -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Unexpected error page -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- righe 27-30: gli URL [/do/*] saranno gestiti dal servlet [people]
- righe 9-12: il servlet [personnes] è un'istanza della classe [Application], una classe che creeremo.
- righe 13-24: definiscono tre parametri [urlList, urlEdit, urlErrors] che identificano gli URL delle pagine JSP per le viste [list, edit, errors].
- righe 32–34: L'applicazione ha una pagina di accesso predefinita [index.jsp] situata nella radice della cartella dell'applicazione web.
- righe 36–39: L'applicazione ha una pagina di errore predefinita che viene visualizzata quando il server web incontra un'eccezione non gestita dall'applicazione.
- Riga 37: il tag <exception-type> specifica il tipo di eccezione gestita dalla direttiva <error-page>; in questo caso, si tratta del tipo [java.lang.Exception] e dei suoi sottotipi, ovvero tutte le eccezioni.
- Riga 38: il tag <location> specifica la pagina JSP da visualizzare quando si verifica un'eccezione del tipo definito da <exception-type>. L'eccezione verificatasi è disponibile su questa pagina in un oggetto denominato exception se la pagina contiene la direttiva:
<%@ page isErrorPage="true" %>
- (continua)
- Se <exception-type> specifica un tipo T1 e un'eccezione di tipo T2 (non derivata da T1) viene propagata fino al server web, il server invia al client una pagina di eccezione proprietaria, che in genere non è molto intuitiva. Da qui l'importanza del tag <error-page> nel file [web.xml].
[index.jsp]
Questa pagina viene visualizzata se un utente richiede direttamente il contesto dell'applicazione senza specificare un URL, ovvero, in questo caso [/personnes-01]. Il suo contenuto è il seguente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/do/list"/>
[index.jsp] reindirizza il client all'URL [/do/list]. Questo URL visualizza l'elenco delle persone presenti nel gruppo.
14.8.2. Le pagine JSP/JSTL dell'applicazione
Viene utilizzata per visualizzare l'elenco delle persone:

Il codice è il seguente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>Liste des personnes</h2>
<table border="1">
<tr>
<th>Id</th>
<th>Version</th>
<th>Prénom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</th>
<th>Nombre d'enfants</th>
<th></th>
</tr>
<c:forEach var="personne" items="${personnes}">
<tr>
<td><c:out value="${personne.id}"/></td>
<td><c:out value="${personne.version}"/></td>
<td><c:out value="${personne.prenom}"/></td>
<td><c:out value="${personne.nom}"/></td>
<td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
<td><c:out value="${personne.marie}"/></td>
<td><c:out value="${personne.nbEnfants}"/></td>
<td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
<td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
</tr>
</c:forEach>
</table>
<br>
<a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
</body>
</html>
- Questa vista riceve un elemento nel proprio modello:
- l'elemento [people] associato a un [ArrayList] di oggetti [Person]
- righe 22–34: iteriamo attraverso l'elenco ${people} per visualizzare una tabella HTML contenente le persone del gruppo.
- riga 31: l'URL a cui punta il link [Edit] viene impostato utilizzando il campo [id] della persona corrente in modo che il controller associato all'URL [/do/edit] sappia quale persona modificare.
- riga 32: lo stesso viene fatto per il link [Delete].
- riga 28: per visualizzare la data di nascita della persona nel formato GG/MM/AAAA, utilizziamo il tag <dt> dalla libreria di tag [DateTime] del progetto Apache [Jakarta Taglibs]:

Il file di descrizione per questa libreria di tag è definito alla riga 3.
- Riga 37: il link [Add] per l'aggiunta di una nuova persona punta all'URL [/do/edit], proprio come il link [Edit] alla riga 31. Il valore -1 per il parametro [id] indica che si tratta di un'aggiunta piuttosto che di una modifica.
Viene utilizzata per visualizzare il modulo per l'aggiunta di una nuova persona o la modifica di una esistente:
![]() |
Il codice per la vista [edit.jsp] è il seguente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="../ressources/standard.jpg">
<h2>Ajout/Modification d'une personne</h2>
<c:if test="${erreurEdit != ''}">
<h3>Echec de la mise à jour :</h3>
L'erreur suivante s'est produite : ${erreurEdit}
<hr>
</c:if>
<form method="post" action="<c:url value="/do/validate"/>">
<table border="1">
<tr>
<td>Id</td>
<td>${id}</td>
</tr>
<tr>
<td>Version</td>
<td>${version}</td>
</tr>
<tr>
<td>Prénom</td>
<td>
<input type="text" value="${prenom}" name="prenom" size="20">
</td>
<td>${erreurPrenom}</td>
</tr>
<tr>
<td>Nom</td>
<td>
<input type="text" value="${nom}" name="nom" size="20">
</td>
<td>${erreurNom}</td>
</tr>
<tr>
<td>Date de naissance (JJ/MM/AAAA)</td>
<td>
<input type="text" value="${dateNaissance}" name="dateNaissance">
</td>
<td>${erreurDateNaissance}</td>
</tr>
<tr>
<td>Marié</td>
<td>
<c:choose>
<c:when test="${marie}">
<input type="radio" name="marie" value="true" checked>Oui
<input type="radio" name="marie" value="false">Non
</c:when>
<c:otherwise>
<input type="radio" name="marie" value="true">Oui
<input type="radio" name="marie" value="false" checked>Non
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td>Nombre d'enfants</td>
<td>
<input type="text" value="${nbEnfants}" name="nbEnfants">
</td>
<td>${erreurNbEnfants}</td>
</tr>
</table>
<br>
<input type="hidden" value="${id}" name="id">
<input type="hidden" value="${version}" name="version">
<input type="submit" value="Valider">
<a href="<c:url value="/do/list"/>">Annuler</a>
</form>
</body>
</html>
Questa vista mostra un modulo per aggiungere una nuova persona o aggiornare una esistente. D'ora in poi, per semplificare il testo, useremo il termine unico [aggiornamento]. Il pulsante [Invia] (riga 73) attiva una richiesta POST all'URL [/do/validate] (riga 16). Se la richiesta POST fallisce, viene nuovamente visualizzata la vista [edit.jsp] con gli errori verificatisi; in caso contrario, viene visualizzata la vista [list.jsp].
- La vista [edit.jsp], che viene visualizzata sia in seguito a una richiesta GET che a una richiesta POST fallita, riceve i seguenti elementi nel proprio modello:
attributo | GET | POST |
ID della persona che viene aggiornata | stesso | |
la sua versione | uguale | |
nome | Nome inserito | |
il suo cognome | Cognome inserito | |
la sua data di nascita | data di nascita inserita | |
stato civile | Stato civile inserito | |
numero di figli | numero di figli inserito | |
vuoto | Messaggio di errore che indica che l'aggiunta o la modifica non è andata a buon fine durante il POST attivato dal pulsante [Invia]. Vuoto se non ci sono errori. | |
vuoto | indica un nome non corretto – vuoto in caso contrario | |
vuoto | segnala un cognome errato – vuoto in caso contrario | |
vuoto | indica una data di nascita errata – vuoto altrimenti | |
vuoto | indica un numero di figli errato – vuoto in caso contrario |
- righe 11-15: se il POST del modulo fallisce, verrà restituito [errorEdit!=''] e verrà visualizzato un messaggio di errore.
- riga 16: il modulo verrà inviato all'URL [/do/validate]
- riga 20: viene visualizzato l'elemento [id] del template
- riga 24: viene visualizzato l'elemento [version] del template
- righe 26-32: inserimento del nome della persona:
- Quando il modulo viene visualizzato inizialmente (GET), ${firstName} mostra il valore corrente del campo [firstName] dell'oggetto [Person] aggiornato, mentre ${firstNameError} è vuoto.
- in caso di errore dopo il POST, il valore inserito ${firstName} viene visualizzato nuovamente, insieme a qualsiasi messaggio di errore ${firstNameError}
- righe 33-39: inserimento del cognome della persona
- righe 40–46: inserimento della data di nascita della persona
- Righe 47–61: inserimento dello stato civile della persona tramite un pulsante di opzione. Utilizziamo il valore del campo [married] dell'oggetto [Person] per determinare quale dei due pulsanti di opzione debba essere selezionato.
- righe 62-68: inserimento del numero di figli della persona
- riga 71: un campo HTML nascosto denominato [id] con un valore uguale al campo [id] della persona che si sta aggiornando, -1 per un'aggiunta o un altro valore per una modifica.
- riga 72: un campo HTML nascosto denominato [version] con un valore uguale al campo [id] della persona che si sta aggiornando.
- Riga 73: il pulsante [Invia] del modulo
- riga 74: un link per tornare all'elenco delle persone. È etichettato [Cancel] perché permette all'utente di uscire dal modulo senza inviarlo.
Viene utilizzata per visualizzare una pagina che indica che si è verificata un'eccezione non gestita dall'applicazione e che è stata propagata al server web.
Ad esempio, proviamo a cancellare una persona che non esiste nel gruppo:
![]() |
Il codice per la vista [exception.jsp] è il seguente:
<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>
<%
response.setStatus(200);
%>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body background="<c:url value="/ressources/standard.jpg"/>">
<h2>MVC - personnes</h2>
L'exception suivante s'est produite :
<%= exception.getMessage()%>
<br><br>
<a href="<c:url value="/do/list"/>">Retour à la liste</a>
</body>
</html>
- Questa vista riceve una chiave nel proprio template, l'elemento [exception], che rappresenta l'eccezione intercettata dal server web. Affinché questo elemento venga incluso nel template della pagina JSP dal server web, la pagina deve avere definito il tag alla riga 3.
- Riga 6: Impostiamo il codice di stato HTTP della risposta a 200. Questa è la prima intestazione HTTP della risposta. Il codice di stato 200 indica al client che la sua richiesta è andata a buon fine. In genere, nella risposta del server è stato incluso un documento HTML. È il caso qui. Se il codice di stato HTTP della risposta non è impostato su 200, avrà il valore 500, il che significa che si è verificato un errore. Infatti, quando il server web intercetta un'eccezione non gestita, considera questa situazione anomala e la segnala con un codice 500. La risposta a un codice HTTP 500 varia a seconda del browser: Firefox visualizza il documento HTML che può accompagnare questa risposta, mentre IE ignora questo documento e visualizza la propria pagina. Questo è il motivo per cui abbiamo sostituito il codice 500 con il codice 200.
- Riga 16: Viene visualizzato il testo dell'eccezione
- Riga 18: All'utente viene offerto un link per tornare all'elenco delle persone
Viene utilizzato per visualizzare una pagina che riporta gli errori di inizializzazione dell'applicazione, ovvero gli errori rilevati durante l'esecuzione del metodo [init] del servlet del controller. Potrebbe trattarsi, ad esempio, dell'assenza di un parametro nel file [web.xml], come illustrato nell'esempio seguente:

Il codice della pagina [errors.jsp] è il seguente:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>MVC - Personnes</title>
</head>
<body>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li>${erreur}</li>
</c:forEach>
</ul>
</body>
</html>
La pagina riceve un elemento [errors] nel proprio template, che è un [ArrayList] di oggetti [String]; questi sono messaggi di errore. Vengono visualizzati dal ciclo nelle righe 13–15.
14.8.3. Il controller dell'applicazione
Il controller [Application] è definito nel pacchetto [istia.st.mvc.personnes.web]:
![]()
Struttura e inizializzazione del controller
Lo scheletro del controller [Application] è il seguente:
- righe 20–36: recupera i parametri specificati nel file [web.xml].
- Righe 39–41: il parametro [urlErrors] deve essere presente perché specifica l'URL della vista [errors], che visualizza eventuali errori di inizializzazione. Se non esiste, l'applicazione viene terminata generando un'eccezione [ServletException] (riga 40). Questa eccezione verrà propagata al server web e gestita dal tag <error-page> nel file [web.xml]. Viene quindi visualizzata la vista [exception.jsp]:

Il link [Torna all'elenco] in alto è inattivo. Facendo clic su di esso si ottiene la stessa risposta fintanto che l'applicazione non è stata modificata e ricaricata. È utile per altri tipi di eccezioni, come abbiamo già visto.
- riga 43: crea un'istanza [DaoImpl] che implementa il livello [dao]
- riga 44: inizializza questa istanza (crea un elenco iniziale di tre persone)
- riga 46: crea un'istanza di [ServiceImpl] che implementa il livello [service]
- riga 47: inizializza il livello [service] fornendogli un riferimento al livello [dao]
Dopo l'inizializzazione del controller, i suoi metodi dispongono di un riferimento [service] al livello [service] (riga 15) che useranno per eseguire le azioni richieste dall'utente. Queste saranno intercettate dal metodo [doGet], che le farà elaborare da un metodo specifico del controller:
Url | Metodo HTTP | Metodo del controller |
GET | doListPeople | |
GET | doModificaPersona | |
POST | doValidatePerson | |
GET | doDeletePerson |
Il metodo [doGet]
Lo scopo di questo metodo è indirizzare l'elaborazione delle azioni richieste dall'utente al metodo corretto. Il suo codice è il seguente:
- righe 7–13: Verifichiamo che l'elenco degli errori di inizializzazione sia vuoto. Se non lo è, visualizziamo la vista [errors(errors)], che segnalerà gli errori.
- riga 15: Recuperiamo il metodo [get] o [post] che il client ha utilizzato per effettuare la richiesta.
- riga 17: recuperiamo il valore del parametro [action] dalla richiesta.
- Righe 23–27: Elaboriamo la richiesta [GET /do/list], che richiede l'elenco delle persone.
- Righe 28–32: Elaborazione della richiesta [GET /do/delete], che richiede la cancellazione di una persona.
- Righe 33–37: Elaborazione della richiesta [GET /do/edit], che richiede il modulo per aggiornare una persona.
- Righe 38–42: elaborazione della richiesta [POST /do/validate], che richiede la convalida della persona aggiornata.
- Riga 44: se l'azione richiesta non è una delle cinque precedenti, la trattiamo come se fosse [GET /do/list].
Il metodo [doListPersonnes]
Questo metodo gestisce la richiesta [GET /do/list], che richiede l'elenco delle persone:

Il codice è il seguente:
- Riga 5: Richiediamo l'elenco delle persone del gruppo dal livello [service] e lo memorizziamo nel modello sotto la chiave "people".
- Riga 7: viene visualizzata la vista [list.jsp] descritta nella sezione 14.8.2.
Il metodo [doDeletePerson]
Questo metodo gestisce la richiesta [GET /do/delete?id=XX], che richiede l'eliminazione della persona con id=XX. L'URL [/do/delete?id=XX] è quello dei link [Delete] nella vista [list.jsp]:

il cui codice è il seguente:
La riga 12 mostra l'URL [/do/delete?id=XX] per il link [Elimina]. Il metodo [doDeletePerson], che gestisce questo URL, deve eliminare la persona con id=XX e quindi visualizzare l'elenco aggiornato delle persone nel gruppo. Il suo codice è il seguente:
- Riga 5: L'URL in elaborazione ha il formato [/do/delete?id=XX]. Recuperiamo il valore [XX] dal parametro [id].
- Riga 7: chiediamo al livello [service] di eliminare la persona con l'ID ottenuto. Non eseguiamo alcuna convalida. Se la persona che stiamo cercando di eliminare non esiste, il livello [dao] genera un'eccezione che viene propagata fino al livello [service]. Non la gestiamo nemmeno qui nel controller. Si propagherà quindi fino al server web, il quale, in base alla configurazione, visualizzerà la pagina [exception.jsp], descritta nella sezione 14.8.2:

- Riga 9: Se l'eliminazione è andata a buon fine (nessuna eccezione), il client viene reindirizzato all'URL relativo [list]. Poiché l'URL appena elaborato era [/do/delete], l'URL di reindirizzamento sarà [/do/list]. Il browser effettuerà quindi una richiesta [GET /do/list], che visualizzerà l'elenco delle persone.
Il metodo [doEditPerson]
Questo metodo gestisce la richiesta [GET /do/edit?id=XX], che richiede il modulo per aggiornare la persona con id=XX. L'URL [/do/edit?id=XX] è quello utilizzato per i link [Modifica] e [Aggiungi] nella vista [list.jsp]:

il cui codice è il seguente:
Alla riga 11, vediamo l'URL [/do/edit?id=XX] per il link [Modifica] e alla riga 17 l'URL [/do/edit?id=-1] per il link [Aggiungi]. Il metodo [doEditPersonne] deve visualizzare il modulo di modifica per la persona con id=XX oppure, se si tratta di un'aggiunta, visualizzare un modulo vuoto.
![]() | ![]() |
Il codice per il metodo [doEditPerson] è il seguente:
- La richiesta GET punta a un URL del tipo [/do/edit?id=XX]. Alla riga 5, recuperiamo il valore di [id]. A questo punto si presentano due casi:
- Se id non è uguale a -1, si tratta di un aggiornamento e dobbiamo visualizzare un modulo precompilato con le informazioni della persona da modificare. Alla riga 10, questa persona viene richiesta dal livello [service].
- Se id è uguale a -1, si tratta di un'aggiunta e deve essere visualizzato un modulo vuoto. A tal fine, alle righe 13–14 viene creata una persona vuota.
- L'oggetto [Person] viene inserito nel modello di pagina [edit.jsp] descritto nella Sezione 14.8.2. Questo modello include i seguenti elementi: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, dateOfBirth, errorDateOfBirth, spouse, numberOfChildren, errorNumberOfChildren]. Questi elementi vengono inizializzati nelle righe 17–30, ad eccezione di quelli il cui valore è una stringa vuota [firstNameError, lastNameError, birthDateError, childrenCountError]. Sappiamo che se mancano dal modello, la libreria JSTL visualizzerà una stringa vuota come loro valore. Sebbene anche l'elemento [errorEdit] abbia una stringa vuota come valore, viene comunque inizializzato perché viene eseguito un controllo sul suo valore nella pagina [edit.jsp].
- Una volta che il modello è pronto, il controllo viene trasferito alla pagina [edit.jsp], righe 32–33, che genererà la vista [edit].
Il metodo [doValidatePersonne]
Questo metodo gestisce la richiesta [POST /do/validate], che convalida il modulo di aggiornamento. Questo POST viene attivato dal pulsante [Validate]:

Esaminiamo gli elementi di input del modulo HTML nella vista sopra riportata:
La richiesta POST contiene i parametri [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] e viene inviata all'URL [/do/validate] (riga 1). Viene elaborata dal seguente metodo [doValidatePerson]:
- righe 8-14: viene recuperato il parametro [firstName] dalla richiesta POST e ne viene verificata la validità. Se non è corretto, l'elemento [firstNameError] viene inizializzato con un messaggio di errore e inserito negli attributi della richiesta.
- righe 16–22: lo stesso processo viene seguito per il parametro [lastName]
- righe 24–32: lo stesso processo viene applicato al parametro [dateOfBirth]
- Riga 34: viene recuperato il parametro [spouse]. Non ne verifichiamo la validità perché, in linea di principio, deriva dal valore di un pulsante di opzione. Detto questo, nulla impedisce a un programma di effettuare una richiesta [POST /people-01/do/validate] accompagnata da un parametro [spouse] fittizio. Dovremmo quindi verificare la validità di questo parametro. In questo caso, ci affidiamo alla nostra gestione delle eccezioni, che fa sì che venga visualizzata la pagina [exception.jsp] se il controller non gestisce l'eccezione autonomamente. Quindi, se la conversione del parametro [marie] in un valore booleano fallisce alla riga 34, verrà generata un'eccezione, con il risultato che la pagina [exception.jsp] verrà inviata al client. Questo comportamento è quello che ci serve.
- Righe 34–54: Recuperiamo il parametro [nbEnfants] e ne controlliamo il valore.
- Riga 56: recuperiamo il parametro [id] senza verificarne il valore
- Riga 58: Facciamo lo stesso per il parametro [version]
- Righe 60–65: Se il modulo non è valido, viene visualizzato nuovamente con i messaggi di errore generati in precedenza
- Righe 67–69: Se è valido, creiamo un nuovo oggetto [Person] utilizzando i campi del modulo
- Righe 70–78: la persona viene salvata. L'operazione di salvataggio potrebbe fallire. In un ambiente multiutente, la persona da modificare potrebbe essere stata cancellata o già modificata da qualcun altro. In questo caso, il livello [dao] genererà un'eccezione, che gestiamo qui.
- Riga 80: Se non si è verificata alcuna eccezione, il client viene reindirizzato all’URL [/do/list] per visualizzare il nuovo stato del gruppo.
- Riga 75: se si è verificata un'eccezione durante il salvataggio, richiediamo che il modulo iniziale venga visualizzato nuovamente, passando il messaggio di errore dell'eccezione (terzo parametro).
Il metodo [showFormulaire] (righe 84–101) crea il modello necessario per la pagina [edit.jsp] utilizzando i valori inseriti (request.getParameter(" ... ")). Si ricordi che i messaggi di errore sono già stati aggiunti al modello dal metodo [doValidatePersonne]. La pagina [edit.jsp] viene visualizzata alle righe 99–100.
14.9. Test dell'applicazione Web
Nella Sezione 14.1 sono stati presentati diversi test. Invitiamo il lettore a eseguirli nuovamente. Qui mostriamo ulteriori schermate che illustrano casi di conflitti di accesso ai dati in un ambiente multiutente:
[Firefox] sarà il browser dell'utente U1. L'utente U1 richiede l'URL [http://localhost:8080/personnes-01]:

[IE] sarà il browser dell'utente U2. L'utente U2 richiede lo stesso URL:

L'utente U1 inizia a modificare il record relativo a [Lemarchand]:

L'utente U2 fa lo stesso:

L'utente U1 apporta delle modifiche e salva:
![]() |
L'utente U2 fa lo stesso:
![]() |
L'utente U2 torna all'elenco delle persone che utilizzano il link [Annulla] nel modulo:

Trova la persona [Lemarchand] così come modificata da U1. Ora U2 elimina [Lemarchand]:
![]() |
U1 ha ancora la propria lista e vuole modificare nuovamente [Lemarchand]:
![]() |
U1 usa il link [Torna alla lista] per vedere cosa sta succedendo:

Scopre che [Lemarchand] non è più nell'elenco...
14.10. Conclusione
Abbiamo implementato l'architettura MVC all'interno di un'architettura a tre livelli [web, logica di business, DAO] utilizzando un esempio semplice di gestione di un elenco di persone. Questo ci ha permesso di applicare i concetti presentati nelle sezioni precedenti. Nella versione che abbiamo esaminato, l'elenco delle persone era conservato in memoria. Presto esploreremo versioni in cui questo elenco è memorizzato in una tabella di database.
Ma prima, introdurremo uno strumento chiamato Spring IoC, che facilita l'integrazione dei diversi livelli di un'applicazione a più livelli.

















