4. Acties: het model
Laten we terugkeren naar de architectuur van een Spring-applicatie MVC:
![]() |
In het vorige hoofdstuk hebben we gekeken naar het proces dat het verzoek [1] naar de controller en de actie [2a] leidt, die het zullen verwerken; een mechanisme dat we routing noemen. We hebben bovendien de verschillende reacties besproken die een actie aan de browser kan geven. Tot nu toe hebben we acties besproken die geen gebruik maakten van het verzoek dat aan hen werd voorgelegd. Een verzoek [1] bevat diverse gegevens die Spring MVC in de vorm van een model aan de actie [2a] aanbiedt. Deze term mag niet worden verward met het model M van een weergave V [2c] dat door de actie wordt gegenereerd:
![]() |
- de aanvraag HTTP van de klant komt binnen als [1];
- in [2] wordt de informatie uit het verzoek omgezet in het actiemodel [3], vaak maar niet noodzakelijkerwijs een klasse, die als invoer dient voor de actie [4];
- in [4] zal de actie, op basis van dit model, een antwoord genereren. Dit antwoord bestaat uit twee onderdelen: een weergave V [6] en het model M van deze weergave [5];
- de weergave V [6] gebruikt haar model M [5] om het antwoord HTTP voor de klant te genereren.
In het model MVC maakt de actie [4] deel uit van de C (controller), is het model van de weergave [5] de M en is de weergave [6] de V.
In dit hoofdstuk worden de koppelingsmechanismen onderzocht tussen de informatie die door de aanvraag wordt overgedragen – die van nature uit tekenreeksen bestaat – en het actiemodel, dat een klasse kan zijn met eigenschappen van verschillende typen.
Opmerking: de term [Modèle d'action] is geen erkende term.
We maken een nieuwe controller aan voor deze nieuwe acties:
![]() |
De controller [ActionModelController] zal voorlopig als volgt zijn:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- regel 5: ter herinnering: de annotatie [@RestController] zorgt ervoor dat het antwoord dat naar de client wordt verzonden, de als tekenreeks geserialiseerde weergave is van het resultaat van de acties van de controller;
4.1. [/m01]: parameters van een GET
We voegen de volgende actie [/m01] toe:
// ----------------------- parameters ophalen met 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);
}
- regel 4: de actie accepteert twee parameters met de namen [nom] en [age]. Deze worden geïnitialiseerd met parameters met dezelfde namen in de aanroep HTTP GET;
De resultaten in Chrome voor [1-3] zijn als volgt:
![]() |
- in [1], de aanvraag GET met de parameters [nom] en [age];
- in [3] zien we dat de actie [/m01] deze parameters inderdaad heeft opgehaald;
4.2. [/m02]: parameters van een POST
We voegen de volgende actie [/m02] toe:
// ----------------------- parameters ophalen met 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);
}
- regel 4: de actie accepteert twee parameters met de namen [nom] en [age]. Deze worden geïnitialiseerd met parameters met dezelfde namen in de query HTTP POST;
De resultaten met [Advanced rest Client] zijn als volgt:
![]() |
- in [1-3], de query POST met de parameters [nom] en [age];
- in [4-5] wordt de header HTTP [Content-Type] van de aanvraag POST vastgelegd. Dit moet [Content-Type: application/x-www-form-urlencoded] zijn;
- in [6] geeft [Form Data] de lijst met parameters van een bewerking POST weer. Hier zien we de parameters [nom] en [age];
- in [7], het antwoord van de server waaruit blijkt dat de actie [/m02] de parameters [nom] en [age] inderdaad heeft opgehaald; ;
4.3. [/m03]: parameters met dezelfde namen
In paragraaf 2.5.2.8 hebben we gezien dat de lijst met meerdere selectiemogelijkheden parameters met dezelfde namen naar de server kon sturen. Laten we eens kijken hoe een actie deze kan ophalen. We voegen de volgende actie [/m03] toe:
// ----------------------- parameters met dezelfde namen ophalen-----------------
@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));
}
- regel 2: de actie accepteert een parameter met de naam [naam[]]. Deze wordt hier geïnitialiseerd met alle parameters die deze naam dragen, of deze nu in een GET of een POST voorkomen, aangezien het type van de aanvraag hier niet is gespecificeerd;
De resultaten zijn als volgt:
![]() |
- via een POST [1] worden de parameters [2] verzonden;
- er worden ook parameters toegevoegd aan de URL en [3];
- in [4] staan de vier parameters met dezelfde naam als [nom]: [Query String parameters] zijn de parameters van URL, [Form Data] zijn de verzonden parameters;
- in [5] zien we dat de actie [/m03] de vier parameters met de naam [nom] heeft opgehaald;
4.4. [/m04]: de parameters van de actie toewijzen aan een Java-object
Stel dat we de volgende nieuwe actie [/m04] hebben:
// ------ parameters toewijzen aan een object (Command Object) ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- regel 3: de actie heeft als parameter een persoon van het volgende type:
public class Personne {
// ID
private Integer id;
// naam
private String nom;
// leeftijd
private int age;
....
// getters en setters
...
}
- om de parameter [Personne personne] aan te maken, maakt Spring MVC een [new Personne()] aan;
- vervolgens, als er parameters zijn met de veldnamen [id, nom, age] van het aangemaakte object, maakt het een instantie aan met de velden via hun setters;
- regel 4: de actie retourneert een type [Personne] dat dus tot een tekenreeks wordt geserialiseerd voordat het naar de client wordt verzonden. We hebben gezien dat de serialisatie standaard een jSON-serialisatie was. De klant zou dus de tekenreeks jSON van een persoon moeten ontvangen;
Hier is een voorbeeld:
![]() |
- in [1], de parameters [id, nom, age] om een object [Personne] samen te stellen;
- in [2], de tekenreeks jSON van deze persoon;
Wat gebeurt er als niet alle velden van een persoon worden verzonden? Laten we het eens proberen:
![]() |
- naar [2], alleen de parameter [id] is geïnitialiseerd;
4.5. [/m05]: de elementen van een URL ophalen
Neem de volgende nieuwe actie [/m05]:
// ----------------------- elementen ophalen uit het 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;
}
- regel 2: de verwerkte URL heeft de vorm [/m05/{a}/x/{b}], waarbij {param} een parameterelement is van de URL;
- regel 3: de parameterelementen van het URL worden opgehaald met de annotatie [@PathVariable];
- regels 4-6: de opgehaalde elementen [a] en [b] worden in een woordenboek geplaatst;
- regel 7: het antwoord is de tekenreeks jSON uit dit woordenboek;
De resultaten zijn als volgt:
![]() |
4.6. [/m06]: elementen uit URL en parameters ophalen
Stel dat de nieuwe actie [/m06] als volgt is:
// -------- elementen uit URL en parameters ophalen---------------
@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;
}
- regel 3: er worden zowel elementen uit URL en [Integer a, Double b] als een parameter (GET of POST) uit [Double c] opgehaald;
- regels 4-7: deze elementen worden in een woordenboek geplaatst;
- regel 8: dit vormt het antwoord van de client, die dus de tekenreeks jSON uit dit woordenboek ontvangt;
Dit zijn de resultaten:
![]() |
Let op de / aan het einde van het pad [http://localhost:8080/m06/100/x/200.43/]. Zonder deze / krijg je het volgende onjuiste resultaat:
![]() |
4.7. [/m07]: toegang tot de volledige aanvraag
Neem de volgende nieuwe actie [/m07]:
// ------ toegang tot de query HttpServletRequest ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// de headers van 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();
}
- regel 3: we vragen Spring MVC om het object [HttpServletRequest request] te injecteren, dat alle informatie bevat die we over de verzoek kunnen verkrijgen;
- regels 5-10: we halen alle headers HTTP van het verzoek op om ze samen te voegen tot een tekenreeks die we naar de client sturen (regel 11);
De resultaten zijn als volgt:
![]() |
- in [1], de HTTP-headers van de aanvraag;
![]() |
- in [2], het antwoord. Hierin zijn inderdaad alle headers HTTP van het verzoek terug te vinden.
4.8. [/m08]: toegang tot het object [Writer]
Laten we de volgende actie eens bekijken:
// ----------------------- writer-injectie ------------------------
@RequestMapping(value = "/m08", method = RequestMethod.GET)
public void m08(Writer writer) throws IOException {
writer.write("Bonjour le monde !");
}
- regel 3: Spring MVC injecteert het object [Writer writer] waarmee in de responsstroom naar de klant kan worden geschreven;
- regel 3: de actie retourneert een type [void], wat aangeeft dat deze zelf het antwoord aan de klant moet samenstellen;
- regel 4: toevoegen van tekst aan de responsstroom naar de klant;
De resultaten zijn als volgt:
![]() |
- in [2] zien we dat de header HTTP [Content-Type] niet is verzonden;
- in [3], het antwoord;
4.9. [/m09]: toegang tot een header HTTP
Laten we de volgende actie eens bekijken:
// ----------------------- injectie van RequestHeader ------------------------
@RequestMapping(value = "/m09", method = RequestMethod.GET)
public String m09(@RequestHeader("User-Agent") String userAgent) {
return userAgent;
}
- regel 3: met de annotatie [@RequestHeader("User-Agent")] kan de header HTTP [User-Agent] worden opgehaald;
- regel 4: de tekst van deze header wordt weergegeven;
De resultaten zijn als volgt:
![]() |
- in [2], de header HTTP [User-Agent];
![]() |
- in [3]; de actie [/m08] heeft deze header correct opgehaald;
4.10. [/m10, /m11]: toegang krijgen tot een cookie
Een cookie is doorgaans een header HTTP die de:
- server de eerste keer naar de client stuurt;
- de client vervolgens systematisch terugstuurt naar de server;
Laten we eerst een actie maken die de cookie aanmaakt:
// ----------------------- aanmaken van een cookie ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- regel 3: we voegen het object [HttpServletResponse response] in om volledige controle over het antwoord te hebben;
- regel 4: we maken een cookie aan met de sleutel [cookie1] en de waarde [remember me] (Opmerking: tekens met accenten in de waarde van een cookie veroorzaken fouten);
- regel 3: de actie levert niets op. Bovendien schrijft ze niets in de hoofdtekst van het antwoord. De client ontvangt dus een leeg document. Het antwoord wordt alleen gebruikt om de header HTTP van een cookie toe te voegen;
Laten we de resultaten bekijken:
![]() |
- in [1]: het verzoek;
- in [2]: het antwoord is leeg;
- in [3]: de cookie die door de actie is aangemaakt;
Laten we nu een actie maken om deze cookie op te halen, die de browser voortaan bij elk verzoek zal meesturen:
// ----------------------- cookie-injectie ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- regel 3: met de annotatie [@CookieValue("cookie1")] kan de sleutelcookie [cookie1] worden opgehaald;
- regel 4: deze waarde wordt als antwoord naar de client gestuurd;
Laten we de resultaten bekijken:
![]() |
- in [2] zien we dat de browser de cookie terugstuurt;
- in [3] heeft de actie het cookie inderdaad opgehaald;
4.11. [/m12]: toegang tot de body van een POST
De verzonden parameters gaan meestal gepaard met de header HTTP [Content-Type: application/x-www-form-urlencoded]. We hebben toegang tot de volledige verzonden string. We maken de volgende actie aan:
// ----------- de body van een POST van het type String ophalen------------------------
@RequestMapping(value = "/m12", method = RequestMethod.POST)
public String m12(@RequestBody String requestBody) {
return requestBody;
}
- regel 3: met de annotatie [@RequestBody] kan de body van POST worden opgehaald. Hier gaan we ervan uit dat deze van het type [String] is;
- regel 4: deze inhoud wordt teruggestuurd naar de klant;
Hier volgt een eerste voorbeeld:
![]() |
- in [2], de verzonden waarden;
- in [3], de header HTTP [Content-Type] van het verzoek;
- in [4], het antwoord van de server;
De verzonden parameters hebben niet altijd de eenvoudige vorm [p1=v1&p2=v2] die we tot nu toe vaak hebben gebruikt. Laten we een complexer geval bekijken:
![]() |
- in [2-3]: we voeren de verzonden waarden in de vorm [clé:value] in;
- in [5], de reeks die is gepost;
Bij het type [Content-Type: application/x-www-form-urlencoded] moet de te verzenden tekenreeks de vorm [p1=v1&p2=v2] hebben. Als je willekeurige gegevens wilt verzenden, gebruik je het type [Content-Type: text/plain]. Hier is een voorbeeld:
![]() |
- in [2-3] wordt de header HTTP [Content-Type] aangemaakt. Standaard is [5]; deze wordt gebruikt in plaats van de in [6] gedefinieerde. Het attribuut [charset=utf-8] is belangrijk. Zonder dit attribuut gaan de accenten in de verzonden tekenreeks verloren;
- in [4] wordt de verzonden tekenreeks correct teruggehaald in [7];
4.12. [/m13, /m14]: verzonden waarden ophalen in jSON
Het is mogelijk om parameters te verzenden met de header HTTP [Content-Type: application/json]. We maken de volgende actie aan:
// ----------------------- de inhoud jSON van een POST ophalen
@RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
public String m13(@RequestBody Personne personne) {
return personne.toString();
}
- regel 2: [consumes = "application/json"] geeft aan dat de actie een hoofdtekst van het type jSON verwacht;
- regel 3: [@RequestBody] vertegenwoordigt deze body. Deze annotatie is gekoppeld aan een object van het type [Personne]. De body jSON wordt automatisch in dit object gedeserialiseerd;
- regel 4: de methode [Personne].toString() wordt gebruikt om iets terug te geven dat niet de verzonden tekenreeks jSON is;
Hier volgt een voorbeeld:
![]() |
- in [2], de verzonden tekenreeks jSON;
- in [3], de [Content-Type] uit het verzoek;
- in [4], het antwoord van de server;
Dit kan ook op een andere manier:
// ----------------------- de inhoud jSON van een POST ophalen 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();
}
- regel 2: we hebben aangegeven dat de methode een stream van het type [text/plain] verwacht. Spring MVC zal de body van het verzoek dan verwerken als een type [String] (regel 3);
- regel 4: de tekenreeks jSON wordt gedeserialiseerd tot een object van het type [Personne] (zie paragraaf 9.7, pagina 542);
De resultaten zijn als volgt:
![]() |
- in [3], dit moet echter [text/plain] zijn;
4.13. [/m15]: de sessie ophalen
Laten we nog eens terugkomen op de uitvoeringsarchitectuur van een actie:
![]() |
De controllerklasse wordt aan het begin van het verzoek van de client geïnstantieerd en aan het einde ervan vernietigd. Daarom kan deze niet worden gebruikt om gegevens tussen twee verzoeken op te slaan, zelfs niet als deze herhaaldelijk wordt aangeroepen. Er zijn twee soorten gegevens die men mogelijk wil opslaan:
- gegevens die door alle gebruikers van de webapplicatie worden gedeeld. Dit zijn doorgaans alleen-lezen gegevens;
- gegevens die worden gedeeld door de verzoeken van dezelfde klant. Deze gegevens worden opgeslagen in een object dat ‘Session’ wordt genoemd. We spreken dan van een klantsessie om het geheugen van de klant aan te duiden. Alle verzoeken van een klant hebben toegang tot deze sessie. Ze kunnen er informatie opslaan en uit lezen.
![]() |
Hierboven laten we de soorten geheugen zien waartoe een actie toegang heeft:
- het applicatiegeheugen, dat meestal alleen-lezen-gegevens bevat en toegankelijk is voor alle gebruikers;
- het geheugen van een specifieke gebruiker, of sessie, dat lees- en schrijfgegevens bevat en toegankelijk is voor opeenvolgende verzoeken van dezelfde gebruiker;
- niet weergegeven hierboven, bestaat er een verzoekgeheugen, of verzoekcontext. Het verzoek van een gebruiker kan door meerdere opeenvolgende acties worden verwerkt. De verzoekcontext stelt actie 1 in staat om informatie door te geven aan actie 2.
Laten we een eerste voorbeeld bekijken dat deze verschillende geheugens illustreert:
// ----------------------- de sessie ophalen ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// we halen het sleutelobject [compteur] op uit de sessie
Object objCompteur = session.getAttribute("compteur");
// het wordt omgezet naar een geheel getal om het te verhogen
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// het wordt teruggeplaatst in de sessie
session.setAttribute("compteur", iCompteur);
// het wordt weergegeven als resultaat van de actie
return String.valueOf(iCompteur);
}
Spring MVC bewaart de sessie van de gebruiker in een object van het type [HttpSession].
- regel 3: Spring MVC wordt gevraagd om het object [HttpSession] in de parameters van de actie te injecteren;
- regel 5: hieruit wordt een attribuut met de naam [compteur] opgehaald. Een sessie gedraagt zich als een woordenboek, een verzameling van paren [clé, valeur]. Als de sleutel [compteur] niet in de sessie voorkomt, wordt een pointer null opgehaald;
- regel 7: de waarde die bij de sleutel [compteur] hoort, is van het type [Integer];
- regel 8: de teller wordt verhoogd;
- regel 10: de teller in de sessie wordt bijgewerkt;
- regel 12: de waarde van de teller wordt naar de client verzonden;
Wanneer [/m15] voor de:
- de eerste keer, regel 12, de teller de waarde 1 heeft;
- de tweede keer, in regel 5, wordt deze waarde 1 opgehaald en op 2 gezet;
- ...
Hier volgt een voorbeeld van de uitvoering:
![]() |
- in [1] krijgen we inderdaad de eerste waarde van de teller;
- bij [2] heeft de server een sessiecookie verzonden. Deze heeft de sleutel [JSESSIONID] en als waarde een unieke tekenreeks voor elke gebruiker. We weten dat de browser de cookies die hij ontvangt altijd terugstuurt. Dus wanneer we de actie [/m15] een tweede keer opvragen, stuurt de client dit cookie terug, waardoor de server hem kan herkennen en aan zijn sessie kan koppelen. Op deze manier wordt het geheugen van de gebruiker behouden;
Laten we eens kijken naar het tweede verzoek:
![]() |
- in [3] zien we dat de client de sessiecookie terugstuurt. We merken op dat deze sessiecookie niet meer in het antwoord van de server voorkomt. Het is nu de client die deze verstuurt om herkend te worden;
- in [4], de tweede waarde van de teller. Deze is inderdaad verhoogd;
4.14. [/m16]: een object met bereik [session] ophalen
Het kan wenselijk zijn om alle gegevens van de sessie van een gebruiker in één enkel object te plaatsen en alleen dit object in de sessie op te nemen. We volgen deze aanpak. We plaatsen de teller in het volgende object [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;
}
}
- regel 7: de annotatie [@Component] is een Spring-annotatie (regel 5) die van de klasse [SessionModel] een component maakt waarvan de levenscyclus door Spring wordt beheerd;
- regel 8: de annotatie [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] is eveneens een Spring-annotatie (regels 3-4). Wanneer Spring MVC deze tegenkomt, wordt de bijbehorende klasse aangemaakt en in de sessie van de gebruiker geplaatst. Het attribuut [proxyMode = ScopedProxyMode.TARGET_CLASS] is belangrijk. Dankzij dit attribuut maakt Spring MVC één instantie per gebruiker aan in plaats van één enkele instantie voor alle gebruikers (singleton);
- regel 11: de teller;
Om ervoor te zorgen dat deze nieuwe Spring-component wordt herkend, moet de configuratie van de applicatie in de klasse [Application] worden gecontroleerd:
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);
}
}
- regel 9: de Spring-componenten worden gezocht in het pakket [istia.st.springmvc.controllers]. Dit is niet langer voldoende. We passen deze regel als volgt aan:
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
We hebben het pakket toegevoegd waarin de klasse [SessionModel] zich bevindt.
Nu voegen we de volgende actie toe:
@Autowired
private SessionModel session;
// ------ een object met sessiescope beheren [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());
}
- regels 1-2: de Spring-component [SessionModel] wordt in de controller geïnjecteerd als [@Autowired]. We herinneren er hier aan dat een Spring-controller een singleton is. Het is dan ook paradoxaal om er een component met een kleiner bereik in te injecteren, in dit geval met het bereik [Session]. Hier komt de annotatie [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] van de component [SessionModel] om de hoek kijken. Telkens wanneer de code van de controller het veld [session] op regel 2 benadert, wordt een proxy-methode uitgevoerd om de sessie van het verzoek dat op dat moment door de controller wordt verwerkt, beschikbaar te maken;
- regel 6: het object [HttpSession] is niet langer nodig in de parameters van de actie;
- regel 7: de teller wordt opgehaald/verhoogd;
- regel 8: de waarde wordt teruggegeven;
Hier volgt een voorbeeld van de uitvoering:
De eerste keer
![]() |
De tweede keer
![]() |
Laten we nu een andere browser nemen die een tweede gebruiker vertegenwoordigt. We nemen hier de Opera-browser:
![]() |
Hierboven, in [1], krijgt deze tweede gebruiker een tellerwaarde van 1. Dit toont aan dat zijn sessie verschilt van die van de eerste gebruiker. Als we de client-servercommunicatie bekijken (ook in Opera via Ctrl-Shift-I), zien we in [2] dat deze tweede gebruiker een andere sessiecookie heeft dan de eerste gebruiker. Dit zorgt ervoor dat de sessies onafhankelijk van elkaar zijn.
4.15. [/m17]: een object uit het bereik van [application] ophalen
Laten we nog eens terugkomen op de uitvoeringsarchitectuur van een actie:
![]() |
We weten hoe we de sessie van de gebruiker moeten opbouwen. We gaan nu een scope-object [application] aanmaken waarvan de inhoud alleen-lezen is en toegankelijk voor alle gebruikers. We introduceren de klasse [ApplicationModel], die het scope-object [application] zal zijn:
![]() |
package istia.st.springmvc.models;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class ApplicationModel {
// teller
private AtomicLong compteur = new AtomicLong(0);
// getters en setters
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- regel 5: de annotatie [@Component] zorgt ervoor dat de klasse [ApplicationModel] een door Spring beheerde component wordt. De standaardkarakteristiek van Spring-componenten is het type [singleton]: er wordt slechts één exemplaar van de component aangemaakt wanneer de Spring-container wordt geïnstantieerd, d.w.z. doorgaans bij het opstarten van de applicatie. We kunnen deze levenscyclus gebruiken om configuratie-informatie in de singleton op te slaan die voor alle gebruikers toegankelijk is;
- regel 11: een teller van het type [AtomicLong]. Dit type heeft een zogenaamde atomaire methode [incrementAndGet]. Dit betekent dat een thread die deze methode uitvoert, er zeker van kan zijn dat een andere thread de waarde van de teller (Get) niet zal lezen tussen het moment van het uitlezen (Get) en het verhogen (increment) door de eerste thread. Dit zou namelijk fouten veroorzaken, aangezien twee threads dezelfde waarde van de teller zouden lezen, waardoor de teller in plaats van met twee te worden verhoogd, slechts met één zou worden verhoogd;
We maken de volgende nieuwe actie [/m17] aan:
@Autowired
private ApplicationModel application;
// ----- een object met applicatiebereik beheren [Autowired] ------------------------
@RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m17() {
return String.valueOf(application.getCompteur().incrementAndGet());
}
- regels 1-2: we injecteren de component [ApplicationModel] in de controller. Dit is een singleton. Elke gebruiker heeft dus een verwijzing naar hetzelfde object;
- regel 7: we geven de bereik-teller [application] terug nadat we deze hebben verhoogd;
Hier zijn twee voorbeelden, één met Chrome en één met Opera:
![]() | ![]() |
Hierboven zien we dat beide browsers met dezelfde teller hebben gewerkt, wat bij de sessie niet het geval was. Deze twee browsers staan symbool voor twee verschillende gebruikers die beiden toegang hebben tot de gegevens van het bereik [application]. Over het algemeen moet men vermijden om in de scope-objecten [application] lees-/schrijfgegevens op te nemen, zoals hierboven met de teller is gedaan. De uitvoeringsthreads van de verschillende gebruikers hebben namelijk tegelijkertijd toegang tot de gegevens van de scope [application]. Als er schrijfbare gegevens aanwezig zijn, moet de schrijftoegang worden gesynchroniseerd, zoals hierboven is gedaan met het type [AtomicLong]. Gelijktijdige toegang leidt tot programmeerfouten. Daarom verdient het de voorkeur om alleen alleen-lezen-gegevens in de [application]-scopebijlagen op te nemen.
4.16. [/m18]: een object van het bereik [session] ophalen met [@SessionAttributes]
Er is nog een andere manier om informatie uit het bereik [session] op te halen. We gaan het volgende object in de sessie plaatsen:
package istia.st.springmvc.models;
public class Container {
// de teller
public int compteur=10;
// de getters en setters
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
We gaan dit object gebruiken met de volgende twee acties:
// gebruik van [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// hier voegen we de sleutel [container] toe aan de sessie
session.setAttribute("container", new Container());
}
// gebruik van [@ModelAttribute] ----------------------
// de sleutel [container] van de sessie wordt hier ingevoegd
@RequestMapping(value = "/m19", method = RequestMethod.GET)
public String m19(@ModelAttribute("container") Container container) {
container.setCompteur(1 + container.getCompteur());
return String.valueOf(container.getCompteur());
}
- regels 3-6: de actie [/m18] levert geen resultaat op. Deze dient alleen om een object in de sessie aan te maken met de sleutel [container];
- regel 11: in de actie [/m19] wordt de annotatie [@ModelAttribute] gebruikt. Het gedrag van deze annotatie is vrij complex. De parameter [container] van deze annotatie kan verschillende dingen aanduiden, en met name een object uit de sessie. Hiervoor moet dit object zijn gedeclareerd met een annotatie [@SessionAttributes] op de klasse zelf:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- regel 2 hierboven geeft aan dat de sleutel [container] deel uitmaakt van de attributen van de sessie;
Samenvattend:
- in [/m18] wordt de sleutel [container] in de sessie opgenomen;
- de annotatie [@SessionAttributes({"container"})] zorgt ervoor dat deze sleutel kan worden ingevoegd in een parameter die is geannoteerd met [@ModelAttribute("container")];
- dit is niet zichtbaar in het volgende uitvoervoorbeeld, maar informatie die is geannoteerd met [@ModelAttribute] maakt automatisch deel uit van het model M dat naar de weergave V wordt verzonden;
Hier volgt een voorbeeld van de uitvoering. Eerst voegen we de sleutel [container] toe aan de sessie met de actie [/m18] [1]. Vervolgens roepen we de actie [/m19] twee keer aan om te zien hoe de teller wordt verhoogd.
![]() |
4.17. [/m20-/m23]: informatie invoegen met [@ModelAttribute]
Laten we de volgende nieuwe actie eens bekijken:
// het attribuut p zal deel uitmaken van alle weergavesjablonen [Model] ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------instantie van @ModelAttribute --------------------------
// wordt geïnjecteerd als het in de sessie voorkomt
// wordt ingevoegd als de controller een methode voor dit attribuut heeft gedefinieerd
// kan afkomstig zijn uit de velden van URL als er een converter bestaat van String naar het type van het attribuut
// anders wordt het aangemaakt met de standaardconstructor
// vervolgens worden de attributen van het model geïnitialiseerd met de parameters van GET of POST
// het eindresultaat wordt onderdeel van het model dat door de actie wordt gegenereerd
// het attribuut p wordt in de argumenten ingevoegd------------------------
@RequestMapping(value = "/m20", method = RequestMethod.GET)
public Personne m20(@ModelAttribute("p") Personne personne) {
return personne;
}
- regel 2-5: definiëren een modelattribuut met de naam [p]. Dit is het model M van een weergave V, een model dat wordt vertegenwoordigd door een type [Model] in Spring MVC. Een model gedraagt zich als een woordenboek van paren [clé, valeur]. Hier is de sleutel [p] gekoppeld aan het object [Personne] dat is aangemaakt door de methode [getPersonne]. De naam van de methode kan willekeurig zijn;
- regel 17: het sleutelsjabloonattribuut [p] wordt in de parameters van de actie geïnjecteerd. Deze injectie gebeurt volgens de regels van de regels 8-12. Hier hebben we te maken met het geval dat in regel 9 is gedefinieerd. Dus in regel 17 zal de parameter [Personne personne] het object [Personne(7,'abcd',14)] zijn;
- regel 18: het object [personne] wordt ter controle teruggegeven. Dit wordt geserialiseerd tot jSON voordat het naar de client wordt verzonden.
Hier volgt een voorbeeld:
![]() |
Laten we nu de volgende actie bekijken:
// --------- het attribuut p maakt automatisch deel uit van het sjabloon M van de weergave V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Een actie die een weergave V wil weergeven, moet het bijbehorende model M opbouwen. Spring MVC beheert dit met een type [Model] dat in de parameters van de actie kan worden geïnjecteerd. In eerste instantie is dit model leeg of bevat het de informatie die is gemarkeerd met de annotatie [@ModelAttribute]. De actie vult dit model al dan niet aan voordat het naar een weergave wordt doorgegeven.
- regel 3: invoegen van het M-model;
- regel 4: we willen zien wat erin staat. We serialiseren het als een tekenreeks om het naar de client te sturen. Hier wordt de methode [Personne.toString] gebruikt. Deze moet dus bestaan;
Hier volgt een uitvoering:
![]() |
Hierboven zien we dat de instructies:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
een invoer [p, Personne(7,'abcd',14)] in het model hebben aangemaakt. Dit is altijd het geval.
Laten we nu eens het volgende geval bekijken:
// anders wordt het opgebouwd met de standaardconstructor
// vervolgens worden de attributen van het model geïnitialiseerd met de parameters van de GET of de POST
met de volgende actie:
// --------- het modelattribuut [param1] maakt deel uit van het model, maar is niet geïnitialiseerd
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- regel 3: het sleutelsjabloonattribuut [param1] bestaat niet. In dit geval moet het bijbehorende type een standaardconstructor hebben. Dit is hier het geval voor het type [String], maar we kunnen niet [@ModelAttribute("param1") Integer p1] schrijven omdat de klasse [Integer] geen standaardconstructor heeft;
- regel 4: het model wordt teruggestuurd om te controleren of het sleutelmodelattribuut [param1] er deel van uitmaakt;
Hier volgt een voorbeeld van de uitvoering:
![]() |
Het sjabloonattribuut [param1] is wel degelijk aanwezig in het sjabloon, maar de methode [toString] van de bijbehorende waarde geeft geen informatie over deze waarde.
Laten we nu eens kijken naar de volgende actie, waarbij we expliciet informatie in het model opnemen:
// --------- het modelattribuut [param2] wordt expliciet in het model opgenomen
@RequestMapping(value = "/m23", method = RequestMethod.GET)
public String m23(String p2, Model model) {
model.addAttribute("param2",p2);
return model.toString();
}
- regel 4: de waarde [p2] die op regel 3 is opgehaald, wordt in het model geplaatst in verband met de sleutel [param2]:
Hier volgt een voorbeeld van de uitvoering:
![]() |
De regels veranderen als de parameter van de actie een object is. Hier is een eerste voorbeeld:
// ------ het modelattribuut [unePersonne] wordt automatisch in het model opgenomen
@RequestMapping(value = "/m23b", method = RequestMethod.GET)
public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
return model.toString();
}
De actie brengt geen wijzigingen aan in het model dat eraan is doorgegeven. Het resultaat is als volgt:
![]() |
We zien dat de annotatie [@ModelAttribute("unePersonne") Personne p1] de persoon [p1] in het model heeft geplaatst, gekoppeld aan de sleutel [unePersonne].
Laten we nu eens kijken naar de volgende actie:
// --------- de persoon p1 wordt automatisch in het model geplaatst
// -------- met als sleutel de naam van zijn klasse, waarbij het eerste teken een kleine letter is
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- regel 4: de annotatie [@ModelAttribute] is niet toegevoegd;
Het resultaat is als volgt:
![]() |
We zien dat de aanwezigheid van de parameter [Personne p1] de persoon [p1] in het model heeft geplaatst, gekoppeld aan de sleutel [personne], wat de naam is van de klasse [Personne] met het eerste teken in kleine letters.
4.18. [/m24]: validatie van het actiemodel
Laten we het volgende actiemodel [ActionModel01] eens bekijken:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// gegevens
@NotNull
private Integer a;
@NotNull
private Double b;
// getters en setters
...
}
- regels 8 en 9: de annotatie [@NotNull] is een validatiebeperking die aangeeft dat de geannoteerde gegevens niet de waarde null mogen hebben;
Laten we nu de volgende actie bekijken:
// ----------------------- validatie van een model ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// fouten?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// doorlopen van de lijst met fouten
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 {
// geen fouten
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
- regel 3: er wordt een object [ActionModel01] geïnstantieerd en de velden [a, b] ervan worden geïnitialiseerd met parameters met dezelfde namen. De annotatie [@Valid] geeft aan dat de geldigheidsbeperkingen moeten worden gecontroleerd. De resultaten van deze controle worden opgeslagen in de parameter van het type [BindingResult] (tweede parameter). De volgende controles vinden plaats:
- vanwege de annotaties [@NotNull] moeten de parameters [a] en [b] aanwezig zijn;
- vanwege het type [Integer a] moet de parameter [a], die van nature van het type [String] is, converteerbaar zijn naar het type [Integer];
- vanwege het type [Double b] moet de parameter [b], die van nature van het type [String] is, converteerbaar zijn naar het type [Double];
Met de annotatie [@Valid] worden de validatiefouten doorgegeven aan de parameter [BindingResult result]. Zonder de annotatie [@Valid] leiden de validatiefouten tot het vastlopen van de actie en stuurt de server een antwoord HTTP met status 500 (Internal server error) naar de client.
- regel 3: het resultaat van de actie is van het type [Map]. De tekenreeks jSON uit dit resultaat wordt naar de client verzonden. Er worden twee soorten woordenboeken samengesteld:
- bij een mislukking, een woordenboek met een vermelding ['errors', value], waarbij [value] een tekenreeks is die alle fouten beschrijft (regel 13);
- bij succes, een woordenboek met één item ['data',value], waarbij [value] zelf een woordenboek is met twee items: ['a', value], ['b', value] (regel 19);
- regels 9-12: voor elke gedetecteerde fout [error] wordt de tekenreeks [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()] samengesteld:
- het eerste element is het foutieve veld, [a] of [b],
- het tweede element is de afgewezen waarde, bijvoorbeeld [x],
- het derde element is een lijst met foutcodes. We zullen straks zien wat hun functie is;
- het vierde element is de foutcode. Deze maakt deel uit van de voorgaande lijst;
- het laatste element is de standaardfoutmelding. Er kunnen namelijk meerdere foutmeldingen zijn;
Hier volgen enkele uitvoervoorbeelden:
![]() |
Hierboven zien we dat:
- de toewijzing van 'x' aan het veld [ActionModel01.a] is mislukt en de foutmelding aangeeft waarom;
- de toewijzing van 'y' aan het veld [ActionModel01.b] is mislukt en de foutmelding geeft aan waarom;
Let op de foutcodes bij het veld [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. We komen op deze foutcodes terug wanneer we de foutmelding moeten aanpassen. Merk op dat de foutcode [typeMismatch] is.
Nog een voorbeeld:
![]() |
Hier zijn de parameters [a] en [b] niet doorgegeven. De validatoren [@NotNull] van het actiemodel [ActionModel01] hebben toen hun rol vervuld;
Eindelijk de juiste waarden:
![]() |
4.19. [m/24]: aanpassing van foutmeldingen
Laten we teruggaan naar een schermafbeelding van het vorige voorbeeld:
![]() |
Hierboven zien we de standaardfoutmeldingen. Het is duidelijk dat we deze in een echte applicatie niet kunnen behouden. Het is mogelijk om deze foutmeldingen zelf te definiëren. Hiervoor maken we gebruik van de foutcodes. Hierboven zien we dat de fout voor het veld [a] de volgende codes heeft: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Deze foutcodes lopen van de meest specifieke naar de minst specifieke:
- [typeMismatch.actionModel01.a]: typefout in het veld [a] van het type [ActionModel01];
- [typeMismatch.a]: typefout in een veld met de naam [a];
- [typeMismatch.java.lang.Integer]: typefout bij het type Integer;
- [typeMismatch]: typefout;
We merken ook op dat de foutcode voor het veld [a], verkregen via [error.getCode()], [typeMismatch] is (zie bovenstaande schermafbeelding).
We gaan de foutmeldingen in een eigenschappenbestand plaatsen:
![]() |
Het bovenstaande bestand [messages.properties] ziet er dan als volgt uit:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier
Elke regel heeft de volgende vorm:
Hier is de sleutel een foutcode en het bericht de foutmelding die bij deze code hoort.
Ter herinnering: de foutcodes voor de twee velden zijn:
- [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], wanneer de parameter [a] ongeldig is;
- [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] wanneer de parameter [b] ongeldig is;
- [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] wanneer de parameter [a] ontbreekt;
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] wanneer de parameter [b] ontbreekt;
Het bestand [messages.properties] moet voor alle mogelijke foutgevallen een foutmelding bevatten. In het geval dat:
- de parameters [a] en [b] ontbreken, wordt de code [NotNull] gebruikt;
- de parameter [a] is onjuist; we hebben foutmeldingen ingesteld voor twee codes: [typeMismatch.actionModel01.a, typeMismatch]. We zullen zien welke wordt gebruikt;
- als de parameter [b] onjuist is, wordt de code [typeMismatch] gebruikt;
Om ervoor te zorgen dat het bestand [messages.properties] wordt gebruikt, moet Spring worden geconfigureerd:
![]() |
We verwijderen de configuratie-annotaties uit de klasse [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);
}
}
- regel 8: de Spring Boot-applicatie wordt gestart. De eerste parameter van de statische methode [SpringApplication.run] is de klasse die de applicatie nu configureert;
De klasse [Config] is als volgt:
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;
}
}
- regels 11-13: hier staan de configuratie-annotaties die voorheen in de klasse [Application] stonden;
- regel 14: om een Spring-applicatie MVC te configureren, moet de klasse [WebMvcConfigurerAdapter] worden uitgebreid;
- regel 15: de annotatie [@Bean] introduceert een Spring-component, een singleton;
- regel 16: er wordt een bean gedefinieerd met de naam [messageSource] (de naam van de methode). Deze bean dient om de berichtbestanden van de applicatie te definiëren en moet verplicht deze naam hebben;
- regels 17-19: geven aan Spring door dat het berichtenbestand:
- zich bevindt in de map [i18n] in het classpath van het project (regel 18),
- [messages.properties] heet (regel 18). In feite is de term [messages] de basis van de namen van de berichtbestanden, in plaats van de naam zelf. We zullen zien dat er in het kader van internationalisering meerdere berichtbestanden kunnen zijn, één per ondersteunde taal. Zo kunnen we bijvoorbeeld [messages_fr.properties] hebben voor de Franse taal en [messages_en.properties] voor de Engelse taal. De achtervoegsels die aan de basisnaam [messages] worden toegevoegd, zijn gestandaardiseerd. Je kunt niet zomaar alles gebruiken;
In het project STS moet de map [i18n] in de map met bronbestanden worden geplaatst, omdat deze in het classpath van het project wordt opgenomen:
![]() |
Om dit bestand te gebruiken, maken we de volgende nieuwe actie aan:
// validatie van een model, beheer van foutmeldingen ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// het resultatenwoordenboek
Map<String, Object> map = new HashMap<String, Object>();
// de Spring-applicatiecontext
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// lokale
Locale locale = RequestContextUtils.getLocale(request);
// fouten?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// zoeken naar het foutbericht op basis van foutcodes
// de foutmelding wordt gezocht in de berichtenbestanden
// de foutcodes in tabelvorm
String[] codes = error.getCodes();
// in de vorm van een tekenreeks
String listCodes = String.join(" - ", codes);
// zoeken
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// is er iets gevonden?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// gevonden – de foutmelding wordt toegevoegd aan de lijst met foutmeldingen
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;
}
Deze code is vergelijkbaar met die van de actie [/m24]. We leggen de verschillen uit:
- regel 3: we injecteren de aanroep [HttpServletRequest request] in de parameters van de actie. Die hebben we nodig;
- regels 7-8: we halen de Spring-context op. Deze context bevat alle Spring-beans van de applicatie. Hiermee hebben we ook toegang tot de berichtbestanden;
- regel 10: we halen de locale van de applicatie op. Deze term wordt iets verderop toegelicht;
- regels 15-31: voor elke fout zoeken we een bericht dat overeenkomt met een van deze foutcodes. Deze worden gezocht in de volgorde van de codes die in [error.getCodes()] zijn gevonden. Zodra er een bericht is gevonden, stoppen we;
- regel 26: hoe je een bericht kunt ophalen in [messages.properties]:
- de eerste parameter is de code die wordt gezocht in [messages.properties],
- de tweede is een array met parameters, omdat berichten soms geparametriseerd zijn. Dat is hier niet het geval,
- de derde is de gebruikte locale (verkregen uit regel 10). De locale geeft de gebruikte taal aan: [fr_FR] voor Frans (Frankrijk), [en_US] voor Engels van de USA. Het bericht wordt gezocht in messages_[locale].properties, dus bijvoorbeeld [messages_fr_FR.properties]. Als dit bestand niet bestaat, wordt het bericht gezocht in [messages_fr.properties]. Als dit bestand niet bestaat, wordt het bericht gezocht in [messages.properties]. Dit laatste geval is voor ons van toepassing;
- regels 25-29: enigszins onverwacht is dat wanneer we naar een niet-bestaande code zoeken in een berichtenbestand, we een uitzondering krijgen in plaats van een null-pointer;
- regels 33-35: we behandelen het geval waarin er geen foutmelding is;
- regels 37-38: we stellen de foutstring samen. Hierin nemen we de locale en de gevonden foutmelding op;
Hier volgen enkele uitvoervoorbeelden:
![]() |
We zien dat:
- de locale van de applicatie [fr_FR] is. Dit is een standaardwaarde, aangezien we niets hebben gedaan om deze te initialiseren;
- dat de melding die voor beide velden wordt gebruikt, als volgt luidt:
NotNull=Le champ ne peut être vide
Nog een voorbeeld:
![]() |
We zien dat:
- de foutmelding die wordt gebruikt voor de parameter [a] als volgt luidt:
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
- de foutmelding die wordt gebruikt voor de parameter [b] is als volgt:
typeMismatch=Format invalide
Waarom zijn er twee verschillende foutmeldingen? Voor de parameter [a] waren er twee mogelijke foutmeldingen:
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
De foutcodes zijn in de volgorde van de tabel [error.getCodes()] doorgenomen. Deze volgorde loopt van de meest specifieke code naar de meest algemene code. Daarom werd de code [typeMismatch.model01.a] als eerste gevonden.
4.20. [/m25]: internationalisering van een Spring-applicatie MVC
Nu we weten hoe we foutmeldingen in het Frans kunnen aanpassen, willen we deze ook in het Engels hebben, wat ons bij de internationalisering van een Spring-applicatie MVC brengt. Om dit te regelen, gaan we de configuratieklasse [Config] uitbreiden, die er dan als volgt uitziet:
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;
}
}
- regels 28-32: we maken een verzoekinterceptor aan. Een verzoekinterceptor breidt de interface [HandlerInterceptor] uit. Een dergelijke klasse inspecteert het binnenkomende verzoek voordat het door een actie wordt verwerkt. Hier zoekt de interceptor [localeChangeInterceptor] in het binnenkomende verzoek naar een parameter met de naam [lang], GET of POST en past de taalinstelling van de applicatie aan op basis van deze parameter. Als de parameter dus [lang=en_US] is, wordt de taalinstelling van de applicatie Engels (USA);
- regels 34-37: de methode [WebMvcConfigurerAdapter.addInterceptors] wordt opnieuw gedefinieerd om de voorgaande interceptor toe te voegen;
- regels 39-45: dienen om in te stellen hoe de taalinstelling in een cookie wordt ingekapseld. We weten dat een cookie kan dienen als geheugen van de gebruiker, aangezien de clientbrowser deze systematisch naar de server terugstuurt. De voorgaande interceptor [localeChangeInterceptor] maakt een cookie aan waarin de taalinstelling is ingekapseld. Regel 42 geeft deze cookie de naam [lang]. De cookie wordt ook gebruikt om de locale te wijzigen;
- regel 43: geeft aan dat, bij afwezigheid van het cookie [lang], de locale [fr] zal zijn;
Kortom, de locale van een verzoek kan op twee manieren worden vastgelegd:
- door een parameter met de naam [lang] door te geven;
- door een cookie met de naam [lang] te verzenden. Deze cookie wordt automatisch aangemaakt na uitvoering van de vorige methode;
Om deze locale te gebruiken, gaan we berichtbestanden aanmaken voor de locales [fr] en [en]:
![]() |
Het bestand [messages_fr.properties] ziet er als volgt uit:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Het bestand [messages_en.properties] is als volgt:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Het bestand [messages.properties] is een kopie van het bestand [messages_en.properties]. Ter herinnering: het bestand [messages.properties] wordt gebruikt wanneer er geen bestand wordt gevonden dat overeenkomt met de locale van het verzoek. In ons geval, als de gebruiker de parameter [lang=en] verstuurt, zal het bestand [messages.properties] worden gebruikt, aangezien het bestand [messages_en.properties] niet bestaat. De gebruiker krijgt dan berichten in het Engels te zien.
Laten we het eens proberen. Controleer allereerst in de Chrome-ontwikkelomgeving (Ctrl-Shift-I) je cookies:
![]() |
Als je een cookie hebt met de naam [lang], verwijder deze dan. Vraag vervolgens in Chrome de bestanden URL en [http://localhost:8080/m25] op:
![]() |
De browser heeft de volgende headers HTTP verzonden:
We zien dat er in deze headers geen cookie [lang] voorkomt. Onze code gebruikt in dit geval de locale [fr]. Dit is te zien op de schermafbeelding. Laten we nog een ander geval bekijken:
![]() |
- in [1] hebben we de parameter [lang=en] doorgegeven om de locale te wijzigen naar [en];
- bij [2] zien we de nieuwe locale;
- in [3] is het bericht in het Engels weergegeven;
Laten we nu eens kijken naar de uitwisselingen in HTTP:
![]() |
Hierboven zien we dat de server een cookie [lang] heeft teruggestuurd. Dit heeft een belangrijk gevolg: de locale van het volgende verzoek zal weer [en] zijn vanwege de cookie [lang] die door de browser zal worden teruggestuurd. We zouden de berichten dus in het Engels moeten houden. Laten we dit eens controleren:
![]() |
Hierboven zien we dat de locale op [en] is gebleven. Vanwege het cookie dat de browser systematisch verstuurt, zal dit zo blijven zolang de gebruiker het niet wijzigt door de parameter [lang] als volgt te versturen:
![]() |
4.21. [/m26]: de locale in het actiemodel invoegen
In het vorige voorbeeld hebben we een manier gezien om de locale uit het verzoek op te halen:
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
...
// lokaal
Locale locale = RequestContextUtils.getLocale(request);
// fouten?
De locale kan rechtstreeks in de parameters van de actie worden ingevoegd. Hier volgt een voorbeeld:
@RequestMapping(value = "/m26", method = RequestMethod.GET)
public String m26(Locale locale) {
return String.format("locale=%s", locale.toString());
}
![]() | ![]() |
![]() |
Hierboven is te zien dat de geldigheid van de gevraagde locale niet wordt gecontroleerd. Toch veroorzaakt het volgende verzoek van de browser een uitzondering aan de serverzijde, omdat de locale-cookie die hij ontvangt onjuist is.
4.22. [/m27]: de geldigheid van een model controleren met Hibernate Validator
Laten we de volgende nieuwe actie eens bekijken:
//validatie van een model met 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>();
// fouten?
if (result.hasErrors()) {
// door de lijst met fouten bladeren
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// geen fouten
map.put("data", data);
}
return map;
}
Dit is code die we inmiddels al meerdere keren hebben gezien:
- regel 3: de actie [/m27] wordt aangevraagd via een POST;
- regels 8-11: elke fout wordt gekenmerkt door [champ, message] met:
- veld: het foutieve veld,
- message: de bijbehorende foutmelding en de lijst met foutcodes;
- regel 14: als er geen fouten zijn, wordt de tekenreeks jSON met de verzonden waarden teruggestuurd;
Op regel 3 wordt het volgende actiesjabloon [ActionModel02] gebruikt:
![]() |
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 en setters
...
}
De klasse maakt gebruik van validatievoorwaarden uit twee pakketten:
- [javax.validation.constraints] op de regels 5-13;
- [org.hibernate.validator.constraints] op de regels 15-19;
De Maven-afhankelijkheden van deze twee pakketten zijn aanwezig in het project:
![]() |
Hier gaan we geen geïnternationaliseerde berichten gebruiken, maar berichten die binnen de validatiecontrole zijn gedefinieerd met het attribuut [message]. Om deze actie te testen, gebruiken we [Advanced Rest Client]:
![]() |
- in [1-2], de aanvraag POST;
- in [3], de header HTTP [Content-Type] die moet worden gebruikt;
- in [4] maakt de link [Add new value] het mogelijk om een koppel [paramètre, value] toe te voegen;
- in [5] moet een veld uit [ActionModel02] worden geplaatst, in dit geval het veld [assertFalse]:
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
- in [6] een onjuiste waarde invoeren om een foutmelding te zien. Hierboven vereist de beperking [@AssertFalse] dat het veld [assertFalse] de waarde [false] heeft;
![]() |
- in [7], het antwoord van de server: de beperking [@NotNull] voor lege velden is geactiveerd en de bijbehorende foutmelding is weergegeven;
- in [8], het bericht van het veld [assertFalse] waarvoor de beperking [@AssertFalse] niet werd nageleefd, evenals de codes van deze fout. We herinneren eraan dat deze codes gekoppeld kunnen zijn aan geïnternationaliseerde berichten;
Hier volgt nog een voorbeeld:
![]() |

