4. Azioni: il modello
Torniamo all'architettura di un'applicazione Spring MVC:
![]() |
Nel capitolo precedente abbiamo esaminato il processo che porta la richiesta [1] al controller e all’azione [2a] che la elaboreranno, un meccanismo chiamato routing. Abbiamo inoltre presentato le diverse risposte che un’azione può fornire al browser. Finora abbiamo presentato azioni che non sfruttavano la richiesta loro sottoposta. Una richiesta [1] trasporta con sé diverse informazioni che Spring MVC presenta all’azione sotto forma di un modello. Non bisogna confondere questo termine con il modello M di una vista V [2c] generata dall’azione:
![]() |
- la richiesta HTTP del cliente arriva in [1];
- in [2], le informazioni contenute nella richiesta verranno trasformate nel modello di azione [3], spesso (ma non necessariamente) una classe, che fungerà da input per l’azione [4];
- in [4], l’azione, a partire da questo modello, genererà una risposta. Questa avrà due componenti: una vista V [6] e il modello M di tale vista [5];
- la vista V [6] utilizzerà il proprio modello M [5] per generare la risposta HTTP destinata al cliente.
Nel modello MVC, l'azione [4] fa parte del C (controllore), il modello della vista [5] è l'M e la vista [6] è la V.
Questo capitolo esamina i meccanismi di collegamento tra le informazioni trasportate dalla richiesta, che sono per loro natura stringhe di caratteri, e il modello dell’azione, che può essere una classe con proprietà di vario tipo.
Nota: il termine [Modèle d'action] non è un termine riconosciuto.
Creiamo un nuovo controller per queste nuove azioni:
![]() |
Il controller [ActionModelController] sarà, per il momento, il seguente:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- riga 5: si ricorda che l'annotazione [@RestController] fa sì che la risposta inviata al client sia la serializzazione in stringa di caratteri del risultato delle azioni del controller;
4.1. [/m01]: parametri di un GET
Aggiungiamo la seguente azione [/m01]:
// ----------------------- recupero dei parametri con GET------------------------
@RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m01(String nom, String age) {
return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
- riga 4: l'azione accetta due parametri denominati [nom] e [age]. Essi saranno inizializzati con i parametri che riportano gli stessi nomi nella richiesta HTTP GET;
I risultati in Chrome per [1-3] sono i seguenti:
![]() |
- in [1], la richiesta GET con i parametri [nom] e [age];
- in [3], si vede che l'azione [/m01] ha effettivamente recuperato questi parametri;
4.2. [/m02]: parametri di un'azione POST
Aggiungiamo la seguente azione [/m02]:
// ----------------------- recupero dei parametri con POST------------------------
@RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
public String m02(String nom, String age) {
return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
- riga 4: l'azione accetta due parametri denominati [nom] e [age]. Essi saranno inizializzati con i parametri che riportano gli stessi nomi nella richiesta HTTP POST;
I risultati con [Advanced rest Client] sono i seguenti:
![]() |
- in [1-3], la query POST con i parametri [nom] e [age];
- in [4-5], si imposta l’intestazione HTTP [Content-Type] della richiesta POST. Deve essere [Content-Type: application/x-www-form-urlencoded];
- in [6], [Form Data] fornisce l’elenco dei parametri di un’operazione POST. Qui si vedono i parametri [nom] e [age];
- in [7], la risposta del server che mostra che l’azione [/m02] ha effettivamente recuperato i parametri [nom] e [age]; ;
4.3. [/m03]: parametri con lo stesso nome
Abbiamo visto nel paragrafo 2.5.2.8 che l'elenco a selezione multipla poteva inviare al server parametri con lo stesso nome. Vediamo come un'azione possa recuperarli. Aggiungiamo la seguente azione [/m03]:
// ----------------------- recupero dei parametri con lo stesso nome-----------------
@RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
public String m03(String nom[]) {
return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
- riga 2: l'azione accetta un parametro denominato [nome[]]. Qui verrà inizializzato con tutti i parametri che portano quel nome, sia in un GET che in un POST, poiché in questo caso il tipo di richiesta non è stato specificato;
I risultati sono i seguenti:
![]() |
- tramite un POST [1], si inviano i parametri [2];
- si inseriscono inoltre dei parametri in URL e [3];
- nel [4], i quattro parametri con lo stesso nome [nom]: [Query String parameters] sono i parametri di URL, mentre [Form Data] sono i parametri inviati;
- in [5], si vede che l’azione [/m03] ha recuperato i quattro parametri denominati [nom];
4.4. [/m04]: mappare i parametri dell’azione in un oggetto Java
Si consideri la seguente nuova azione [/m04]:
// ------ mappare i parametri in un oggetto (Command Object) ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- riga 3: l'azione ha come parametro una persona del seguente tipo:
public class Personne {
// identificativo
private Integer id;
// nome
private String nom;
// età
private int age;
....
// getter e setter
...
}
- per creare il parametro [Personne personne], Spring MVC crea un [new Personne()];
- quindi, se sono presenti parametri con i nomi dei campi [id, nom, age] dell’oggetto creato, lo istanzia con i campi tramite i relativi setter;
- riga 4: l'azione restituisce un tipo [Personne] che verrà quindi serializzato in una stringa di caratteri prima di essere inviato al client. Abbiamo visto che, per impostazione predefinita, la serializzazione effettuata era di tipo jSON. Il cliente dovrebbe quindi ricevere la stringa jSON relativa a una persona;
Ecco un esempio:
![]() |
- in [1], i parametri [id, nom, age] per costruire un oggetto [Personne];
- in [2], la stringa jSON relativa a questa persona;
Cosa succede se non si inviano tutti i campi relativi a una persona? Proviamo:
![]() |
- in [2], è stato inizializzato solo il parametro [id];
4.5. [/m05]: recuperare gli elementi di un URL
Si consideri la seguente nuova azione [/m05]:
// ----------------------- recuperare gli elementi dall'URL ------------------------
@RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
Map<String, String> map = new HashMap<String, String>();
map.put("a", a);
map.put("b", b);
return map;
}
- riga 2: l'URL elaborata ha la forma [/m05/{a}/x/{b}] dove {param} è un elemento parametro dell'URL;
- riga 3: gli elementi parametro di URL vengono recuperati con l’annotazione [@PathVariable];
- righe 4-6: gli elementi [a] e [b] recuperati vengono inseriti in un dizionario;
- riga 7: la risposta sarà la stringa jSON contenuta in questo dizionario;
I risultati sono i seguenti:
![]() |
4.6. [/m06]: recuperare elementi da URL e dai parametri
Si consideri la seguente nuova azione [/m06]:
// -------- recuperare elementi da URL e parametri---------------
@RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("a", a);
map.put("b", b);
map.put("c", c);
return map;
}
- riga 3: si recuperano sia elementi da URL e [Integer a, Double b] sia un parametro (GET o POST) da [Double c];
- righe 4-7: questi elementi vengono inseriti in un dizionario;
- riga 8: che costituisce la risposta del client, il quale riceverà quindi la stringa jSON da questo dizionario;
Ecco i risultati:
![]() |
Si noti il carattere / alla fine del percorso [http://localhost:8080/m06/100/x/200.43/]. Senza di esso, si ottiene il seguente risultato errato:
![]() |
4.7. [/m07]: accedere all’intera richiesta
Si consideri la seguente nuova azione [/m07]:
// ------ accedere alla richiesta HttpServletRequest ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// le intestazioni HTTP
Enumeration<String> headerNames = request.getHeaderNames();
StringBuffer buffer = new StringBuffer();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
}
return buffer.toString();
}
- riga 3: si chiede a Spring MVC di iniettare l’oggetto [HttpServletRequest request] che incapsula tutte le informazioni ottenibili sulla richiesta;
- righe 5-10: si recuperano tutte le intestazioni HTTP della richiesta per assemblarle in una stringa di caratteri che viene inviata al client (riga 11);
I risultati sono i seguenti:
![]() |
- in [1], le intestazioni HTTP della richiesta;
![]() |
- in [2], la risposta. Vi si ritrovano effettivamente tutte le intestazioni HTTP della richiesta.
4.8. [/m08]: accesso all'oggetto [Writer]
Consideriamo la seguente azione:
// ----------------------- iniezione di writer ------------------------
@RequestMapping(value = "/m08", method = RequestMethod.GET)
public void m08(Writer writer) throws IOException {
writer.write("Bonjour le monde !");
}
- riga 3: Spring MVC inietta l’oggetto [Writer writer] che consente di scrivere nel flusso della risposta al cliente;
- riga 3: l'azione restituisce un tipo [void], il che indica che deve costruire autonomamente la risposta al cliente;
- riga 4: aggiunta di un testo nel flusso della risposta al cliente;
I risultati sono i seguenti:
![]() |
- in [2], si nota che l'intestazione HTTP [Content-Type] non è stata inviata;
- in [3], la risposta;
4.9. [/m09]: accedere a un'intestazione HTTP
Consideriamo la seguente azione:
// ----------------------- iniezione di RequestHeader ------------------------
@RequestMapping(value = "/m09", method = RequestMethod.GET)
public String m09(@RequestHeader("User-Agent") String userAgent) {
return userAgent;
}
- riga 3: l'annotazione [@RequestHeader("User-Agent")] consente di recuperare l'intestazione HTTP [User-Agent];
- riga 4: si restituisce il testo di questa intestazione;
I risultati sono i seguenti:
![]() |
- in [2], l'intestazione HTTP [User-Agent];
![]() |
- in [3], l'azione [/m08] ha recuperato correttamente questa intestazione;
4.10. [/m10, /m11]: accesso a un cookie
Un cookie è in genere un'intestazione HTTP che il:
- server invia una prima volta al client;
- il client poi rinvia sistematicamente al server;
Creiamo innanzitutto un'azione che crei il cookie:
// ----------------------- creazione di cookie ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- riga 3: si inserisce l’oggetto [HttpServletResponse response] per avere il controllo totale sulla risposta;
- riga 4: si crea un cookie con una chiave [cookie1] e un valore [remember me] (Nota: i caratteri accentati nel valore di un cookie causano errori);
- riga 3: l'azione non restituisce alcun risultato. Inoltre, non scrive nulla nel corpo della risposta. Il client riceverà quindi un documento vuoto. La risposta viene utilizzata solo per aggiungervi l'intestazione HTTP di un cookie;
Vediamo i risultati:
![]() |
- in [1]: la richiesta;
- in [2]: la risposta è vuota;
- in [3]: il cookie creato dall’azione;
Ora creiamo un'azione per recuperare questo cookie che il browser invierà d'ora in poi ad ogni richiesta:
// ----------------------- iniezione di cookie ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- riga 3: l'annotazione [@CookieValue("cookie1")] consente di recuperare il cookie con chiave [cookie1];
- riga 4: questo valore costituirà la risposta inviata al client;
Vediamo i risultati:
![]() |
- in [2], si vede che il browser restituisce il cookie;
- in [3], l’azione lo ha recuperato correttamente;
4.11. [/m12]: accedere al corpo di un POST
I parametri inviati sono solitamente accompagnati dall’intestazione HTTP [Content-Type: application/x-www-form-urlencoded]. È possibile accedere all’intera stringa inviata. Creiamo la seguente azione:
// ----------- recupero del corpo di un POST di tipo String------------------------
@RequestMapping(value = "/m12", method = RequestMethod.POST)
public String m12(@RequestBody String requestBody) {
return requestBody;
}
- riga 3: l'annotazione [@RequestBody] consente di recuperare il corpo del POST. In questo caso, si suppone che quest'ultimo sia di tipo [String];
- riga 4: si restituisce questo corpo al cliente;
Ecco un primo esempio:
![]() |
- in [2], i valori inviati;
- in [3], l'intestazione HTTP [Content-Type] della richiesta;
- in [4], la risposta del server;
I parametri inviati via POST non hanno sempre la forma semplice [p1=v1&p2=v2] che abbiamo spesso utilizzato finora. Prendiamo un caso più complesso:
![]() |
- in [2-3]: si inseriscono i valori inviati nel formato [clé:value];
- in [5], la stringa che è stata inviata;
Con il tipo [Content-Type: application/x-www-form-urlencoded], la stringa da inviare deve avere il formato [p1=v1&p2=v2]. Se si desidera inviare qualsiasi cosa, si utilizzerà il tipo [Content-Type: text/plain]. Ecco un esempio:
![]() |
- in [2-3], si crea l'intestazione HTTP [Content-Type]. Per impostazione predefinita, verrà utilizzato [5] al posto di quello definito in [6]. L’attributo [charset=utf-8] è importante. Senza di esso, si perdono i caratteri accentati della stringa inviata;
- in [4], la stringa inviata viene recuperata correttamente in [7];
4.12. [/m13, /m14]: recuperare i valori inviati in jSON
È possibile inviare parametri con l'intestazione HTTP [Content-Type: application/json]. Creiamo la seguente azione:
// ----------------------- recupero del corpo jSON di un POST
@RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
public String m13(@RequestBody Personne personne) {
return personne.toString();
}
- riga 2: [consumes = "application/json"] specifica che l’azione richiede un corpo jSON;
- riga 3: [@RequestBody] rappresenta tale corpo. Questa annotazione è stata associata a un oggetto di tipo [Personne]. Il corpo jSON verrà automaticamente deserializzato in questo oggetto;
- riga 4: si utilizza il metodo [Personne].toString() per restituire un valore diverso dalla stringa jSON inviata;
Ecco un esempio:
![]() |
- in [2], la stringa jSON inviata;
- in [3], il [Content-Type] della richiesta;
- in [4], la risposta del server;
Si può ottenere lo stesso risultato in modo diverso:
// ----------------------- recuperare il corpo jSON di un POST 2 -------------------
@RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
return personne.toString();
}
- riga 2: si è specificato che il metodo si aspettava un flusso di tipo [text/plain]. Spring MVC tratterà quindi il corpo della richiesta come un tipo [String] (riga 3);
- riga 4: si deserializza la stringa jSON in un oggetto [Personne] (cfr. paragrafo 9.7, pagina 542);
I risultati sono i seguenti:
![]() |
- in [3], correggere in [text/plain];
4.13. [/m15]: recuperare la sessione
Torniamo all’architettura di esecuzione di un’azione:
![]() |
La classe del controller viene istanziata all'inizio della richiesta del client e distrutta al termine della stessa. Pertanto, non può essere utilizzata per memorizzare dati tra due richieste, anche se viene chiamata ripetutamente. Si potrebbero voler memorizzare due tipi di dati:
- dati condivisi da tutti gli utenti dell’applicazione web. Si tratta in genere di dati in sola lettura;
- dati condivisi dalle richieste dello stesso cliente. Questi dati vengono memorizzati in un oggetto denominato Sessione. Si parla quindi di sessione del cliente per indicare la memoria del cliente. Tutte le richieste di un cliente hanno accesso a questa sessione e possono memorizzarvi e leggerne le informazioni.
![]() |
Di seguito illustriamo i tipi di memoria a cui ha accesso un'azione:
- la memoria dell’applicazione, che nella maggior parte dei casi contiene dati in sola lettura ed è accessibile a tutti gli utenti;
- la memoria di un utente specifico, o sessione, che contiene dati in lettura/scrittura ed è accessibile alle richieste successive dello stesso utente;
- non rappresentata sopra, esiste una memoria di richiesta, o contesto di richiesta. La richiesta di un utente può essere elaborata da diverse azioni successive. Il contesto della richiesta consente a un’azione 1 di trasmettere informazioni a un’azione 2.
Vediamo un primo esempio che mette in luce queste diverse memorie:
// ----------------------- recuperare la sessione ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// si recupera l'oggetto chiave [compteur] dalla sessione
Object objCompteur = session.getAttribute("compteur");
// lo si converte in un intero per incrementarlo
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// lo si reinserisce nella sessione
session.setAttribute("compteur", iCompteur);
// viene restituito come risultato dell'azione
return String.valueOf(iCompteur);
}
Spring MVC mantiene la sessione dell’utente in un oggetto di tipo [HttpSession].
- riga 3: si chiede a Spring MVC di iniettare l’oggetto [HttpSession] nei parametri dell’azione;
- riga 5: da questi si recupera un attributo denominato [compteur]. Una sessione si comporta come un dizionario, un insieme di coppie [clé, valeur]. Se la chiave [compteur] non esiste nella sessione, si recupera un puntatore null;
- riga 7: il valore associato alla chiave [compteur] sarà di tipo [Integer];
- riga 8: incremento del contatore;
- riga 10: aggiornamento del contatore nella sessione;
- riga 12: il valore del contatore viene inviato al client;
Quando [/m15] verrà eseguita per la:
- prima volta, alla riga 12 il contatore avrà il valore 1;
- la seconda volta, alla riga 5, si recupererà questo valore 1 per portarlo a 2;
- ...
Ecco un esempio di esecuzione:
![]() |
- in [1], si ottiene effettivamente il primo valore del contatore;
- in [2], il server ha inviato un cookie di sessione. Ha come chiave [JSESSIONID] e come valore una stringa di caratteri unica per ogni utente. Ricordiamo che il browser rinvia sistematicamente i cookie che riceve. Pertanto, quando si richiederà l’azione [/m15] una seconda volta, il client rinvierà questo cookie, il che consentirà al server di riconoscerlo e di ricollegarlo alla sua sessione. È in questo modo che viene mantenuta la memoria dell’utente;
Vediamo la seconda richiesta:
![]() |
- in [3], si nota che il client rinvia il cookie di sessione. Si può osservare che nella risposta del server non è più presente questo cookie di sessione. Ora è il client a inviarlo per farsi riconoscere;
- in [4], il secondo valore del contatore. È stato effettivamente incrementato;
4.14. [/m16]: recuperare un oggetto con ambito [session]
Si potrebbe voler inserire tutti i dati della sessione di un utente in un unico oggetto e inserire solo quest’ultimo nella sessione. Seguiamo questa strada. Inseriamo il contatore nel seguente oggetto [SessionModel]:
![]() |
package istia.st.sprinmvc.models;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {
private int compteur;
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
- riga 7: l'annotazione [@Component] è un'annotazione Spring (riga 5) che rende la classe [SessionModel] un componente il cui ciclo di vita è gestito da Spring;
- riga 8: l'annotazione [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] è anch'essa un'annotazione Spring (righe 3-4). Quando Spring MVC la incontra, la classe corrispondente viene creata e inserita nella sessione dell'utente. L'attributo [proxyMode = ScopedProxyMode.TARGET_CLASS] è importante. È grazie a esso che Spring MVC crea un'istanza per ogni utente e non un'unica istanza per tutti gli utenti (singleton);
- riga 11: il contatore;
Affinché questo nuovo componente Spring venga riconosciuto, è necessario verificare la configurazione dell’applicazione nella classe [Application]:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- riga 9: i componenti Spring vengono cercati nel pacchetto [istia.st.springmvc.controllers]. Questo non è più sufficiente. Modifichiamo questa riga come segue:
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
Abbiamo aggiunto il pacchetto in cui si trova la classe [SessionModel].
Ora aggiungiamo la seguente azione:
@Autowired
private SessionModel session;
// ------ gestione di un oggetto con ambito (scope) di sessione [Autowired] -----------
@RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m16() {
session.setCompteur(session.getCompteur() + 1);
return String.valueOf(session.getCompteur());
}
- righe 1-2: il componente Spring [SessionModel] viene iniettato come [@Autowired] nel controller. Ricordiamo che un controller Spring è un singleton. È quindi paradossale iniettarvi un componente con un ambito più ristretto, in questo caso [Session]. È qui che entra in gioco l’annotazione [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] del componente [SessionModel]. Ogni volta che il codice del controller accede al campo [session] della riga 2, viene eseguito un metodo proxy per rendere la sessione della richiesta attualmente elaborata dal controller;
- riga 6: l'oggetto [HttpSession] non è più necessario nei parametri dell'azione;
- riga 7: si recupera/incrementa il contatore;
- riga 8: si restituisce il suo valore;
Ecco un esempio di esecuzione:
La prima volta
![]() |
La seconda volta
![]() |
Ora prendiamo un altro browser che rappresenterà un secondo utente. In questo caso utilizziamo il browser Opera:
![]() |
Come si vede in [1], questo secondo utente ottiene un valore del contatore pari a 1. Ciò dimostra che la sua sessione e quella del primo utente sono diverse. Se osserviamo gli scambi client/server (Ctrl-Maiusc-I anche per Opera), vediamo in [2] che questo secondo utente ha un cookie di sessione diverso da quello del primo utente. È questo che garantisce l’indipendenza delle sessioni.
4.15. [/m17]: recuperare un oggetto di ambito [application]
Torniamo all’architettura di esecuzione di un’azione:
![]() |
Sappiamo come costruire la sessione dell’utente. Ora costruiremo un oggetto di ambito [application] il cui contenuto sarà di sola lettura e accessibile a tutti gli utenti. Introduciamo la classe [ApplicationModel] che sarà l’oggetto di ambito [application]:
![]() |
package istia.st.springmvc.models;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class ApplicationModel {
// contatore
private AtomicLong compteur = new AtomicLong(0);
// getter e setter
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- riga 5: l'annotazione [@Component] fa sì che la classe [ApplicationModel] sia un componente gestito da Spring. La natura predefinita dei componenti Spring è il tipo [singleton]: il componente viene creato in un unico esemplare quando viene istanziato il contenitore Spring, ovvero in genere all’avvio dell’applicazione. Possiamo utilizzare questo ciclo di vita per memorizzare nel singleton informazioni di configurazione che saranno accessibili a tutti gli utenti;
- riga 11: un contatore di tipo [AtomicLong]. Questo tipo dispone di un metodo [incrementAndGet] detto atomico. Ciò significa che un thread che esegue questo metodo ha la garanzia che nessun altro thread leggerà il valore del contatore (Get) tra la sua lettura (Get) e il suo incremento (increment) da parte del primo thread; ciò causerebbe errori poiché due thread leggerebbero lo stesso valore del contatore, che invece di essere incrementato di due verrebbe incrementato di uno;
Creiamo la seguente nuova azione [/m17]:
@Autowired
private ApplicationModel application;
// ----- gestire un oggetto con ambito di applicazione [Autowired] ------------------------
@RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m17() {
return String.valueOf(application.getCompteur().incrementAndGet());
}
- righe 1-2: si inserisce il componente [ApplicationModel] nel controller. Si tratta di un singleton. Pertanto, ogni utente avrà un riferimento allo stesso oggetto;
- riga 7: restituiamo il contatore di ambito [application] dopo averlo incrementato;
Ecco due esempi, uno con Chrome e l’altro con Opera:
![]() | ![]() |
Come si vede sopra, entrambi i browser hanno utilizzato lo stesso contatore, cosa che non avveniva con la sessione. Questi due browser rappresentano due utenti diversi che hanno entrambi accesso ai dati dell’ambito [application]. In generale, si eviterà di inserire negli oggetti dell’ambito [application] informazioni in lettura/scrittura, come è stato fatto sopra con il contatore. Infatti, i thread di esecuzione dei diversi utenti accedono contemporaneamente ai dati dell’ambito [application]. Se sono presenti informazioni in scrittura, è necessario sincronizzare gli accessi in scrittura come è stato fatto sopra con il tipo [AtomicLong]. Gli accessi concorrenti sono fonte di errori di programmazione. Pertanto, è preferibile inserire solo informazioni di sola lettura negli oggetti di ambito [application].
4.16. [/m18]: recuperare un oggetto di ambito [session] con [@SessionAttributes]
Esiste un altro modo per recuperare informazioni dall'ambito [session]. Inseriremo nella sessione il seguente oggetto:
package istia.st.springmvc.models;
public class Container {
// il contatore
public int compteur=10;
// i getter e i setter
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
Utilizzeremo questo oggetto con le due azioni seguenti:
// utilizzo di [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// qui si inserisce la chiave [container] nella sessione
session.setAttribute("container", new Container());
}
// utilizzo di [@ModelAttribute] ----------------------
// qui verrà inserita la chiave [container] della sessione
@RequestMapping(value = "/m19", method = RequestMethod.GET)
public String m19(@ModelAttribute("container") Container container) {
container.setCompteur(1 + container.getCompteur());
return String.valueOf(container.getCompteur());
}
- righe 3-6: l'azione [/m18] non restituisce alcun risultato. Serve solo a creare un oggetto nella sessione con la chiave [container];
- riga 11: nell’azione [/m19] si utilizza l’annotazione [@ModelAttribute]. Il comportamento di questa annotazione è piuttosto complesso. Il parametro [container] di questa annotazione può indicare diverse cose e, in particolare, un oggetto della sessione. A tal fine, è necessario che quest’ultimo sia stato dichiarato con un’annotazione [@SessionAttributes] sulla classe stessa:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- la riga 2 sopra riportata indica che la chiave [container] fa parte degli attributi della sessione;
Riassumiamo:
- in [/m18], la chiave [container] viene inserita nella sessione;
- l'annotazione [@SessionAttributes({"container"})] fa sì che questa chiave possa essere inserita in un parametro annotato con [@ModelAttribute("container")];
- non visibile nell’esempio di esecuzione che seguirà, ma un’informazione annotata con [@ModelAttribute] diventa automaticamente parte del modello M trasmesso alla vista V;
Ecco un esempio di esecuzione. Innanzitutto, si inserisce la chiave [container] nella sessione tramite l'azione [/m18] [1]. Successivamente, si chiama due volte l’azione [/m19] per vedere il contatore incrementarsi.
![]() |
4.17. [/m20-/m23]: inserimento di informazioni con [@ModelAttribute]
Consideriamo la seguente nuova azione:
// l'attributo p farà parte di tutti i modelli di vista [Model] ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------istanza di @ModelAttribute --------------------------
// verrà inserito se presente nella sessione
// verrà inserito se il controller ha definito un metodo per questo attributo
// può provenire dai campi di URL se esiste un convertitore da String al tipo dell'attributo
// altrimenti viene generato con il costruttore predefinito
// successivamente gli attributi del modello vengono inizializzati con i parametri di GET o di POST
// il risultato finale farà parte del modello generato dall'azione
// l'attributo p viene inserito negli argomenti------------------------
@RequestMapping(value = "/m20", method = RequestMethod.GET)
public Personne m20(@ModelAttribute("p") Personne personne) {
return personne;
}
- righe 2-5: definiscono un attributo del modello denominato [p]. Si tratta del modello M di una vista V, modello rappresentato da un tipo [Model] in Spring MVC. Un modello si comporta come un dizionario di coppie [clé, valeur]. In questo caso, la chiave [p] è associata all’oggetto [Personne] costruito dal metodo [getPersonne]. Il nome del metodo può essere qualsiasi;
- riga 17: l’attributo modello della chiave [p] viene iniettato nei parametri dell’azione. Tale iniezione avviene secondo le regole delle righe 8-12. In questo caso, ci troviamo nella situazione definita alla riga 9. Pertanto, alla riga 17 il parametro [Personne personne] sarà l'oggetto [Personne(7,'abcd',14)];
- riga 18: si restituisce l'oggetto [personne] per la verifica. Questo verrà serializzato in jSON prima di essere inviato al cliente.
Ecco un esempio:
![]() |
Ora esaminiamo l'azione seguente:
// --------- l’attributo p diventa automaticamente parte del modello M della vista V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Un'azione che intende visualizzare una vista V deve costruire il modello M di quest'ultima. Spring MVC gestisce quest'ultimo con un tipo [Model] che può essere iniettato nei parametri dell'azione. Inizialmente questo modello è vuoto oppure contiene le informazioni contrassegnate con l’annotazione [@ModelAttribute]. L’azione arricchisce o meno questo modello prima di trasmetterlo a una vista.
- riga 3: inserimento del modello M;
- riga 4: si vuole vedere cosa c'è dentro. Lo si serializza in una stringa di caratteri per inviarlo al client. In questo caso, verrà utilizzato il metodo [Personne.toString]. È quindi necessario che esista;
Ecco un'esecuzione:
![]() |
Sopra, si vede che le istruzioni:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
hanno creato una voce [p, Personne(7,'abcd',14)] nel modello. È sempre così.
Consideriamo ora il seguente caso:
// altrimenti viene costruito con il costruttore predefinito
// successivamente gli attributi del modello vengono inizializzati con i parametri di GET o di POST
con la seguente azione:
// --------- l'attributo del modello [param1] fa parte del modello ma non è inizializzato
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- riga 3: l'attributo modello della chiave [param1] non esiste. In questo caso, il tipo associato deve avere un costruttore predefinito. È il caso, in questo caso, del tipo [String], ma non è possibile scrivere [@ModelAttribute("param1") Integer p1] poiché la classe [Integer] non dispone di un costruttore predefinito;
- riga 4: si restituisce il modello per verificare se l'attributo chiave del modello [param1] ne fa parte;
Ecco un esempio di esecuzione:
![]() |
L'attributo del modello [param1] è effettivamente presente nel modello, ma il metodo [toString] del valore associato non fornisce alcuna indicazione su tale valore.
Consideriamo ora la seguente azione, in cui inseriamo esplicitamente un’informazione nel modello:
// --------- l'attributo del modello [param2] è inserito esplicitamente nel modello
@RequestMapping(value = "/m23", method = RequestMethod.GET)
public String m23(String p2, Model model) {
model.addAttribute("param2",p2);
return model.toString();
}
- riga 4: il valore [p2] recuperato alla riga 3 viene inserito nel modello associato alla chiave [param2]:
Ecco un esempio di esecuzione:
![]() |
Le regole cambiano se il parametro dell’azione è un oggetto. Ecco un primo esempio:
// ------ l'attributo del modello [unePersonne] viene inserito automaticamente nel modello
@RequestMapping(value = "/m23b", method = RequestMethod.GET)
public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
return model.toString();
}
L'azione non modifica il modello che le è stato fornito. Il risultato è il seguente:
![]() |
Si nota che l'annotazione [@ModelAttribute("unePersonne") Personne p1] ha inserito la persona [p1] nel modello, associata alla chiave [unePersonne].
Consideriamo ora la seguente azione:
// --------- la persona p1 viene inserita automaticamente nel modello
// -------- con come chiave il nome della sua classe con la prima lettera minuscola
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- riga 4: non è stata inserita l'annotazione [@ModelAttribute];
Il risultato è il seguente:
![]() |
Si nota che la presenza del parametro [Personne p1] ha inserito la persona [p1] nel modello, associata alla chiave [personne], che è il nome della classe [Personne] con il primo carattere in minuscolo.
4.18. [/m24]: convalida del modello dell’azione
Consideriamo il seguente modello di azione [ActionModel01]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// dati
@NotNull
private Integer a;
@NotNull
private Double b;
// getter e setter
...
}
- righe 8 e 9: l'annotazione [@NotNull] è un vincolo di convalida che indica che il dato annotato non può assumere il valore null;
Esaminiamo ora la seguente azione:
// ----------------------- convalida di un modello ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// errori?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// scorrimento dell'elenco degli errori
for (FieldError error : result.getFieldErrors()) {
buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
}
map.put("errors", buffer.toString());
} else {
// nessun errore
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
- riga 3: verrà istanziato un oggetto [ActionModel01] e i suoi campi [a, b] saranno inizializzati con parametri che hanno gli stessi nomi. L'annotazione [@Valid] indica che i vincoli di validità devono essere verificati. I risultati di questa verifica saranno inseriti nel parametro di tipo [BindingResult] (secondo parametro). Verranno effettuate le seguenti verifiche:
- a causa delle annotazioni [@NotNull], i parametri [a] e [b] devono essere presenti;
- a causa del tipo [Integer a], il parametro [a], che per sua natura è di tipo [String], deve essere convertibile in un tipo [Integer];
- a causa del tipo [Double b], il parametro [b], che per sua natura è di tipo [String], deve essere convertibile in un tipo [Double];
Con l'annotazione [@Valid], gli errori di convalida verranno riportati nel parametro [BindingResult result]. Senza l'annotazione [@Valid], gli errori di convalida causano un arresto anomalo dell'azione e il server invia al client una risposta HTTP con stato 500 (Errore interno del server).
- riga 3: il risultato dell’azione è di tipo [Map]. Sarà la stringa jSON di questo risultato a essere inviata al cliente. Vengono creati due tipi di dizionario:
- in caso di errore, un dizionario con una voce ['errors', value], dove [value] è una stringa di caratteri che descrive tutti gli errori (riga 13);
- in caso di esito positivo, un dizionario con una voce ['data',value], dove [value] è a sua volta un dizionario con due voci: ['a', value], ['b', value] (riga 19);
- righe 9-12: per ogni errore [error] rilevato, si costruisce la stringa [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
- il primo elemento è il campo errato, [a] o [b],
- il secondo elemento è il valore rifiutato, ad esempio [x],
- il terzo elemento è un elenco di codici di errore. Ne vedremo a breve la funzione;
- il quarto elemento è il codice dell’errore. Fa parte dell’elenco precedente;
- l'ultimo elemento è il messaggio di errore predefinito. È infatti possibile avere più messaggi di errore;
Ecco alcuni esempi di esecuzione:
![]() |
Da quanto sopra si evince che:
- l'assegnazione di 'x' al campo [ActionModel01.a] non è andata a buon fine e il messaggio di errore ne spiega il motivo;
- l’assegnazione di 'y' al campo [ActionModel01.b] non è andata a buon fine e il messaggio di errore ne spiega il motivo;
Si notino i codici di errore relativi al campo [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Torneremo su questi codici di errore quando sarà necessario personalizzare il messaggio di errore. Si noti che il codice di errore è [typeMismatch].
Un altro esempio:
![]() |
In questo caso, non sono stati passati i parametri [a] e [b]. I validatori [@NotNull] del modello di azione [ActionModel01] hanno quindi svolto la loro funzione;
Finalmente, valori corretti:
![]() |
4.19. [m/24]: personalizzazione dei messaggi di errore
Torniamo a uno screenshot dell’esempio precedente:
![]() |
Qui sopra vediamo i messaggi di errore predefiniti. È chiaro che non possiamo mantenerli in un'applicazione reale. È possibile definire questi messaggi di errore. Per farlo, ci aiuteremo con i codici di errore. Sopra vediamo che l’errore relativo al campo [a] presenta i seguenti codici: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Questi codici di errore vanno dal più specifico al meno specifico:
- [typeMismatch.actionModel01.a]: errore di tipo nel campo [a] di tipo [ActionModel01];
- [typeMismatch.a]: errore di tipo su un campo denominato [a];
- [typeMismatch.java.lang.Integer]: errore di tipo su un tipo Integer;
- [typeMismatch]: errore di tipo;
Si nota inoltre che il codice di errore relativo al campo [a], ottenuto da [error.getCode()], è [typeMismatch] (cfr. screenshot sopra).
Inseriremo i messaggi di errore in un file di proprietà:
![]() |
Il file [messages.properties] sopra riportato sarà il seguente:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier
Ogni riga ha la seguente struttura:
In questo caso, la chiave sarà un codice di errore e il messaggio sarà il messaggio di errore associato a tale codice.
Ricordiamo i codici di errore per i due campi:
- [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], quando il parametro [a] non è valido;
- [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] quando il parametro [b] non è valido;
- [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] quando il parametro [a] è assente;
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] quando il parametro [b] è assente;
Il file [messages.properties] deve contenere un messaggio di errore per tutti i possibili casi di errore. Nel caso in cui:
- i parametri [a] e [b] sono assenti, verrà utilizzato il codice [NotNull];
- in caso di parametro [a] errato, abbiamo inserito messaggi per due codici [typeMismatch.actionModel01.a, typeMismatch]. Vedremo quale verrà utilizzato;
- in caso di parametro [b] non corretto, verrà utilizzato il codice [typeMismatch];
Affinché venga utilizzato il file [messages.properties], è necessario configurare Spring:
![]() |
Rimuoviamo le annotazioni di configurazione dalla classe [Application]:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Config.class, args);
}
}
- riga 8: viene avviata l'applicazione Spring Boot. Il primo parametro del metodo statico [SpringApplication.run] è la classe che ora configura l'applicazione;
La classe [Config] è la seguente:
package istia.st.springmvc.main;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
}
- righe 11-13: sono presenti le annotazioni di configurazione che in precedenza si trovavano nella classe [Application];
- riga 14: per configurare un'applicazione Spring MVC, è necessario estendere la classe [WebMvcConfigurerAdapter];
- riga 15: l'annotazione [@Bean] introduce un componente Spring, un singleton;
- riga 16: si definisce un bean denominato [messageSource] (il nome del metodo). Questo bean serve a definire i file dei messaggi dell'applicazione e deve obbligatoriamente avere questo nome;
- righe 17-19: indicano a Spring che il file dei messaggi:
- si trova nella cartella [i18n] nel Classpath del progetto (riga 18),
- si chiama [messages.properties] (riga 18). In realtà, il termine [messages] è la radice dei nomi dei file dei messaggi piuttosto che il nome stesso. Vedremo che, nell’ambito dell’internazionalizzazione, possono esserci diversi file dei messaggi, uno per ogni cultura gestita. Si può quindi avere [messages_fr.properties] per la lingua francese e [messages_en.properties] per la lingua inglese. I suffissi aggiunti alla radice [messages] sono standardizzati. Non è possibile inserire qualsiasi cosa;
Nel progetto STS, è necessario inserire la cartella [i18n] nella cartella delle risorse poiché questa viene inserita nel Classpath del progetto:
![]() |
Per utilizzare questo file, creiamo la seguente nuova azione:
// convalida di un modello, gestione dei messaggi di errore ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// il dizionario dei risultati
Map<String, Object> map = new HashMap<String, Object>();
// il contesto dell'applicazione Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// locale
Locale locale = RequestContextUtils.getLocale(request);
// di errori?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// ricerca del messaggio di errore in base ai codici di errore
// il messaggio viene cercato nei file dei messaggi
// i codici di errore sotto forma di tabella
String[] codes = error.getCodes();
// sotto forma di stringa
String listCodes = String.join(" - ", codes);
// ricerca
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// è stato trovato?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// è stato trovato - si aggiunge il messaggio di errore alla stringa dei messaggi di errore
buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
String.join(" - ", msg)));
}
map.put("errors", buffer.toString());
} else {
// ok
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
Questo codice è analogo a quello dell'azione [/m24]. Spieghiamo le differenze:
- riga 3: si inietta la richiesta [HttpServletRequest request] nei parametri dell’azione. Ne avremo bisogno;
- righe 7-8: recuperiamo il contesto Spring. Questo contesto contiene tutti i bean Spring dell’applicazione. Consente inoltre di accedere ai file dei messaggi;
- riga 10: si recupera la locale dell’applicazione. Questo termine viene spiegato più avanti;
- righe 15-31: per ogni errore, si cerca un messaggio corrispondente a uno di questi codici di errore. La ricerca avviene nell’ordine dei codici presenti in [error.getCodes()]. Non appena viene trovato un messaggio, ci si ferma;
- riga 26: come recuperare un messaggio in [messages.properties]:
- il primo parametro è il codice cercato in [messages.properties],
- il secondo è un array di parametri poiché a volte i messaggi sono configurati. In questo caso non è così,
- il terzo è la locale utilizzata (ottenuta alla riga 10). La locale indica la lingua utilizzata: [fr_FR] per il francese di Francia, [en_US] per l'inglese di USA. Il messaggio viene cercato in messages_[locale].properties, quindi ad esempio in [messages_fr_FR.properties]. Se questo file non esiste, il messaggio viene cercato in [messages_fr.properties]. Se questo file non esiste, il messaggio viene cercato in [messages.properties]. È quest'ultimo caso che funzionerà per noi;
- righe 25-29: in modo un po’ inaspettato, quando si cerca un codice inesistente in un file di messaggi, si ottiene un’eccezione anziché un puntatore nullo;
- righe 33-35: si gestisce il caso in cui non sia presente alcun messaggio di errore;
- righe 37-38: si costruisce la stringa di errore. In essa si includono le impostazioni locali e il messaggio di errore trovato;
Ecco alcuni esempi di esecuzione:
![]() |
Si nota che:
- la locale dell'applicazione è [fr_FR]. Si tratta di un valore predefinito poiché non abbiamo fatto nulla per inizializzarla;
- che il messaggio utilizzato per entrambi i campi è il seguente:
NotNull=Le champ ne peut être vide
Un altro esempio:
![]() |
Si nota che:
- il messaggio di errore utilizzato per il parametro [a] è il seguente:
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
- il messaggio di errore utilizzato per il parametro [b] è il seguente:
typeMismatch=Format invalide
Perché due messaggi diversi? Per il parametro [a], erano possibili due messaggi:
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
I codici di errore sono stati esaminati nell’ordine della tabella [error.getCodes()]. Si è riscontrato che tale ordine va dal codice più specifico a quello più generico. Ecco perché il codice [typeMismatch.model01.a] è stato individuato per primo.
4.20. [/m25]: internazionalizzazione di un’applicazione Spring MVC
Ora che sappiamo come personalizzare i messaggi di errore in francese, vorremmo averli anche in inglese, il che ci porta all’internazionalizzazione di un’applicazione Spring MVC. Per gestirla, amplieremo la classe di configurazione [Config], che diventerà la seguente:
package istia.st.springmvc.main;
import java.util.Locale;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
@Bean
public CookieLocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieName("lang");
localeResolver.setDefaultLocale(new Locale("fr"));
return localeResolver;
}
}
- righe 28-32: si crea un intercettatore di richiesta. Un intercettatore di richiesta estende l’interfaccia [HandlerInterceptor]. Una classe di questo tipo ispeziona la richiesta in entrata prima che venga elaborata da un’azione. In questo caso, l’intercettatore [localeChangeInterceptor] cercherà un parametro denominato [lang] nella richiesta in entrata, GET o POST e modificherà le impostazioni locali dell’applicazione in base a tale parametro. Pertanto, se il parametro è [lang=en_US], la lingua dell'applicazione diventerà l'inglese di USA;
- righe 34-37: si ridefinisce il metodo [WebMvcConfigurerAdapter.addInterceptors] per aggiungere l'intercettatore precedente;
- righe 39-45: servono a configurare il modo in cui la lingua verrà incapsulata in un cookie. È noto che un cookie può fungere da memoria dell’utente, poiché il browser client lo rinvia sistematicamente al server. L’intercettatore [localeChangeInterceptor] precedente crea un cookie che incapsula la lingua. La riga 42 assegna a questo cookie il nome [lang]. Il cookie viene utilizzato anche per modificare l’impostazione locale;
- riga 43: indica che, in assenza del cookie [lang], l’impostazione locale sarà [fr];
In sintesi, l’impostazione locale di una richiesta può essere definita in due modi:
- passando un parametro denominato [lang];
- inviando un cookie denominato [lang]. Questo cookie viene creato automaticamente al termine del metodo precedente;
Per utilizzare questa impostazione locale, creeremo dei file di messaggi per le impostazioni locali [fr] e [en]:
![]() |
Il file [messages_fr.properties] è il seguente:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Il file [messages_en.properties] è il seguente:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Il file [messages.properties] è una copia del file [messages_en.properties]. Si ricorda che il file [messages.properties] viene utilizzato quando non viene trovato alcun file corrispondente alla lingua della richiesta. Nel nostro caso, se l’utente invia un parametro [lang=en], poiché il file [messages_en.properties] non esiste, verrà utilizzato il file [messages.properties]. L’utente riceverà quindi i messaggi in inglese.
Proviamo. Innanzitutto, nell’ambiente di sviluppo di Chrome (Ctrl-Maiusc-I), controllate i vostri cookie:
![]() |
Se avete un cookie denominato [lang], eliminatelo. Quindi, con Chrome, richiedete URL e [http://localhost:8080/m25]:
![]() |
Il browser ha inviato le seguenti intestazioni HTTP:
Si nota che in queste intestazioni non è presente alcun cookie [lang]. In questo caso, il nostro codice utilizza l’impostazione locale [fr]. È quanto mostra la schermata. Proviamo con un altro caso:
![]() |
- in [1], abbiamo impostato il parametro [lang=en] per passare alla locale [en];
- in [2], si vede la nuova impostazione locale;
- in [3], il messaggio è ora in inglese;
Ora diamo un'occhiata agli scambi in HTTP:
![]() |
Come si vede sopra, il server ha restituito un cookie [lang]. Ciò ha una conseguenza importante: la lingua della prossima richiesta sarà nuovamente [en] a causa del cookie [lang] che verrà restituito dal browser. Dovremmo quindi mantenere i messaggi in inglese. Verifichiamolo:
![]() |
Come si vede sopra, l’impostazione locale è rimasta [en]. A causa del cookie inviato sistematicamente dal browser, rimarrà tale finché l’utente non la modificherà inviando il parametro [lang] come segue:
![]() |
4.21. [/m26]: inserimento della lingua nel modello dell'azione
Nell’esempio precedente abbiamo visto un modo per recuperare la locale dalla richiesta:
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
...
// locale
Locale locale = RequestContextUtils.getLocale(request);
// errori?
La locale può essere inserita direttamente nei parametri dell’azione. Ecco un esempio:
@RequestMapping(value = "/m26", method = RequestMethod.GET)
public String m26(Locale locale) {
return String.format("locale=%s", locale.toString());
}
![]() | ![]() |
![]() |
Come si può vedere sopra, non viene effettuata alcuna verifica della validità delle impostazioni locali richieste. Tuttavia, la richiesta successiva del browser genera un'eccezione lato server poiché il cookie delle impostazioni locali che riceve non è corretto.
4.22. [/m27]: verificare la validità di un modello con Hibernate Validator
Consideriamo la seguente nuova azione:
//convalida di un modello con Hibernate Validator ------------------------
@RequestMapping(value = "/m27", method = RequestMethod.POST)
public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// errori?
if (result.hasErrors()) {
// scorrimento dell'elenco degli errori
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// nessun errore
map.put("data", data);
}
return map;
}
Si tratta di codice che abbiamo già visto più volte:
- riga 3: l'azione [/m27] viene richiesta tramite un POST;
- righe 8-11: ogni errore sarà identificato da [champ, message] con:
- campo: il campo errato,
- messaggio: il messaggio di errore associato e l’elenco dei codici di errore;
- riga 14: se non ci sono errori, si restituisce la stringa jSON dei valori inviati;
alla riga 3, si utilizza il modello di azione [ActionModel02] seguente:
![]() |
package istia.st.springmvc.models;
import java.util.Date;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
public class ActionModel02 {
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
@NotNull(message = "La donnée est obligatoire")
@AssertTrue(message = "Seule la valeur [true] est acceptée")
private Boolean assertTrue;
@NotNull(message = "La donnée est obligatoire")
@Future(message = "Il faut une date postérieure à aujourd'hui")
private Date dateInFuture;
@NotNull(message = "La donnée est obligatoire")
@Past(message = "Il faut une date antérieure à aujourd'hui")
private Date dateInPast;
@NotNull(message = "La donnée est obligatoire")
@Max(value = 100, message = "Maximum 100")
private Integer intMax100;
@NotNull(message = "La donnée est obligatoire")
@Min(value = 10, message = "Minimum 10")
private Integer intMin10;
@NotNull(message = "La donnée est obligatoire")
@NotBlank(message = "La chaîne doit être non blanche")
private String strNotBlank;
@NotNull(message = "La donnée est obligatoire")
@Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
private String strBetween4and6;
@NotNull(message = "La donnée est obligatoire")
@Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
private String hhmmss;
@NotNull(message = "La donnée est obligatoire")
@Email(message = "Adresse invalide")
private String email;
@NotNull(message = "La donnée est obligatoire")
@Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
private String str4;
@Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
@NotNull(message = "La donnée est obligatoire")
private Integer int1014;
@URL(message = "URL invalide")
private String url;
// getter e setter
...
}
La classe utilizza i vincoli di validazione provenienti da due pacchetti:
- [javax.validation.constraints] alle righe 5-13;
- [org.hibernate.validator.constraints] alle righe 15-19;
Le dipendenze Maven di questi due pacchetti sono presenti nel progetto:
![]() |
In questo caso non utilizzeremo messaggi internazionalizzati, ma messaggi definiti all'interno del vincolo con l'attributo [message]. Per testare questa azione, utilizzeremo [Advanced Rest Client]:
![]() |
- in [1-2], la richiesta POST;
- in [3], l'intestazione HTTP [Content-Type] da utilizzare;
- in [4], il link [Add new value] consente di aggiungere una coppia [paramètre, value];
- in [5], inserire un campo di [ActionModel02], in questo caso il campo [assertFalse]:
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
- in [6], inserire un valore errato per visualizzare un messaggio di errore. Come indicato sopra, il vincolo [@AssertFalse] richiede che il campo [assertFalse] abbia il valore [false];
![]() |
- in [7], la risposta del server: è stato attivato il vincolo [@NotNull] relativo ai campi vuoti ed è stato visualizzato il messaggio di errore associato;
- in [8], il messaggio relativo al campo [assertFalse] per il quale il vincolo [@AssertFalse] non è stato verificato, insieme ai codici di tale errore. Si ricorda che questi codici possono essere associati a messaggi internazionalizzati;
Ecco un altro esempio:
![]() |

