4. Azioni: il modello
Torniamo all'architettura di un'applicazione Spring MVC:
![]() |
Nel capitolo precedente abbiamo esaminato il processo che indirizza la richiesta [1] al controller e all'azione [2a] che la gestirà, un meccanismo noto come routing. Abbiamo inoltre illustrato le varie risposte che un'azione può restituire al browser. Finora abbiamo presentato azioni che non elaboravano la richiesta loro sottoposta. Una richiesta [1] contiene varie informazioni che Spring MVC presenta [2a] all'azione sotto forma di un modello. Questo termine non va confuso con il modello M di una vista V [2c] generata dall'azione:
![]() |
- la richiesta HTTP del client arriva a [1];
- in [2], le informazioni contenute nella richiesta vengono trasformate in un modello di azione [3], spesso ma non necessariamente una classe, che funge da input per l'azione [4];
- in [4], l'azione, basata su questo modello, genera una risposta. Questa risposta ha due componenti: una vista V [6] e il modello M di questa vista [5];
- la vista V [6] utilizzerà il proprio modello M [5] per generare la risposta HTTP destinata al client.
Nel modello MVC, l'azione [4] fa parte del C (controller), il modello della vista [5] è l'M e la vista [6] è la V.
Questo capitolo esamina i meccanismi per collegare le informazioni trasportate dalla richiesta — che sono intrinsecamente stringhe — al modello di azione, che può essere una classe con proprietà di vario tipo.
Nota: il termine [Modello di azione] non è un termine riconosciuto.
Creiamo un nuovo controller per queste nuove azioni:
![]() |
Per ora, il [ActionModelController] sarà il seguente:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- Riga 5: Si noti che l'annotazione [@RestController] fa sì che la risposta inviata al client sia la serializzazione in stringa dei risultati dell'azione del controller;
4.1. [/m01]: Parametri GET
Aggiungiamo la seguente azione [/m01]:
// ----------------------- retrieve parameters with 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 [name] e [age]. Essi verranno inizializzati con i parametri che riportano gli stessi nomi nella richiesta HTTP GET;
I risultati in Chrome sono i seguenti [1-3]:
![]() |
- in [1], la richiesta GET con i parametri [name] e [age];
- in [3], vediamo che l'azione [/m01] ha recuperato correttamente questi parametri;
4.2. [/m02]: parametri POST
Aggiungiamo la seguente azione [/m02]:
// ----------------------- retrieve parameters with 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 [name] e [age]. Essi verranno 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 richiesta POST con i parametri [name] e [age];
- Nei paragrafi [4-5] abbiamo impostato l'intestazione HTTP [Content-Type] per la richiesta POST. Deve essere [Content-Type: application/x-www-form-urlencoded];
- in [6], [Form Data] fornisce l'elenco dei parametri per un'operazione POST. Qui vediamo i parametri [name] e [age];
- in [7], la risposta del server mostra che l'azione [/m02] ha recuperato con successo i parametri [name] e [age];
4.3. [/m03]: Parametri con gli stessi nomi
Abbiamo visto nella sezione 2.5.2.8 che l'elenco a selezione multipla poteva inviare al server parametri con gli stessi nomi. Vediamo come un'azione può recuperarli. Aggiungiamo la seguente azione [/m03]:
// ----------------------- retrieve parameters with the same names-----------------
@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 [name[]]. Verrà inizializzato qui con tutti i parametri che portano questo nome, sia in una richiesta GET che POST, poiché il tipo di richiesta non è stato specificato qui;
I risultati sono i seguenti:
![]() |
- Con un POST [1], inviamo i parametri [2];
- i parametri sono inclusi anche nell'URL [3];
- in [4], i quattro parametri con lo stesso nome [name]: [Query String parameters] sono i parametri dell'URL, [Form Data] sono i parametri inviati;
- in [5], vediamo che l'azione [/m03] ha recuperato i quattro parametri denominati [name];
4.4. [/m04]: mappatura dei parametri dell'azione su un oggetto Java
Consideriamo la seguente nuova azione [/m04]:
// ------ map parameters to a Command Object ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- Riga 3: L'azione accetta come parametro un oggetto Person del seguente tipo:
public class Personne {
// identifier
private Integer id;
// name
private String nom;
// age
private int age;
....
// getters and setters
...
}
- Per creare il parametro [Person], Spring MVC chiama [new Person()];
- quindi, se sono presenti parametri con nomi corrispondenti ai campi [id, name, age] dell'oggetto creato, li istanzia utilizzando i relativi setter;
- riga 4: l'azione restituisce un tipo [Person], che verrà quindi serializzato in una stringa prima di essere inviato al client. Abbiamo visto che, per impostazione predefinita, la serializzazione eseguita è di tipo JSON. Il client dovrebbe quindi ricevere la stringa JSON di una persona;
Ecco un esempio:
![]() |
- in [1], i parametri [id, nome, età] per costruire un oggetto [Persona];
- in [2], la stringa JSON per questa persona;
Cosa succede se non inviamo tutti i campi relativi a una persona? Proviamo:
![]() |
- in [2], è stato inizializzato solo il parametro [id];
4.5. [/m05]: recupera elementi da un URL
Si consideri la seguente nuova azione [/m05]:
// ----------------------- retrieve elements from 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 in elaborazione ha il formato [/m05/{a}/x/{b}], dove {param} è un parametro URL;
- Riga 3: gli elementi dei parametri URL vengono recuperati utilizzando l'annotazione [@PathVariable];
- righe 4–6: gli elementi recuperati [a] e [b] vengono inseriti in un dizionario;
- riga 7: la risposta sarà la stringa JSON di questo dizionario;
I risultati sono i seguenti:
![]() |
4.6. [/m06]: Recupero degli elementi e dei parametri dell'URL
Si consideri la seguente nuova azione [/m06]:
// -------- retrieve elements from URL and parameters---------------
@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: recuperiamo sia gli elementi dell'URL [Integer a, Double b] sia un parametro (GET o POST) [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:
![]() |
Notate la barra / alla fine del percorso [http://localhost:8080/m06/100/x/200.43/]. Senza di essa, otteniamo il seguente risultato errato:
![]() |
4.7. [/m07]: accedi all'intera richiesta
Ecco la nuova azione [/m07]:
// ------ access the HttpServletRequest query ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// HTTP headers
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: chiediamo a Spring MVC di iniettare l'oggetto [HttpServletRequest request], che incapsula tutte le informazioni disponibili sulla richiesta;
- righe 5–10: recuperiamo tutte le intestazioni HTTP dalla richiesta per assemblarle in una stringa che inviamo al client (riga 11);
I risultati sono i seguenti:
![]() |
- in [1], le intestazioni HTTP della richiesta;
![]() |
- in [2], la risposta. Tutte le intestazioni HTTP della richiesta sono effettivamente presenti lì.
4.8. [/m08]: accesso all'oggetto [Writer]
Consideriamo la seguente azione:
// ----------------------- injection de 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 di risposta al client;
- riga 3: l'azione restituisce un tipo [void], indicando che deve costruire la risposta al client autonomamente;
- riga 4: Aggiunta di testo al flusso di risposta al client;
I risultati sono i seguenti:
![]() |
- in [2], si osserva che l'intestazione HTTP [Content-Type] non è stata inviata;
- in [3], la risposta;
4.9. [/m09]: accesso a un'intestazione HTTP
Consideriamo la seguente azione:
// ----------------------- injection of RequestHeader ------------------------
@RequestMapping(value = "/m09", method = RequestMethod.GET)
public String m09(@RequestHeader("User-Agent") String userAgent) {
return userAgent;
}
- riga 3: l'annotazione [@RequestHeader("User-Agent")] recupera l'intestazione HTTP [User-Agent];
- riga 4: viene restituito 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 è generalmente un'intestazione HTTP che il:
- il server invia al client per la prima volta;
- il client poi rimanda sistematicamente al server;
Per prima cosa, creiamo un'azione che crei il cookie:
// ----------------------- Cookie creation ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- Riga 3: Inseriamo l'oggetto [HttpServletResponse response] per avere il pieno controllo sulla risposta;
- riga 4: creiamo 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 nulla. Inoltre, non scrive nulla nel corpo della risposta. Il client riceverà quindi un documento vuoto. La risposta viene utilizzata solo per aggiungere l'intestazione HTTP per un cookie;
Diamo un'occhiata ai 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 con ogni richiesta:
// ----------------------- Cookie injection ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- Riga 3: l'annotazione [@CookieValue("cookie1")] recupera il cookie con la chiave [cookie1];
- riga 4: questo valore sarà la risposta inviata al client;
Diamo un'occhiata ai risultati:
![]() |
- in [2], vediamo che il browser restituisce il cookie;
- in [3], l'azione lo ha recuperato con successo;
4.11. [/m12]: l'accesso al corpo di un POST
I parametri POST sono solitamente accompagnati dall'intestazione HTTP [Content-Type: application/x-www-form-urlencoded]. È possibile accedere all'intera stringa inviata. Creiamo la seguente azione:
// ----------- retrieve the body of a POST of type 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 della richiesta POST. Qui, supponiamo che sia di tipo [String];
- riga 4: restituiamo questo corpo al client;
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 con POST non hanno sempre la forma semplice [p1=v1&p2=v2] che abbiamo spesso utilizzato finora. Consideriamo un caso più complesso:
![]() |
- in [2-3]: inseriamo i valori inviati nel formato [chiave:valore];
- in [5], la stringa che è stata inviata;
Con il tipo [Content-Type: application/x-www-form-urlencoded], la stringa inviata deve essere nella forma [p1=v1&p2=v2]. Se vogliamo inviare qualcosa, useremo il tipo [Content-Type: text/plain]. Ecco un esempio:
![]() |
- in [2-3], creiamo l'intestazione HTTP [Content-Type]. Per impostazione predefinita [5], questa è quella che verrà utilizzata al posto di quella definita in [6]. L'attributo [charset=utf-8] è importante. Senza di esso, perdiamo i caratteri accentati nella stringa inviata;
- in [4], la stringa inviata che recuperiamo correttamente in [7];
4.12. [/m13, /m14]: recupero dei valori inviati in JSON
È possibile inviare parametri con l'intestazione HTTP [Content-Type: application/json]. Creiamo la seguente azione:
// ----------------------- retrieve the jSON body from a 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 si aspetta un corpo JSON;
- riga 3: [@RequestBody] rappresenta questo corpo. Questa annotazione è stata associata a un oggetto di tipo [Person]. Il corpo JSON verrà automaticamente deserializzato in questo oggetto;
- riga 4: utilizziamo il metodo [Person].toString() per restituire qualcosa di 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;
È possibile ottenere lo stesso risultato in modo diverso:
// ----------------------- retrieve the jSON body from a 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: abbiamo specificato che il metodo si aspetta un flusso di tipo [text/plain]. Spring MVC tratterà quindi il corpo della richiesta come un tipo [String] (riga 3);
- riga 4: la stringa JSON viene deserializzata in un oggetto [Person] (vedere la sezione 9.7, pagina 542);
I risultati sono i seguenti:
![]() |
- in [3], assicurarsi di utilizzare [text/plain];
4.13. [/m15]: recuperare la sessione
Rivediamo l'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 una richiesta e l'altra, anche se viene chiamata ripetutamente. Potresti voler memorizzare due tipi di dati:
- dati condivisi da tutti gli utenti dell'applicazione web. Si tratta generalmente di dati di sola lettura;
- dati condivisi tra le richieste provenienti dallo stesso client. Questi dati vengono memorizzati in un oggetto chiamato Sessione. Ci riferiamo a questo come alla sessione del client per indicare la memoria del client. Tutte le richieste provenienti da un client hanno accesso a questa sessione. Possono memorizzare e leggere informazioni da essa.
![]() |
Di seguito mostriamo i tipi di memoria a cui un'azione ha accesso:
- la memoria dell'applicazione, che contiene principalmente dati di 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 mostrata sopra, esiste una memoria di richiesta, o contesto di richiesta. La richiesta di un utente può essere elaborata da diverse azioni successive. Il contesto di richiesta consente all'Azione 1 di passare informazioni all'Azione 2.
Vediamo un primo esempio che illustra questi diversi tipi di memoria:
// ----------------------- retrieve session ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// retrieve the [counter] key object from the session
Object objCompteur = session.getAttribute("compteur");
// convert it to an integer to increment it
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// put it back in the session
session.setAttribute("compteur", iCompteur);
// we return it as the result of action
return String.valueOf(iCompteur);
}
Spring MVC gestisce la sessione dell'utente in un oggetto di tipo [HttpSession].
- riga 3: chiediamo a Spring MVC di iniettare l'oggetto [HttpSession] nei parametri dell'azione;
- riga 5: recuperiamo da esso un attributo denominato [counter]. Una sessione si comporta come un dizionario, un insieme di coppie [chiave, valore]. Se la chiave [counter] non esiste nella sessione, otteniamo un puntatore nullo;
- Riga 7: il valore associato alla chiave [counter] sarà di tipo [Integer];
- riga 8: incrementiamo il contatore;
- riga 10: aggiorniamo il contatore nella sessione;
- riga 12: il valore del contatore viene inviato al client;
Quando [/m15] viene eseguito per la:
- prima volta, alla riga 12, il contatore avrà il valore 1;
- la seconda volta, la riga 5 recupererà questo valore 1 e lo imposterà a 2;
- ...
Ecco un esempio di esecuzione:
![]() |
- in [1], otteniamo effettivamente il primo valore del contatore;
- in [2], il server ha inviato un cookie di sessione. Ha la chiave [JSESSIONID] e un valore che è una stringa di caratteri univoca per ogni utente. Ricordiamo che il browser rinvia sempre i cookie che riceve. Quindi, quando richiediamo l’azione [/m15] una seconda volta, il client rinvierà questo cookie, il che permetterà al server di riconoscerlo e collegarlo alla sua sessione. È così che viene mantenuta la sessione dell’utente;
Diamo un'occhiata alla seconda richiesta:
![]() |
- in [3], vediamo che il client invia il cookie di sessione. Si noti che nella risposta del server questo cookie di sessione non è più presente. Ora è il client che lo invia per essere riconosciuto;
- In [4], il secondo valore del contatore. È stato effettivamente incrementato;
4.14. [/m16]: recupero di un oggetto nell'ambito [session]
Potremmo voler inserire tutti i dati della sessione di un utente in un unico oggetto e inserire solo quell'oggetto nella sessione. Adotteremo questo approccio. Inseriremo 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: anche l'annotazione [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] è 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 questo che Spring MVC crea un'istanza per ogni utente anziché 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 contenente la classe [SessionModel].
Ora aggiungiamo la seguente azione:
@Autowired
private SessionModel session;
// ------ manage a scope object session [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 [@Autowired] nel controller. Ricordiamo che un controller Spring è un singleton. È quindi paradossale iniettarvi un componente con un ambito più ristretto, in questo caso l'ambito [Session]. È qui che entra in gioco l'annotazione [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] sul componente [SessionModel]. Ogni volta che il codice del controller accede al campo [session] nella riga 2, viene eseguito un metodo proxy per restituire la sessione della richiesta attualmente in elaborazione da parte del controller;
- riga 6: l'oggetto [HttpSession] non è più necessario nei parametri dell'azione;
- riga 7: il contatore viene recuperato e incrementato;
- riga 8: viene restituito il suo valore;
Ecco un esempio di esecuzione:
La prima volta
![]() |
La seconda volta
![]() |
Ora, prendiamo un altro browser per rappresentare un secondo utente. In questo caso, useremo il browser Opera:
![]() |
In [1] sopra, questo secondo utente recupera 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 (anche in Opera con Ctrl-Shift-I), 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]: Recupero di un oggetto nell'ambito [application]
Rivediamo l'architettura di esecuzione di un'azione:
![]() |
Sappiamo come creare la sessione utente. Ora creeremo un oggetto a livello di [applicazione] il cui contenuto sarà di sola lettura e accessibile a tutti gli utenti. Introduciamo la classe [ApplicationModel], che fungerà da oggetto a livello di [applicazione]:
![]() |
package istia.st.springmvc.models;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class ApplicationModel {
// meter
private AtomicLong compteur = new AtomicLong(0);
// getters and setters
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- Riga 5: l'annotazione [@Component] garantisce che la classe [ApplicationModel] sia un componente gestito da Spring. La natura predefinita dei componenti Spring è di tipo [singleton]: il componente viene creato come singola istanza 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 accessibili a tutti gli utenti;
- riga 11: un contatore di tipo [AtomicLong]. Questo tipo dispone di un metodo atomico denominato [incrementAndGet]. Ciò significa che a un thread che esegue questo metodo viene garantito che un altro thread non leggerà il valore del contatore (Get) tra la propria lettura (Get) e il suo incremento (increment) da parte del primo thread, il che causerebbe errori poiché due thread leggerebbero lo stesso valore del contatore e il contatore, invece di essere incrementato di due, verrebbe incrementato di uno;
Creiamo la seguente nuova azione [/m17]:
@Autowired
private ApplicationModel application;
// ----- manage an application scope object [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: iniettiamo il componente [ApplicationModel] nel controller. Si tratta di un singleton. Pertanto, ogni utente avrà un riferimento allo stesso oggetto;
- Riga 7: restituiamo il contatore dell'ambito [application] dopo averlo incrementato;
Ecco due esempi, uno con Chrome e l'altro con Opera:
![]() | ![]() |
Sopra, vediamo che entrambi i browser hanno utilizzato lo stesso contatore, cosa che non è avvenuta con la sessione. Questi due browser rappresentano due utenti diversi che hanno entrambi accesso ai dati dell'ambito [application]. In generale, dovremmo evitare di inserire informazioni di lettura/scrittura negli oggetti dell'ambito [application], come è stato fatto sopra con il contatore. Infatti, i thread di esecuzione di utenti diversi accedono contemporaneamente ai dati dell'ambito [application]. Se sono presenti informazioni scrivibili, l'accesso in scrittura deve essere sincronizzato, come è stato fatto sopra con il tipo [AtomicLong]. L'accesso simultaneo è fonte di errori di programmazione. Pertanto, è preferibile inserire solo informazioni di sola lettura negli oggetti dell'ambito [application].
4.16. [/m18]: Recupero di un oggetto nell'ambito [session] con [@SessionAttributes]
Esiste un altro modo per recuperare le informazioni nell'ambito [session]. Inseriremo il seguente oggetto nella sessione:
package istia.st.springmvc.models;
public class Container {
// the meter
public int compteur=10;
// getters and setters
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
Utilizzeremo questo oggetto con le due azioni seguenti:
// use of [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// here we put the key [container] in the session
session.setAttribute("container", new Container());
}
// use of [@ModelAttribute] ----------------------
// the session's [container] key will be injected here
@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. Viene utilizzata esclusivamente per creare un oggetto nella sessione con la chiave [container];
- riga 11: nell'azione [/m19] viene utilizzata l'annotazione [@ModelAttribute]. Il comportamento di questa annotazione è piuttosto complesso. Il parametro [container] di questa annotazione può riferirsi a varie cose, e in particolare a un oggetto di sessione. Affinché ciò funzioni, l'oggetto deve essere stato dichiarato con un'annotazione [@SessionAttributes] sulla classe stessa:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- La riga 2 sopra indica la chiave [container] come parte degli attributi di sessione;
Riassumendo:
- in [/m18], la chiave [container] viene inserita nella sessione;
- l'annotazione [@SessionAttributes({"container"})] garantisce che questa chiave possa essere iniettata in un parametro annotato con [@ModelAttribute("container")];
- non visibile nel seguente esempio di esecuzione, ma le informazioni annotate con [@ModelAttribute] fanno automaticamente parte del modello M passato alla vista V;
Ecco un esempio di esecuzione. Per prima cosa, inseriamo la chiave [container] nella sessione con l'azione [/m18] [1]. Successivamente, chiamiamo l'azione [/m19] due volte per vedere l'incremento del contatore.
![]() |
4.17. [/m20-/m23]: inserimento di dati con [@ModelAttribute]
Consideriamo la seguente nuova azione:
// the p attribute will be included in all [Model] view models ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------instanciation of @ModelAttribute --------------------------
// will be injected if it is in the
// will be injected if the controller has defined a method for this attribute
// can come from the URL fields if a String --> type converter exists for the attribute
// otherwise is built with the default constructor
// then the model attributes are initialized with the parameters of GET or POST
// the final result will be part of the model produced by the action
// the p attribute is injected into the arguments------------------------
@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, rappresentato da un tipo [Model] in Spring MVC. Un modello si comporta come un dizionario di coppie [chiave, valore]. Qui, la chiave [p] è associata all'oggetto [Person] costruito dal metodo [getPerson]. Il nome del metodo può essere qualsiasi cosa;
- riga 17: l'attributo del modello con chiave [p] viene iniettato nei parametri dell'azione. Questa iniezione segue le regole delle righe 8–12. Qui ci troviamo nel caso definito alla riga 9. Pertanto, alla riga 17, il parametro [Person person] sarà l'oggetto [Person(7, 'abcd', 14)];
- Riga 18: L'oggetto [person] viene restituito per la convalida. Verrà serializzato in JSON prima di essere inviato al client.
Ecco un esempio:
![]() |
Ora, esaminiamo la seguente azione:
// --------- attribute p is automatically included in the M model of view V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Un'azione che desidera visualizzare una vista V deve costruire il proprio modello M. Spring MVC gestisce questa operazione utilizzando un tipo [Model] che può essere iniettato nei parametri dell'azione. Inizialmente, questo modello è vuoto o contiene informazioni contrassegnate dall'annotazione [@ModelAttribute]. L'azione può arricchire o meno questo modello prima di passarlo a una vista.
- Riga 3: iniezione del modello M;
- riga 4: vogliamo vedere cosa c'è dentro. Lo serializziamo in una stringa per inviarlo al client. Qui verrà utilizzato il metodo [Person.toString]. Deve quindi esistere;
Ecco un'esecuzione:
![]() |
Sopra, vediamo che le istruzioni:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
abbiamo creato una voce [p, Personne(7, "abcd", 14)] nel modello. È sempre così.
Consideriamo ora il seguente caso:
// sinon est construit avec le constructeur par défaut
// ensuite les attributs du modèle sont initialisés avec les paramètres du GET ou du POST
con la seguente azione:
// --------- model attribute [param1] is part of the model but is not initialized
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- riga 3: l'attributo chiave del modello [param1] non esiste. In questo caso, il tipo associato deve avere un costruttore predefinito. Questo è il caso del tipo [String], ma non possiamo scrivere [@ModelAttribute("param1") Integer p1] perché la classe [Integer] non ha un costruttore predefinito;
- Riga 4: restituiamo 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 informazione su tale valore.
Consideriamo ora la seguente azione, in cui inseriamo esplicitamente delle informazioni nel modello:
// --------- the model attribute [param2] is explicitly set in the model
@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 aggiunto al modello sotto la chiave [param2]:
Ecco un esempio di esecuzione:
![]() |
Le regole cambiano se il parametro dell'azione è un oggetto. Ecco un primo esempio:
// ------ the template attribute [unePersonne] is automatically set in the template
@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 viene fornito. Il risultato è il seguente:
![]() |
Possiamo vedere che l'annotazione [@ModelAttribute("unePersonne") Personne p1] ha aggiunto la persona [p1] al modello, associata alla chiave [unePersonne].
Consideriamo ora la seguente azione:
// --------- person p1 is automatically included in the model
// -------- with class name as key, 1st character lowercase
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- riga 4: non abbiamo incluso l'annotazione [@ModelAttribute];
Il risultato è il seguente:
![]() |
Possiamo notare che la presenza del parametro [Person p1] ha inserito la persona [p1] nel modello, associata alla chiave [person], che è il nome della classe [Person] con la prima lettera minuscola.
4.18. [/m24]: convalida del modello di azione
Si consideri il seguente modello di azione [ActionModel01]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// data
@NotNull
private Integer a;
@NotNull
private Double b;
// getters and setters
...
}
- Righe 8 e 9: l'annotazione [@NotNull] è un vincolo di convalida che specifica che i dati annotati non possono essere nulli;
Esaminiamo ora la seguente azione:
// ----------------------- model validation ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// mistakes?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// browsing the error list
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 {
// no errors
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 i parametri omonimi. L'annotazione [@Valid] indica che devono essere verificati i vincoli di validazione. I risultati di questa validazione saranno inseriti nel parametro [BindingResult] (secondo parametro). Verranno eseguite le seguenti validazioni:
- a causa delle annotazioni [@NotNull], i parametri [a] e [b] devono essere presenti;
- a causa del tipo [Integer a], il parametro [a], che è intrinsecamente di tipo [String], deve essere convertibile in tipo [Integer];
- a causa del tipo [Double b], il parametro [b], che è intrinsecamente di tipo [String], deve essere convertibile in un tipo [Double];
Con l'annotazione [@Valid], gli errori di validazione saranno segnalati nel parametro [BindingResult result]. Senza l'annotazione [@Valid], gli errori di validazione causano il crash 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]. La stringa JSON di questo risultato verrà inviata al client. Costruiamo due tipi di dizionari:
- in caso di errore, un dizionario con una voce ['errors', value] dove [value] è una stringa che descrive tutti gli errori (riga 13);
- in caso di successo, un dizionario con una voce ['data', valore] dove [valore] è a sua volta un dizionario con due voci: ['a', valore], ['b', valore] (riga 19);
- righe 9–12: per ogni errore rilevato [error], viene costruita 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 esamineremo i ruoli tra poco;
- il quarto elemento è il codice di errore. Fa parte dell'elenco precedente;
- l'ultimo elemento è il messaggio di errore predefinito. In realtà, possono esserci più messaggi di errore;
Ecco alcuni esempi di esecuzione:
![]() |
Sopra, vediamo 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;
Prendi nota dei 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à il momento di personalizzare il messaggio di errore. Tieni presente che il codice di errore è [typeMismatch].
Un altro esempio:
![]() |
In questo caso, i parametri [a] e [b] non sono stati passati. I validatori [@NotNull] nel modello di azione [ActionModel01] hanno quindi svolto il loro compito;
Infine, i valori corretti:
![]() |
4.19. [m/24]: personalizzazione dei messaggi di errore
Torniamo a uno screenshot dell'esempio precedente:
![]() |
Sopra vediamo i messaggi di errore predefiniti. Ovviamente, non possiamo mantenerli tali in un'applicazione reale. È possibile personalizzare questi messaggi di errore. Per farlo, useremo i codici di errore. Sopra, vediamo che l'errore per il campo [a] ha 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;
Notiamo inoltre che il codice di errore per il campo [a] ottenuto tramite [error.getCode()] è [typeMismatch] (vedi 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 il seguente formato:
In questo caso, la chiave sarà un codice di errore e il messaggio sarà il messaggio di errore associato a quel codice.
Esaminiamo 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 manca il parametro [a];
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] quando manca il parametro [b];
Il file [messages.properties] deve contenere un messaggio di errore per tutti i possibili casi di errore. Nel caso in cui
- mancano i parametri [a] e [b], verrà utilizzato il codice [NotNull];
- se il parametro [a] è errato, abbiamo incluso messaggi per due codici [typeMismatch.actionModel01.a, typeMismatch]. Vedremo quale verrà utilizzato;
- se il parametro [b] è errato, verrà utilizzato il codice [typeMismatch];
Per utilizzare 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: le annotazioni di configurazione che prima si trovavano nella classe [Application] ora sono qui;
- 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: definiamo un bean denominato [messageSource] (il nome del metodo). Questo bean viene utilizzato per definire i file di messaggi dell'applicazione e deve avere questo nome;
- Righe 17–19: Indicare a Spring che il file dei messaggi:
- si trova nella cartella [i18n] all'interno del classpath del progetto (riga 18),
- si chiama [messages.properties] (riga 18). In realtà, il termine [messages] è la radice dei nomi dei file di messaggi piuttosto che il nome stesso. Vedremo che, nel contesto dell'internazionalizzazione, possono esserci più file di messaggi, uno per ogni locale supportato. Pertanto, potremmo avere [messages_fr.properties] per il francese e [messages_en.properties] per l'inglese. I suffissi aggiunti alla radice [messages] sono standardizzati. Non è possibile utilizzare qualsiasi cosa;
Nel progetto STS, la cartella [i18n] deve essere collocata nella cartella resources perché viene aggiunta al classpath del progetto:
![]() |
Per utilizzare questo file, creiamo la seguente nuova azione:
// model validation, error message handling ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// results dictionary
Map<String, Object> map = new HashMap<String, Object>();
// spring application context
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// search for error msg using error codes
// the msg is searched in the message files
// error codes in table format
String[] codes = error.getCodes();
// in chain form
String listCodes = String.join(" - ", codes);
// research
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// have we found?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// found - add error msg to error msg chain
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 è simile a quello dell'azione [/m24]. Ecco le differenze:
- riga 3: iniettiamo 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 nell'applicazione. Fornisce inoltre l'accesso ai file dei messaggi;
- riga 10: recuperiamo le impostazioni locali dell'applicazione. Questo termine è spiegato più dettagliatamente di seguito;
- righe 15–31: per ogni errore, cerchiamo 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 fermiamo;
- riga 26: come recuperare un messaggio da [messages.properties]:
- il primo parametro è il codice cercato in [messages.properties],
- il secondo è un array di parametri, poiché i messaggi sono talvolta parametrizzati. Non è questo il caso qui,
- il terzo è la lingua utilizzata (ottenuta alla riga 10). La lingua specifica la lingua utilizzata, [fr_FR] per il francese (Francia), [en_US] per l'inglese (USA). Il messaggio viene cercato in messages_[locale].properties, quindi ad esempio [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 ci interessa;
- righe 25–29: in modo un po' inaspettato, quando si cerca un codice inesistente in un file di messaggi, viene generata un'eccezione anziché un puntatore nullo;
- righe 33–35: gestiamo il caso in cui non ci sia alcun messaggio di errore;
- righe 37–38: costruiamo la stringa di errore. In essa includiamo la lingua e il messaggio di errore trovato;
Ecco alcuni esempi di esecuzione:
![]() |
Notiamo che:
- la lingua dell'applicazione è [fr_FR]. Si tratta di un valore predefinito poiché non abbiamo effettuato alcuna operazione per inizializzarla;
- il messaggio utilizzato per entrambi i campi è il seguente:
NotNull=Le champ ne peut être vide
Un altro esempio:
![]() |
Vediamo 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é ci sono due messaggi diversi? Per il parametro [a], c'erano due possibili messaggi:
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
I codici di errore sono stati esaminati nell'ordine specificato dall'array [error.getCodes()]. È emerso 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 gestire questo aspetto, estenderemo la classe di configurazione [Config] in modo che appaia così:
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: creiamo un intercettatore di richiesta. Un intercettatore di richiesta estende l'interfaccia [HandlerInterceptor]. Una classe di questo tipo ispeziona la richiesta in arrivo prima che venga elaborata da un'azione. Qui, [localeChangeInterceptor] cercherà un parametro denominato [lang] nella richiesta GET o POST in arrivo e modificherà le impostazioni locali dell'applicazione in base a tale parametro. Pertanto, se il parametro è [lang=en_US], le impostazioni locali dell'applicazione diventeranno inglese (USA);
- righe 34–37: sovrascriviamo il metodo [WebMvcConfigurerAdapter.addInterceptors] per aggiungere l'intercettatore precedente;
- righe 39–45: sono utilizzate per configurare il modo in cui la lingua verrà incapsulata in un cookie. Sappiamo che un cookie può fungere da memoria dell'utente, poiché il browser client lo rinvia sistematicamente al server. Il precedente intercettatore [localeChangeInterceptor] crea un cookie che incapsula la lingua. La riga 42 assegna a questo cookie il nome [lang]. Il cookie viene utilizzato anche per modificare la lingua;
- riga 43: specifica che se il cookie [lang] è assente, la lingua sarà [fr];
In sintesi, la lingua per una richiesta può essere impostata in due modi:
- passando un parametro denominato [lang];
- inviando un cookie denominato [lang]. Questo cookie viene creato automaticamente dopo l'esecuzione del metodo precedente;
Per utilizzare questa impostazione locale, creeremo 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 noti che il file [messages.properties] viene utilizzato quando non viene trovato alcun file corrispondente alle impostazioni locali 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 vedrà quindi i messaggi in inglese.
Proviamo. Innanzitutto, negli strumenti di sviluppo di Chrome (Ctrl-Shift-I), controlla i tuoi cookie:
![]() |
Se hai un cookie chiamato [lang], eliminalo. Poi, in Chrome, vai all'URL [http://localhost:8080/m25]:
![]() |
Il browser ha inviato le seguenti intestazioni HTTP:
Possiamo notare che in queste intestazioni non è presente il cookie [lang]. In questo caso, il nostro codice utilizza la lingua [fr]. Ciò è mostrato nella schermata. Proviamo un altro caso:
![]() |
- in [1], abbiamo passato il parametro [lang=en] per impostare la lingua su [en];
- in [2], vediamo la nuova impostazione locale;
- in [3], il messaggio è ora in inglese;
Ora diamo un'occhiata agli scambi HTTP:
![]() |
Come si può vedere sopra, il server ha restituito un cookie [lang]. Ciò comporta una conseguenza importante: la lingua per la richiesta successiva sarà nuovamente [en] a causa del cookie [lang] che verrà inviato dal browser. Dobbiamo quindi mantenere i messaggi in inglese. Verifichiamolo:
![]() |
Sopra, vediamo che la lingua è rimasta [en]. A causa del cookie che il browser invia sistematicamente, rimarrà così finché l'utente non la cambierà 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 le impostazioni locali dalla richiesta:
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
...
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
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 mostrato sopra, la validità delle impostazioni locali richieste non viene verificata. Tuttavia, la richiesta successiva del browser genera un'eccezione lato server poiché il cookie delle impostazioni locali che riceve non è corretto.
4.22. [/m27]: Convalida di un modello con Hibernate Validator
Si consideri la seguente nuova azione:
//model validation with 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>();
// mistakes?
if (result.hasErrors()) {
// browsing the error list
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// no errors
map.put("data", data);
}
return map;
}
Qui abbiamo un codice che abbiamo già visto diverse volte:
- riga 3: l'azione [/m27] viene richiesta tramite un POST;
- righe 8–11: ogni errore sarà identificato da [campo, messaggio] con:
- campo: il campo in cui si è verificato l'errore,
- messaggio: il messaggio di errore associato e l'elenco dei codici di errore;
- riga 14: se non ci sono errori, viene restituita la stringa JSON dei valori inviati;
Riga 3: viene utilizzato il seguente modello di azione [ActionModel02]:
![]() |
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;
// getters and setters
...
}
La classe utilizza i vincoli di convalida di due pacchetti:
- [javax.validation.constraints] alle righe 5–13;
- [org.hibernate.validator.constraints] alle righe 15–19;
Le dipendenze Maven per questi due pacchetti sono incluse nel progetto:
![]() |
In questo caso non useremo messaggi internazionalizzati, ma messaggi definiti all'interno del vincolo tramite l'attributo [message]. Per testare questa azione, useremo [Advanced Rest Client]:
![]() |
- in [1-2], la richiesta POST;
- in [3], l'intestazione HTTP [Content-Type] da utilizzare;
- in [4], il link [Aggiungi nuovo valore] consente di aggiungere una coppia [parametro, valore];
- in [5], inserisci un campo da [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], inserisci un valore errato per visualizzare un messaggio di errore. Sopra, il vincolo [@AssertFalse] richiede che il campo [assertFalse] abbia il valore [false];
![]() |
- in [7], la risposta del server: è stato attivato il vincolo [@NotNull] per i campi vuoti ed è stato restituito il messaggio di errore associato;
- in [8], il messaggio relativo al campo [assertFalse] per il quale il vincolo [@AssertFalse] non è stato soddisfatto, insieme ai codici di errore. Si noti che questi codici possono essere associati a messaggi internazionalizzati;
Ecco un altro esempio:
![]() |