De lezer wordt uitgenodigd om de verschillende foutgevallen te testen tot aan POST, waarbij alle gegevens geldig zijn:
![]() | ![]() |
Opmerking: de datumnotatie is de Angelsaksische notatie: mm/dd/jjjj.
4.23. [/m28]: uitbesteding van foutmeldingen
In de klasse [ActionModel02] hebben we de meldingen 'hard gecodeerd'. Het is beter om deze onder te brengen in afzonderlijke meldingsbestanden. We volgen het voorbeeld van de actie [/m25]. We maken het volgende nieuwe actiesjabloon [ActionModel03] aan:
![]() |
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 en setters
...
}
De foutmeldingen worden ondergebracht in de bestanden [messages.properties]:
![]() |
Het bestand [messages_fr.properties] is als volgt:
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
De foutmeldingen zijn toegevoegd aan de regels 4-16. Ze hebben de volgende vorm:
De codes mogen niet willekeurig zijn. Het zijn de codes die worden weergegeven in de vorige actie [/m27]. Bijvoorbeeld:
![]()
In de berichtbestanden moet voor het veld [int1014] een van de vier bovenstaande codes worden gebruikt.
Het bestand [messages_en.properties] ziet er als volgt uit:
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
Het actiesjabloon [ActionModel03] wordt gebruikt door de volgende actie:
// ----------------------- uitbesteding van foutmeldingen ------------------------
@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>();
// de Spring-applicatiecontext
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// locale
Locale locale = RequestContextUtils.getLocale(request);
// van fouten?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// zoeken naar het foutbericht op basis van foutcodes
// de foutmelding wordt gezocht in de berichtenbestanden
// de foutcodes in tabelvorm
String[] codes = error.getCodes();
// in de vorm van een tekenreeks
String listCodes = String.join(" - ", codes);
// zoeken
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// is er iets gevonden?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// gevonden – de fout wordt aan het woordenboek toegevoegd
map.put(error.getField(), msg);
}
} else {
// geen fouten
map.put("data", data);
}
return map;
}
Dit type code is al eerder besproken. Het enige wat echt belangrijk is, is regel 23: de weergegeven foutmelding is afhankelijk van de locale van de aanvraag.
Hier volgt een voorbeeld in het Frans:
![]() | ![]() |
en nu in het Engels:
![]() | ![]() |









































