Si invita il lettore a testare i diversi casi di errore fino al codice POST, in cui tutti i dati sono validi:
![]() | ![]() |
Nota: il formato delle date è quello anglosassone: mm/gg/aaaa.
4.23. [/m28]: esternalizzazione dei messaggi di errore
Nella classe [ActionModel02], abbiamo inserito i messaggi in modo "fisso". È preferibile esternalizzarli in file di messaggi. Seguiamo l'esempio dell'azione [/m25]. Creiamo il seguente nuovo modello di azione [ActionModel03]:
![]() |
package istia.st.springmvc.models;
import java.util.Date;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
public class ActionModel03 {
@NotNull
@AssertFalse
private Boolean assertFalse;
@NotNull
@AssertTrue
private Boolean assertTrue;
@NotNull
@Future
private Date dateInFuture;
@NotNull
@Past
private Date dateInPast;
@NotNull
@Max(value = 100)
private Integer intMax100;
@NotNull
@Min(value = 10)
private Integer intMin10;
@NotNull
@NotBlank
private String strNotBlank;
@NotNull
@Size(min = 4, max = 6)
private String strBetween4and6;
@NotNull
@Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
private String hhmmss;
@NotNull
@Email
private String email;
@NotNull
@Length(max = 4, min = 4)
private String str4;
@Range(min = 10, max = 14)
@NotNull
private Integer int1014;
@URL
private String url;
// getter e setter
...
}
I messaggi di errore vengono esternalizzati nei file [messages.properties]:
![]() |
Il file [messages_fr.properties] è il seguente:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide
I messaggi di errore sono stati aggiunti alle righe 4-16. Si presentano nella forma:
I codici non possono essere arbitrari. Sono quelli visualizzati nell’azione precedente [/m27]. Ad esempio:
![]()
Nei file dei messaggi, per il campo [int1014] è necessario utilizzare uno dei quattro codici sopra indicati.
Il file [messages_en.properties] è il seguente:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL
Il modello di azione [ActionModel03] viene utilizzato dalla seguente azione:
// ----------------------- esternalizzazione dei messaggi di errore ------------------------
@RequestMapping(value = "/m28", method = RequestMethod.POST)
public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
// il contesto dell'applicazione Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// locale
Locale locale = RequestContextUtils.getLocale(request);
// degli errori?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// ricerca del messaggio di errore in base ai codici di errore
// il messaggio viene cercato nei file dei messaggi
// i codici di errore sotto forma di tabella
String[] codes = error.getCodes();
// sotto forma di stringa
String listCodes = String.join(" - ", codes);
// ricerca
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// è stato trovato?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// è stato trovato - si aggiunge l'errore al dizionario
map.put(error.getField(), msg);
}
} else {
// nessun errore
map.put("data", data);
}
return map;
}
Abbiamo già commentato questo tipo di codice. L’unica cosa davvero importante è la riga 23: il messaggio di errore restituito dipende dalle impostazioni locali della richiesta.
Ecco un esempio in francese:
![]() | ![]() |
e ora in inglese:
![]() | ![]() |









































