Il lettore è invitato a provare i vari casi di errore fino a quando non vengono inseriti tutti i dati validi:
![]() | ![]() |
Nota: il formato della data è quello anglosassone: mm/gg/aaaa.
4.23. [/m28]: Esternalizzazione dei messaggi di errore
Nella classe [ActionModel02] abbiamo hard-coded i messaggi. È preferibile esternarli 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;
// getters and setters
...
}
I messaggi di errore sono 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
Sono stati aggiunti messaggi di errore alle righe 4–16. Hanno il seguente formato:
I codici non possono essere arbitrari. Sono quelli visualizzati nella precedente azione [/m27]. Ad esempio:
![]()
Nei file dei messaggi, è necessario utilizzare uno dei quattro codici sopra indicati per il campo [int1014].
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] è utilizzato dalla seguente azione:
// ----------------------- externalization of error messages ------------------------
@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>();
// spring application context
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// mistakes?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// search for error msg using error codes
// the msg is searched in the message files
// error codes in table format
String[] codes = error.getCodes();
// in chain form
String listCodes = String.join(" - ", codes);
// research
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// have we found?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// we have found - we add the error to the dictionary
map.put(error.getField(), msg);
}
} else {
// no errors
map.put("data", data);
}
return map;
}
Abbiamo già parlato di questo tipo di codice. L'unica cosa che conta davvero è la riga 23: il messaggio di errore restituito dipende dalle impostazioni locali della richiesta.
Ecco un esempio in francese:
![]() | ![]() |
e ora in inglese:
![]() | ![]() |









































































