4. Działania: model
Wróćmy do architektury aplikacji Spring MVC:
![]() |
W poprzednim rozdziale przyjrzeliśmy się procesowi, w ramach którego żądanie [1] trafia do kontrolera i akcji [2a], które je przetworzą – mechanizm ten nazywamy routingiem. Przedstawiliśmy również różne odpowiedzi, jakie akcja może zwrócić do przeglądarki. Jak dotąd omawialiśmy akcje, które nie wykorzystywały przekazanego im żądania. Żądanie [1] zawiera różne informacje, które Spring MVC przekazuje akcji w postaci modelu. Nie należy mylić tego terminu z modelem M widoku V [2c], który jest generowany przez akcję:
![]() |
- żądanie klienta HTTP trafia do [1];
- w [2] informacje zawarte w żądaniu zostaną przekształcone w szablon akcji [3] – często, choć niekoniecznie, jest to klasa – który posłuży jako dane wejściowe dla akcji [4];
- w [4] akcja, na podstawie tego modelu, wygeneruje odpowiedź. Będzie ona składać się z dwóch elementów: widoku V [6] oraz modelu M tego widoku [5];
- widok V [6] wykorzysta swój szablon M [5] do wygenerowania odpowiedzi HTTP przeznaczonej dla klienta.
W modelu MVC akcja [4] stanowi część C (kontroler), model widoku [5] to M, a widok [6] to V.
W niniejszym rozdziale omówiono mechanizmy powiązania między informacjami przekazywanymi w żądaniu, które z natury są ciągami znaków, a modelem akcji, który może być klasą posiadającą właściwości różnych typów.
Uwaga: termin „[Modèle d'action]” nie jest rozpoznawany.
Tworzymy nowy kontroler dla tych nowych akcji:
![]() |
Kod kontrolera [ActionModelController] będzie na razie wyglądał następująco:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- wiersz 5: przypominamy, że adnotacja [@RestController] powoduje, że odpowiedź wysyłana do klienta jest serializacją w postaci ciągu znaków wyniku działań kontrolera;
4.1. [/m01]: parametry kontrolera GET
Dodajemy następującą akcję [/m01]:
// ----------------------- pobieranie parametrów za pomocą 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);
}
- wiersz 4: akcja przyjmuje dwa parametry o nazwach [nom] i [age]. Zostaną one zainicjowane parametrami o tych samych nazwach w żądaniu HTTP GET;
W przeglądarce Chrome wyniki dla [1-3] są następujące:
![]() |
- w [1] – zapytanie GET z parametrami [nom] i [age];
- w [3] widać, że akcja [/m01] prawidłowo pobrała te parametry;
4.2. [/m02]: parametry akcji POST
Dodajemy następującą akcję [/m02]:
// ----------------------- pobieranie parametrów za pomocą 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);
}
- wiersz 4: akcja przyjmuje dwa parametry o nazwach [nom] i [age]. Zostaną one zainicjowane parametrami o tych samych nazwach w zapytaniu HTTP POST;
Wyniki dla [Advanced rest Client] są następujące:
![]() |
- w [1-3] zapytanie POST z parametrami [nom] i [age];
- w [4-5] ustala się nagłówek HTTP [Content-Type] zapytania POST. Powinien on brzmieć [Content-Type: application/x-www-form-urlencoded];
- w [6], [Form Data] zawiera listę parametrów operacji POST. Tutaj widoczne są parametry [nom] i [age];
- w [7] znajduje się odpowiedź serwera, która pokazuje, że operacja [/m02] pomyślnie pobrała parametry [nom] i [age]; ;
4.3. [/m03]: parametry o tych samych nazwach
W punkcie 2.5.2.8 widzieliśmy, że lista z możliwością wielokrotnego wyboru może wysyłać do serwera parametry o tych samych nazwach. Zobaczmy, w jaki sposób akcja może je pobrać. Dodajemy następującą akcję [/m03]:
// ----------------------- pobieranie parametrów o tych samych nazwach-----------------
@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));
}
- wiersz 2: akcja przyjmuje parametr o nazwie [nazwa[]]. Zostanie on tutaj zainicjowany wszystkimi parametrami o tej nazwie, niezależnie od tego, czy pochodzą one z GET, czy z POST, ponieważ w tym przypadku typ żądania nie został określony;
Wyniki są następujące:
![]() |
- poprzez POST [1] wysyłane są parametry [2];
- parametry umieszcza się również w URL i [3];
- w pliku [4] znajdują się cztery parametry o tej samej nazwie, co w pliku [nom]: [Query String parameters] są parametrami dla URL, a [Form Data] to przesłane parametry;
- w [5] widać, że akcja [/m03] pobrała cztery parametry o nazwach [nom];
4.4. [/m04]: mapowanie parametrów akcji na obiekt Java
Niech nowa akcja [/m04] będzie wyglądała następująco:
// ------ mapowanie parametrów na obiekt (Command Object) ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- wiersz 3: akcja ma jako parametr osobę o następującym typie:
public class Personne {
// identyfikator
private Integer id;
// nazwa
private String nom;
// wiek
private int age;
....
// metody pobierające i ustawiające
...
}
- aby utworzyć parametr [Personne personne], Spring MVC tworzy obiekt [new Personne()];
- następnie, jeśli istnieją parametry o nazwach odpowiadających polom [id, nom, age] utworzonego obiektu, instancjonuje go z tymi polami za pomocą ich metod setterów;
- wiersz 4: akcja zwraca typ [Personne], który zostanie zatem zserializowany do postaci ciągu znaków przed wysłaniem do klienta. Widzieliśmy, że domyślnie przeprowadzana jest serializacja typu jSON. Klient powinien zatem otrzymać ciąg znaków jSON dotyczący danej osoby;
Oto przykład:
![]() |
- w [1], parametry [id, nom, age] do utworzenia obiektu [Personne];
- w [2], ciąg znaków jSON tej osoby;
Co się stanie, jeśli nie wyślemy wszystkich pól dotyczących danej osoby? Spróbujmy:
![]() |
- na [2], zainicjowano tylko parametr [id];
4.5. [/m05]: pobranie elementów z URL
Niech nowa akcja [/m05] będzie następująca:
// ----------------------- pobieranie elementów z obiektu 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;
}
- wiersz 2: przetwarzana akcja URL ma postać [/m05/{a}/x/{b}], gdzie {param} jest elementem parametrów akcji URL;
- wiersz 3: elementy parametrów z pliku URL są pobierane wraz z adnotacją [@PathVariable];
- wiersze 4–6: pobrane elementy [a] i [b] są umieszczane w słowniku;
- wiersz 7: odpowiedzią będzie ciąg znaków jSON z tego słownika;
Wyniki są następujące:
![]() |
4.6. [/m06]: pobranie elementów z URL oraz parametrów
Oto nowa akcja [/m06]:
// -------- pobieranie elementów z URL oraz parametrów---------------
@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;
}
- wiersz 3: pobierane są zarówno elementy z URL i [Integer a, Double b], jak i parametr (GET lub POST) z [Double c];
- wiersze 4–7: elementy te są umieszczane w słowniku;
- wiersz 8: tworzy to odpowiedź klienta, który otrzyma zatem ciąg znaków jSON z tego słownika;
Oto wyniki:
![]() |
Należy zwrócić uwagę na znak / na końcu ścieżki [http://localhost:8080/m06/100/x/200.43/]. Bez niego otrzymujemy następujący nieprawidłowy wynik:
![]() |
4.7. [/m07]: uzyskanie dostępu do całego zapytania
Rozważmy następującą nową akcję [/m07]:
// ------ dostęp do zapytania HttpServletRequest ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// nagłówki 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();
}
- wiersz 3: prosimy Spring MVC o wstrzyknięcie obiektu [HttpServletRequest request], który zawiera wszystkie informacje, jakie można uzyskać na temat zapytania;
- wiersze 5–10: pobieramy wszystkie nagłówki HTTP z żądania, aby połączyć je w ciąg znaków, który wysyłamy do klienta (wiersz 11);
Wyniki są następujące:
![]() |
- w [1] – nagłówki HTTP z zapytania;
![]() |
- na [2], odpowiedź. Znajdują się w niej wszystkie nagłówki HTTP z żądania.
4.8. [/m08]: dostęp do obiektu [Writer]
Rozważmy następującą akcję:
// ----------------------- wstrzyknięcie writer ------------------------
@RequestMapping(value = "/m08", method = RequestMethod.GET)
public void m08(Writer writer) throws IOException {
writer.write("Bonjour le monde !");
}
- wiersz 3: Spring MVC wstrzykuje obiekt [Writer writer], który umożliwia zapis w strumieniu odpowiedzi dla klienta;
- wiersz 3: akcja zwraca typ [void], co oznacza, że musi samodzielnie skonstruować odpowiedź dla klienta;
- wiersz 4: dodanie tekstu do strumienia odpowiedzi dla klienta;
Wyniki są następujące:
![]() |
- w [2] widać, że nagłówek HTTP [Content-Type] nie został wysłany;
- w przypadku [3] – odpowiedź;
4.9. [/m09]: dostęp do nagłówka HTTP
Rozważmy następującą akcję:
// ----------------------- wstrzyknięcie kodu RequestHeader ------------------------
@RequestMapping(value = "/m09", method = RequestMethod.GET)
public String m09(@RequestHeader("User-Agent") String userAgent) {
return userAgent;
}
- wiersz 3: adnotacja [@RequestHeader("User-Agent")] pozwala pobrać nagłówek HTTP [User-Agent];
- wiersz 4: zwracamy tekst tego nagłówka;
Wyniki są następujące:
![]() |
- w [2] nagłówki HTTP i [User-Agent];
![]() |
- na [3], akcja [/m08] poprawnie pobrała ten nagłówek;
4.10. [/m10, /m11]: dostęp do pliku cookie
Plik cookie to zazwyczaj nagłówek HTTP, który:
- serwer wysyła po raz pierwszy do klienta;
- klient następnie systematycznie odsyła do serwera;
Najpierw utwórzmy akcję, która tworzy plik cookie:
// ----------------------- tworzenie pliku cookie ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- wiersz 3: wstawiamy obiekt [HttpServletResponse response], aby uzyskać pełną kontrolę nad odpowiedzią;
- wiersz 4: tworzymy plik cookie z kluczem [cookie1] i wartością [remember me] (Uwaga: znaki z akcentami w wartości pliku cookie powodują błędy);
- wiersz 3: akcja nie zwraca żadnego wyniku. Ponadto nie zapisuje niczego w treści odpowiedzi. Klient otrzyma zatem pusty dokument. Odpowiedź służy wyłącznie do dodania do niej nagłówka pliku cookie o nazwie HTTP;
Spójrzmy na wyniki:
![]() |
- w [1]: żądanie;
- w [2]: odpowiedź jest pusta;
- w [3]: plik cookie utworzony przez akcję;
Teraz utwórzmy akcję, która pobierze ten plik cookie, który przeglądarka będzie odtąd wysyłać przy każdym żądaniu:
// ----------------------- wstrzyknięcie pliku cookie ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- wiersz 3: adnotacja [@CookieValue("cookie1")] pozwala pobrać plik cookie o kluczu [cookie1];
- wiersz 4: ta wartość będzie odpowiedzią wysłaną do klienta;
Sprawdźmy wyniki:
![]() |
- w [2] widać, że przeglądarka zwraca plik cookie;
- w [3] widać, że akcja pomyślnie odzyskała plik cookie;
4.11. [/m12]: dostęp do treści POST
Parametry wysyłane metodą POST są zazwyczaj opatrzone nagłówkiem HTTP [Content-Type: application/x-www-form-urlencoded]. Można uzyskać dostęp do całego wysłanego ciągu znaków. Tworzymy następującą akcję:
// ----------- pobranie treści obiektu typu String o nazwie POST------------------------
@RequestMapping(value = "/m12", method = RequestMethod.POST)
public String m12(@RequestBody String requestBody) {
return requestBody;
}
- wiersz 3: adnotacja [@RequestBody] pozwala pobrać treść z POST. Zakładamy tutaj, że jest to typ [String];
- wiersz 4: treść ta jest zwracana do klienta;
Oto pierwszy przykład:
![]() |
- w [2] – wartości przesłane;
- w [3] – nagłówek HTTP [Content-Type] żądania;
- w [4] – odpowiedź serwera;
Parametry wysyłane metodą POST nie zawsze mają prostą postać [p1=v1&p2=v2], z której często korzystaliśmy do tej pory. Rozważmy bardziej złożony przypadek:
![]() |
- w [2-3]: wartości wysyłane wprowadzamy w postaci [clé:value];
- w przypadku [5] – ciąg znaków, który został przesłany;
W przypadku typu [Content-Type: application/x-www-form-urlencoded] wysyłany ciąg znaków musi mieć postać [p1=v1&p2=v2]. Jeśli chcemy wysłać dowolną treść, wybieramy typ [Content-Type: text/plain]. Oto przykład:
![]() |
- w przypadku [2-3] tworzymy nagłówek HTTP [Content-Type]. Domyślnie [5] – to właśnie ten zostanie użyty zamiast tego zdefiniowanego w [6]. Atrybut [charset=utf-8] jest ważny. Bez niego tracimy znaki akcentowane z wysłanego ciągu znaków;
- w [4] łańcuch przesłany jest poprawnie odzyskiwany w [7];
4.12. [/m13, /m14]: pobieranie wartości wysłanych w formacie jSON
Możliwe jest wysyłanie parametrów z nagłówkiem HTTP [Content-Type: application/json]. Tworzymy następującą akcję:
// ----------------------- pobieranie treści jSON z obiektu POST
@RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
public String m13(@RequestBody Personne personne) {
return personne.toString();
}
- wiersz 2: [consumes = "application/json"] określa, że akcja oczekuje treści o nazwie jSON;
- wiersz 3: [@RequestBody] reprezentuje tę treść. Ta adnotacja została powiązana z obiektem typu [Personne]. Treść jSON zostanie automatycznie deserializowana w tym obiekcie;
- wiersz 4: stosuje się metodę [Personne].toString(), aby zwrócić wartość inną niż wysłany ciąg znaków jSON;
Oto przykład:
![]() |
- w [2], wysłany ciąg znaków jSON;
- na [3], czyli [Content-Type] z żądania;
- na [4] – odpowiedź serwera;
Można to zrobić inaczej:
// ----------------------- pobranie treści jSON z obiektu typu 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();
}
- wiersz 2: wskazano, że metoda oczekuje strumienia typu [text/plain]. Spring MVC przetworzy wówczas treść żądania jako typ [String] (wiersz 3);
- wiersz 4: deserializujemy ciąg znaków jSON do obiektu typu [Personne] (patrz rozdział 9.7, strona 542);
Wyniki są następujące:
![]() |
- na [3], należy wpisać [text/plain];
4.13. [/m15]: odzyskaj sesję
Wróćmy do architektury wykonywania akcji:
![]() |
Klasa kontrolera jest instancjonowana na początku żądania klienta i niszczona po jego zakończeniu. Nie może więc służyć do przechowywania danych między dwoma żądaniami, nawet jeśli jest wywoływana wielokrotnie. Można chcieć przechowywać dwa rodzaje danych:
- dane wspólne dla wszystkich użytkowników aplikacji internetowej. Są to zazwyczaj dane tylko do odczytu;
- dane współdzielone przez żądania tego samego klienta. Dane te są przechowywane w obiekcie zwanym sesją. Mówimy wówczas o sesji klienta, aby określić pamięć klienta. Wszystkie żądania danego klienta mają dostęp do tej sesji. Mogą w niej przechowywać i odczytywać informacje.
![]() |
Powyżej przedstawiono rodzaje pamięci, do których ma dostęp akcja:
- pamięć aplikacji, która w większości przypadków zawiera dane tylko do odczytu i jest dostępna dla wszystkich użytkowników;
- pamięć konkretnego użytkownika, czyli sesja, która zawiera dane do odczytu i zapisu i jest dostępna dla kolejnych żądań tego samego użytkownika;
- nie przedstawiono powyżej, istnieje pamięć żądania lub kontekst żądania. Żądanie użytkownika może być przetwarzane przez kilka kolejnych akcji. Kontekst żądania pozwala akcji 1 przekazać informacje do akcji 2.
Przyjrzyjmy się pierwszemu przykładowi ilustrującemu te różne rodzaje pamięci:
// ----------------------- pobranie sesji ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// pobieramy obiekt klucza [compteur] z sesji
Object objCompteur = session.getAttribute("compteur");
// konwertujemy go na liczbę całkowitą w celu zwiększenia jego wartości
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// umieszczamy go ponownie w sesji
session.setAttribute("compteur", iCompteur);
// zwracamy go jako wynik akcji
return String.valueOf(iCompteur);
}
Spring MVC utrzymuje sesję użytkownika w obiekcie typu [HttpSession].
- wiersz 3: prosimy Spring MVC o wstrzyknięcie obiektu [HttpSession] do parametrów akcji;
- wiersz 5: pobieramy z nich atrybut o nazwie [compteur]. Sesja zachowuje się jak słownik, czyli zbiór par [clé, valeur]. Jeśli klucz [compteur] nie istnieje w sesji, pobierany jest wskaźnik null;
- wiersz 7: wartość powiązana z kluczem [compteur] będzie miała typ [Integer];
- wiersz 8: zwiększenie wartości licznika;
- wiersz 10: aktualizacja licznika w sesji;
- wiersz 12: wartość licznika jest wysyłana do klienta;
Gdy [/m15] zostanie wykonane po raz:
- pierwszy raz, w wierszu 12 licznik będzie miał wartość 1;
- za drugim razem, w wierszu 5, wartość ta zostanie pobrana i zmieniona na 2;
- ...
Oto przykład wykonania:
![]() |
- w [1] otrzymujemy rzeczywiście pierwszą wartość licznika;
- w [2] serwer wysłał plik cookie sesji. Posiada on klucz [JSESSIONID], a jako wartość unikalny ciąg znaków dla każdego użytkownika. Pamiętajmy, że przeglądarka zawsze odsyła otrzymane pliki cookie. Gdy więc po raz drugi wyślemy żądanie o akcję [/m15], klient odeśle ten plik cookie, co pozwoli serwerowi rozpoznać go i powiązać z sesją użytkownika. W ten sposób utrzymywana jest pamięć użytkownika;
Przyjrzyjmy się drugiemu żądaniu:
![]() |
- w [3] widać, że klient odsyła plik cookie sesji. Można zauważyć, że w odpowiedzi serwera nie ma już tego pliku cookie sesji. Teraz to klient go wysyła, aby zostać rozpoznanym;
- w [4], druga wartość licznika. Została ona prawidłowo zwiększona;
4.14. [/m16]: pobranie obiektu o zakresie [session]
Można chcieć umieścić wszystkie dane sesji użytkownika w jednym obiekcie i umieścić w sesji wyłącznie ten obiekt. Obieramy tę ścieżkę. Umieszczamy licznik w następującym obiekcie [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;
}
}
- wiersz 7: adnotacja [@Component] jest adnotacją Springa (wiersz 5), która sprawia, że klasa [SessionModel] staje się komponentem, którego cykl życia jest zarządzany przez Spring;
- wiersz 8: adnotacja [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] jest również adnotacją Springa (wiersze 3–4). Gdy Spring MVC napotka tę adnotację, tworzona jest odpowiednia klasa i umieszczana w sesji użytkownika. Atrybut [proxyMode = ScopedProxyMode.TARGET_CLASS] jest istotny. To właśnie dzięki niemu Spring MVC tworzy instancję dla każdego użytkownika z osobna, a nie jedną instancję dla wszystkich użytkowników (singleton);
- wiersz 11: licznik;
Aby ten nowy komponent Spring został rozpoznany, należy sprawdzić konfigurację aplikacji w klasie [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);
}
}
- wiersz 9: komponenty Spring są wyszukiwane w pakiecie [istia.st.springmvc.controllers]. To już nie wystarcza. Modyfikujemy ten wiersz w następujący sposób:
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
Dodaliśmy pakiet, w którym znajduje się klasa [SessionModel].
Teraz dodajemy następującą akcję:
@Autowired
private SessionModel session;
// ------ zarządzanie obiektem o zasięgu sesji [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());
}
- wiersze 1–2: komponent Spring [SessionModel] jest wstrzykiwany jako [@Autowired] do kontrolera. Warto tutaj przypomnieć, że kontroler Springa jest singletonem. Paradoksalne jest zatem wstrzykiwanie do niego komponentu o mniejszym zakresie, w tym przypadku o zakresie [Session]. W tym miejscu do akcji wkracza adnotacja [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] komponentu [SessionModel]. Za każdym razem, gdy kod kontrolera uzyskuje dostęp do pola [session] w wierszu 2, wykonywana jest metoda proxy w celu przywrócenia sesji żądania aktualnie przetwarzanego przez kontroler;
- wiersz 6: obiekt [HttpSession] nie jest już potrzebny w parametrach akcji;
- wiersz 7: pobieramy / zwiększamy licznik;
- wiersz 8: zwracana jest jego wartość;
Oto przykład wykonania:
Za pierwszym razem
![]() |
Za drugim razem
![]() |
Teraz weźmy inną przeglądarkę, która będzie reprezentować drugiego użytkownika. W tym przypadku wybieramy przeglądarkę Opera:
![]() |
Jak widać powyżej w [1], ten drugi użytkownik otrzymuje wartość licznika równą 1. Świadczy to o tym, że jego sesja różni się od sesji pierwszego użytkownika. Jeśli przyjrzymy się wymianie danych między klientem a serwerem (w przeglądarce Opera również klawisze Ctrl-Shift-I), widzimy w [2], że ten drugi użytkownik ma plik cookie sesji inny niż ten pierwszego użytkownika. To właśnie zapewnia niezależność sesji.
4.15. [/m17]: pobranie obiektu o zasięgu [application]
Wróćmy do architektury wykonywania akcji:
![]() |
Wiemy już, jak utworzyć sesję użytkownika. Teraz utworzymy obiekt zakresu [application], którego zawartość będzie tylko do odczytu i dostępna dla wszystkich użytkowników. Wprowadzamy klasę [ApplicationModel], która będzie obiektem zakresu [application]:
![]() |
package istia.st.springmvc.models;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class ApplicationModel {
// licznik
private AtomicLong compteur = new AtomicLong(0);
// metody pobierające i ustawiające
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- wiersz 5: adnotacja [@Component] sprawia, że klasa [ApplicationModel] będzie komponentem zarządzanym przez Spring. Domyślnym typem komponentów Springa jest typ [singleton]: komponent ten jest tworzony jako pojedynczy egzemplarz podczas instancjonowania kontenera Springa, czyli zazwyczaj podczas uruchamiania aplikacji. Możemy wykorzystać ten cykl życia do przechowywania w singletonie informacji konfiguracyjnych, które będą dostępne dla wszystkich użytkowników;
- wiersz 11: licznik typu [AtomicLong]. Ten typ posiada metodę [incrementAndGet], zwaną atomową. Oznacza to, że wątek wykonujący tę metodę ma gwarancję, iż żaden inny wątek nie odczyta wartości licznika (Get) w okresie między jego odczytem (Get) a zwiększeniem (increment) przez pierwszy wątek, co spowodowałoby błędy, ponieważ dwa wątki odczytywałyby tę samą wartość licznika, a ten zamiast zostać zwiększony o dwa, zostałby zwiększony o jeden;
Tworzymy następującą nową akcję [/m17]:
@Autowired
private ApplicationModel application;
// ----- zarządzanie obiektem o zasięgu aplikacji [Autowired] ------------------------
@RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m17() {
return String.valueOf(application.getCompteur().incrementAndGet());
}
- wiersze 1–2: wstawiamy komponent [ApplicationModel] do kontrolera. Jest to singleton. Oznacza to, że każdy użytkownik będzie miał odwołanie do tego samego obiektu;
- wiersz 7: zwracamy licznik zakresu [application] po jego zwiększeniu;
Oto dwa przykłady, jeden z przeglądarką Chrome, drugi z przeglądarką Opera:
![]() | ![]() |
Powyżej widać, że obie przeglądarki korzystały z tego samego licznika, co nie miało miejsca w przypadku sesji. Te dwie przeglądarki symbolizują dwóch różnych użytkowników, którzy mają dostęp do danych z zakresu [application]. Ogólnie rzecz biorąc, należy unikać umieszczania w obiektach zakresu [application] informacji przeznaczonych do odczytu i zapisu, tak jak miało to miejsce powyżej w przypadku licznika. W rzeczywistości wątki wykonawcze różnych użytkowników uzyskują jednocześnie dostęp do danych zakresu [application]. Jeśli występują dane do zapisu, należy zsynchronizować operacje zapisu, tak jak zrobiono to powyżej w przypadku typu [AtomicLong]. Równoległy dostęp jest źródłem błędów programistycznych. Dlatego zaleca się umieszczanie w obiektach o zakresie [application] wyłącznie danych tylko do odczytu.
4.16. [/m18]: pobieranie obiektu o zakresie [session] za pomocą [@SessionAttributes]
Istnieje inny sposób na pobranie informacji z zakresu [session]. Umieścimy w sesji następujący obiekt:
package istia.st.springmvc.models;
public class Container {
// licznik
public int compteur=10;
// metody pobierające i ustawiające
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
Wykorzystamy ten obiekt w dwóch poniższych akcjach:
// użycie [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// tutaj umieszczamy klucz [container] w sesji
session.setAttribute("container", new Container());
}
// użycie klucza [@ModelAttribute] ----------------------
// klucz sesji [container] zostanie tutaj wstawiony
@RequestMapping(value = "/m19", method = RequestMethod.GET)
public String m19(@ModelAttribute("container") Container container) {
container.setCompteur(1 + container.getCompteur());
return String.valueOf(container.getCompteur());
}
- wiersze 3–6: akcja [/m18] nie zwraca żadnego wyniku. Służy ona wyłącznie do utworzenia obiektu w sesji o kluczu [container];
- wiersz 11: w akcji [/m19] wykorzystuje się adnotację [@ModelAttribute]. Działanie tej adnotacji jest dość złożone. Parametr [container] tej adnotacji może odnosić się do różnych elementów, a w szczególności do obiektu sesji. Wymaga to jednak, aby obiekt ten został zadeklarowany za pomocą adnotacji [@SessionAttributes] na samej klasie:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- wiersz 2 powyżej wskazuje, że klucz [container] stanowi część atrybutów sesji;
Podsumowując:
- w [/m18] klucz [container] jest umieszczany w sesji;
- adnotacja [@SessionAttributes({"container"})] sprawia, że klucz ten może zostać wstawiony do parametru opatrzonego adnotacją [@ModelAttribute("container")];
- nie widać tego w poniższym przykładzie wykonania, ale informacja opatrzona adnotacją [@ModelAttribute] automatycznie staje się częścią modelu M przekazanego do widoku V;
Oto przykład wykonania. Najpierw umieszczamy klucz [container] w sesji za pomocą akcji [/m18] [1]. Następnie dwukrotnie wywołujemy akcję [/m19], aby zobaczyć, jak licznik zwiększa swoją wartość.
![]() |
4.17. [/m20-/m23]: wstawianie informacji za pomocą [@ModelAttribute]
Rozważmy następującą nową akcję:
// atrybut p będzie częścią wszystkich szablonów widoku [Model] ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------instancja @ModelAttribute --------------------------
// zostanie wstrzyknięty, jeśli znajduje się w sesji
// zostanie wstawiony, jeśli kontroler zdefiniował metodę dla tego atrybutu
// może pochodzić z pól obiektu URL, jeśli istnieje konwerter typu String na typ atrybutu
// w przeciwnym razie jest tworzony za pomocą domyślnego konstruktora
// następnie atrybuty modelu są inicjowane parametrami z GET lub POST
// wynik końcowy stanie się częścią modelu wygenerowanego przez akcję
// atrybut p jest wstawiany do argumentów------------------------
@RequestMapping(value = "/m20", method = RequestMethod.GET)
public Personne m20(@ModelAttribute("p") Personne personne) {
return personne;
}
- wiersze 2–5: definiują atrybut szablonu o nazwie [p]. Jest to szablon M widoku V, reprezentowany przez typ [Model] w Spring MVC. Model zachowuje się jak słownik par [clé, valeur]. W tym przypadku klucz [p] jest powiązany z obiektem [Personne] utworzonym przez metodę [getPersonne]. Nazwa metody może być dowolna;
- wiersz 17: atrybut wzorca klucza [p] jest wstrzykiwany do parametrów akcji. Wstrzyknięcie to odbywa się zgodnie z zasadami opisanymi w wierszach 8–12. W tym przypadku mamy do czynienia z sytuacją określoną w wierszu 9. Zatem w wierszu 17 parametr [Personne personne] będzie obiektem [Personne(7,'abcd',14)];
- w wierszu 18 zwracamy obiekt [personne] w celu weryfikacji. Zostanie on zserializowany do postaci jSON przed wysłaniem do klienta.
Oto przykład:
![]() |
Teraz przyjrzyjmy się następującej akcji:
// --------- atrybut p automatycznie staje się częścią szablonu M widoku V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Akcja, która ma wyświetlić widok V, musi zbudować jego model M. Spring MVC obsługuje ten model za pomocą typu [Model], który może zostać wstrzyknięty do parametrów akcji. Początkowo model ten jest pusty lub zawiera informacje oznaczone adnotacją [@ModelAttribute]. Akcja może wzbogacić ten model lub pozostawić go bez zmian przed przekazaniem go do widoku.
- wiersz 3: wstawienie modelu M;
- wiersz 4: chcemy sprawdzić, co się w nim znajduje. Serializujemy go do postaci ciągu znaków, aby wysłać go do klienta. W tym przypadku zostanie użyta metoda [Personne.toString]. Musi ona więc istnieć;
Oto przykładowe wykonanie:
![]() |
Powyżej widać, że instrukcje:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
utworzyły wpis [p, Personne(7,'abcd',14)] w modelu. Tak jest zawsze.
Rozważmy teraz następujący przypadek:
// w przeciwnym razie jest tworzony za pomocą domyślnego konstruktora
// następnie atrybuty szablonu są inicjowane za pomocą parametrów GET lub POST
z następującą akcją:
// --------- atrybut modelu [param1] jest częścią modelu, ale nie jest zainicjowany
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- wiersz 3: atrybut szablonu klucza [param1] nie istnieje. W takim przypadku powiązany typ musi posiadać konstruktor domyślny. Tak jest w przypadku typu [String], jednak nie można zapisać [@ModelAttribute("param1") Integer p1], ponieważ klasa [Integer] nie posiada konstruktora domyślnego;
- wiersz 4: zwracamy model, aby sprawdzić, czy atrybut klucza [param1] jest jego częścią;
Oto przykładowe wykonanie:
![]() |
Atrybut szablonu [param1] rzeczywiście występuje w szablonie, ale metoda [toString] powiązanej wartości nie dostarcza żadnych informacji na temat tej wartości.
Rozważmy teraz następującą akcję, w której jawnie umieszczamy informację w modelu:
// --------- atrybut szablonu [param2] jest jawnie umieszczony w szablonie
@RequestMapping(value = "/m23", method = RequestMethod.GET)
public String m23(String p2, Model model) {
model.addAttribute("param2",p2);
return model.toString();
}
- wiersz 4: wartość [p2] pobrana w wierszu 3 jest umieszczana w modelu w powiązaniu z kluczem [param2]:
Oto przykład wykonania:
![]() |
Zasady ulegają zmianie, jeśli parametrem akcji jest obiekt. Oto pierwszy przykład:
// ------ atrybut szablonu [unePersonne] jest automatycznie umieszczany w szablonie
@RequestMapping(value = "/m23b", method = RequestMethod.GET)
public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
return model.toString();
}
Akcja nie modyfikuje podanego jej szablonu. Wynik jest następujący:
![]() |
Widać, że adnotacja [@ModelAttribute("unePersonne") Personne p1] umieściła osobę [p1] w modelu, powiązaną z kluczem [unePersonne].
Rozważmy teraz następującą akcję:
// --------- osoba p1 jest automatycznie umieszczana w szablonie
// -------- z kluczem będącym nazwą jej klasy, której pierwsza litera jest mała
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- wiersz 4: nie dodano adnotacji [@ModelAttribute];
Wynik jest następujący:
![]() |
Zauważamy, że obecność parametru [Personne p1] spowodowała umieszczenie osoby [p1] w modelu, powiązaną z kluczem [personne], który jest nazwą klasy [Personne] z pierwszą literą pisana małą literą.
4.18. [/m24]: walidacja modelu akcji
Rozważmy następujący model akcji [ActionModel01]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// dane
@NotNull
private Integer a;
@NotNull
private Double b;
// metody pobierające i ustawiające
...
}
- wiersze 8 i 9: adnotacja [@NotNull] stanowi ograniczenie walidacyjne, które wskazuje, że adnotowane dane nie mogą przyjmować wartości null;
Przyjrzyjmy się teraz następującej akcji:
// ----------------------- walidacja modelu ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// czy są błędy?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// przeglądanie listy błędów
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 {
// brak błędów
Map<String, Object> mapData = new HashMap<String, Object>();
mapData.put("a", data.getA());
mapData.put("b", data.getB());
map.put("data", mapData);
}
return map;
}
- wiersz 3: zostanie utworzony obiekt [ActionModel01], a jego pola [a, b] zostaną zainicjowane parametrami o tych samych nazwach. Adnotacja [@Valid] wskazuje, że należy sprawdzić ograniczenia poprawności. Wyniki tej weryfikacji zostaną umieszczone w parametrze typu [BindingResult] (drugi parametr). Przeprowadzone zostaną następujące weryfikacje:
- z powodu adnotacji [@NotNull] muszą występować parametry [a] i [b];
- ze względu na typ [Integer a] parametr [a], który z natury jest typu [String], musi być konwertowalny na typ [Integer];
- ze względu na typ [Double b] parametr [b], który z natury jest typu [String], musi być konwertowalny na typ [Double];
Wraz z adnotacją [@Valid] błędy walidacji zostaną przeniesione do parametru [BindingResult result]. Bez adnotacji [@Valid] błędy walidacji powodują awarię akcji, a serwer wysyła do klienta odpowiedź HTTP ze statusem 500 (Internal server error).
- wiersz 3: wynik akcji ma typ [Map]. Do klienta zostanie wysłany ciąg znaków jSON z tego wyniku. Tworzy się dwa rodzaje słowników:
- w przypadku niepowodzenia – słownik z wpisem ['errors', value], gdzie [value] to ciąg znaków opisujący wszystkie błędy (wiersz 13);
- w przypadku powodzenia – słownik z jednym wpisem ['data',value], gdzie [value] jest sam w sobie słownikiem zawierającym dwa wpisy: ['a', value], ['b', value] (wiersz 19);
- wiersze 9–12: dla każdego wykrytego błędu [error] tworzy się ciąg [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
- pierwszym elementem jest błędne pole, np. [a] lub [b],
- drugim elementem jest odrzucona wartość, na przykład [x],
- trzecim elementem jest lista kodów błędów. Wkrótce omówimy ich znaczenie;
- czwartym elementem jest kod błędu. Należy on do poprzedniej listy;
- ostatnim elementem jest domyślny komunikat o błędzie. W rzeczywistości może występować kilka komunikatów o błędach;
Oto kilka przykładów wykonania:
![]() |
Powyżej widać, że:
- przypisanie wartości „x” do pola [ActionModel01.a] nie powiodło się, a komunikat o błędzie wyjaśnia, dlaczego;
- przypisanie wartości „y” do pola [ActionModel01.b] nie powiodło się, a komunikat o błędzie wyjaśnia, dlaczego;
Zwróćmy uwagę na kody błędów w polu [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Powrócimy do tych kodów błędów, gdy zajdzie potrzeba dostosowania komunikatu o błędzie. Należy zauważyć, że kod błędu to [typeMismatch].
Kolejny przykład:
![]() |
W tym przypadku nie przekazano parametrów [a] i [b]. Wówczas zadziałały walidatory [@NotNull] z modelu akcji [ActionModel01];
W końcu poprawne wartości:
![]() |
4.19. [m/24]: dostosowywanie komunikatów o błędach
Wróćmy do zrzutu ekranu z poprzedniego przykładu:
![]() |
Powyżej widzimy domyślne komunikaty o błędach. Oczywiste jest, że nie możemy ich pozostawić w rzeczywistej aplikacji. Istnieje możliwość zdefiniowania tych komunikatów o błędach. W tym celu skorzystamy z kodów błędów. Powyżej widzimy, że błąd dotyczący pola [a] ma następujące kody: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Kody błędów są uporządkowane od najbardziej szczegółowego do najmniej szczegółowego:
- [typeMismatch.actionModel01.a]: błąd typu w polu [a] typu [ActionModel01];
- [typeMismatch.a]: błąd typu w polu o nazwie [a];
- [typeMismatch.java.lang.Integer]: błąd typu w typie Integer;
- [typeMismatch]: błąd typu;
Zauważamy również, że kod błędu w polu [a], uzyskanym przez [error.getCode()], to [typeMismatch] (patrz zrzut ekranu powyżej).
Umieścimy komunikaty o błędach w pliku właściwości:
![]() |
Powyższy plik [messages.properties] będzie wyglądał następująco:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier
Każdy wiersz ma następującą postać:
W tym przypadku kluczem będzie kod błędu, a komunikatem – komunikat o błędzie powiązany z tym kodem.
Przypomnijmy kody błędów dla obu pól:
- [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], gdy parametr [a] jest nieprawidłowy;
- [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ], gdy parametr [b] jest nieprawidłowy;
- [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull], gdy brakuje parametru [a];
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull], gdy brakuje parametru [b];
Plik [messages.properties] musi zawierać komunikat o błędzie dla wszystkich możliwych przypadków błędów. W przypadku:
- brak parametrów [a] i [b], zostanie użyty kod [NotNull];
- w przypadku nieprawidłowego parametru [a] wprowadziliśmy komunikaty dla dwóch kodów [typeMismatch.actionModel01.a, typeMismatch]. Zobaczymy, który z nich zostanie użyty;
- w przypadku nieprawidłowego parametru [b] zostanie użyty kod [typeMismatch];
Aby plik [messages.properties] był używany, należy skonfigurować Spring:
![]() |
Usuwamy adnotacje konfiguracyjne z klasy [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);
}
}
- wiersz 8: uruchamia się aplikacja Spring Boot. Pierwszym parametrem metody statycznej [SpringApplication.run] jest klasa, która od tej pory konfiguruje aplikację;
Klasa [Config] ma następującą postać:
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;
}
}
- wiersze 11–13: znajdują się tu adnotacje konfiguracyjne, które wcześniej znajdowały się w klasie [Application];
- wiersz 14: aby skonfigurować aplikację Spring MVC, należy rozszerzyć klasę [WebMvcConfigurerAdapter];
- wiersz 15: adnotacja [@Bean] wprowadza komponent Springa, singleton;
- wiersz 16: definiujemy bean o nazwie [messageSource] (nazwa metody). Bean ten służy do definiowania plików komunikatów aplikacji i musi nosić właśnie tę nazwę;
- wiersze 17–19: informują Spring, że plik komunikatów:
- znajduje się w folderze [i18n] w ścieżce Classpath projektu (wiersz 18),
- nazywa się [messages.properties] (wiersz 18). W rzeczywistości termin [messages] stanowi podstawę nazw plików komunikatów, a nie samą nazwę. Zobaczymy, że w ramach internacjonalizacji może istnieć kilka plików komunikatów, po jednym dla każdej obsługiwanej kultury. Możemy więc mieć plik [messages_fr.properties] dla języka francuskiego i [messages_en.properties] dla języka angielskiego. Rozszerzenia dodawane do rdzenia [messages] są znormalizowane. Nie można wpisać byle czego;
W projekcie STS należy umieścić folder [i18n] w folderze zasobów, ponieważ jest on umieszczony w ścieżce Classpath projektu:
![]() |
Aby wykorzystać ten plik, tworzymy następującą nową akcję:
// walidacja modelu, obsługa komunikatów o błędach ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// słownik wyników
Map<String, Object> map = new HashMap<String, Object>();
// kontekst aplikacji Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// ustawienia regionalne
Locale locale = RequestContextUtils.getLocale(request);
// błędów?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// wyszukiwanie komunikatu o błędzie na podstawie kodów błędów
// komunikat jest wyszukiwany w plikach komunikatów
// kody błędów w formie tabeli
String[] codes = error.getCodes();
// w postaci ciągu znaków
String listCodes = String.join(" - ", codes);
// wyszukiwanie
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// czy znaleziono?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// znaleziono – dodajemy komunikat o błędzie do listy komunikatów o błędach
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;
}
Kod ten jest analogiczny do kodu akcji [/m24]. Wyjaśniamy różnice:
- wiersz 3: wstrzykujemy żądanie [HttpServletRequest request] do parametrów akcji. Będzie nam to potrzebne;
- wiersze 7–8: pobieramy kontekst Springa. Kontekst ten zawiera wszystkie fasole Springa w aplikacji. Umożliwia on również dostęp do plików komunikatów;
- wiersz 10: pobieramy ustawienia regionalne aplikacji. Termin ten zostanie wyjaśniony nieco dalej;
- wiersze 15–31: dla każdego błędu szukamy komunikatu odpowiadającego jednemu z tych kodów błędów. Są one wyszukiwane w kolejności zgodnej z kodami znalezionymi w pliku [error.getCodes()]. Gdy tylko zostanie znaleziony komunikat, zatrzymujemy się;
- wiersz 26: sposób na odzyskanie wiadomości w [messages.properties]:
- pierwszy parametr to kod wyszukiwany w [messages.properties],
- drugi to tablica parametrów, ponieważ czasami komunikaty zawierają parametry. W tym przypadku tak nie jest,
- trzeci to używana lokalizacja (uzyskana w wierszu 10). Lokalizacja określa używany język: [fr_FR] dla francuskiego (Francja), [en_US] dla angielskiego z USA. Komunikat jest wyszukiwany w pliku messages_[locale].properties, a więc na przykład w [messages_fr_FR.properties]. Jeśli ten plik nie istnieje, komunikat jest wyszukiwany w pliku [messages_fr.properties]. Jeśli ten plik nie istnieje, komunikat jest wyszukiwany w pliku [messages.properties]. To właśnie ten ostatni przypadek będzie dla nas odpowiedni;
- wiersze 25–29: co może być nieco zaskakujące, podczas wyszukiwania nieistniejącego kodu w pliku komunikatów pojawia się wyjątek zamiast wskaźnika null;
- wiersze 33–35: obsługujemy przypadek braku komunikatu o błędzie;
- wiersze 37–38: tworzymy ciąg błędu. Zawiera on ustawienia regionalne oraz znaleziony komunikat o błędzie;
Oto przykłady działania:
![]() |
Widać, że:
- ustawienie regionalne aplikacji to [fr_FR]. Jest to wartość domyślna, ponieważ nie podjęliśmy żadnych działań w celu jej zainicjowania;
- komunikat używany dla obu pól brzmi następująco:
NotNull=Le champ ne peut être vide
Kolejny przykład:
![]() |
Widać, że:
- komunikat o błędzie używany dla parametru [a] brzmi następująco:
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
- komunikat o błędzie używany dla parametru [b] brzmi następująco:
typeMismatch=Format invalide
Dlaczego pojawiają się dwa różne komunikaty? W przypadku parametru [a] możliwe były dwa komunikaty:
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Kody błędów zostały przeanalizowane zgodnie z kolejnością w tabeli [error.getCodes()]. Okazuje się, że kolejność ta przebiega od kodu najbardziej szczegółowego do kodu najbardziej ogólnego. Dlatego kod [typeMismatch.model01.a] został znaleziony jako pierwszy.
4.20. [/m25]: internacjonalizacja aplikacji Spring MVC
Skoro już wiemy, jak dostosować komunikaty o błędach do języka francuskiego, chcielibyśmy mieć je również w języku angielskim, co prowadzi nas do internacjonalizacji aplikacji Spring MVC. Aby to zrealizować, rozszerzymy klasę konfiguracyjną [Config], która przyjmie następujący kształt:
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;
}
}
- wiersze 28–32: tworzymy interceptor żądania. Interceptor żądania rozszerza interfejs [HandlerInterceptor]. Taka klasa sprawdza przychodzące żądanie, zanim zostanie ono przetworzone przez akcję. W tym przypadku interceptor [localeChangeInterceptor] wyszuka w przychodzącym żądaniu parametr o nazwie [lang], GET lub POST i zmieni ustawienia regionalne aplikacji zgodnie z tym parametrem. Jeśli więc parametrem jest [lang=en_US], ustawienia regionalne aplikacji zostaną zmienione na angielskie z USA;
- wiersze 34–37: redefiniujemy metodę [WebMvcConfigurerAdapter.addInterceptors], aby dodać poprzedni interceptor;
- wiersze 39–45: służą do skonfigurowania sposobu, w jaki ustawienie regionalne zostanie zapisane w pliku cookie. Wiadomo, że plik cookie może służyć jako pamięć użytkownika, ponieważ przeglądarka klienta systematycznie odsyła go do serwera. Poprzedni interceptor o nazwie [localeChangeInterceptor] tworzy plik cookie zawierający ustawienie regionalne. W wierszu 42 nadano temu plikowi cookie nazwę [lang]. Plik cookie służy również do zmiany ustawień regionalnych;
- wiersz 43: wskazuje, że w przypadku braku pliku cookie o nazwie [lang] ustawieniem regionalnym będzie [fr];
Podsumowując, ustawienie regionalne żądania można ustalić na dwa sposoby:
- przez przekazanie parametru o nazwie [lang];
- poprzez wysłanie pliku cookie o nazwie [lang]. Ten plik cookie jest tworzony automatycznie po zastosowaniu poprzedniej metody;
Aby wykorzystać tę lokalizację, utworzymy pliki komunikatów dla lokalizacji [fr] i [en]:
![]() |
Plik [messages_fr.properties] ma następującą treść:
NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Plik [messages_en.properties] ma następującą treść:
NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Plik [messages.properties] jest kopią pliku [messages_en.properties]. Przypominamy, że plik [messages.properties] jest używany, gdy nie znaleziono żadnego pliku odpowiadającego ustawieniom regionalnym zapytania. W naszym przypadku, jeśli użytkownik wyśle parametr [lang=en], a plik [messages_en.properties] nie istnieje, zostanie użyty plik [messages.properties]. Użytkownik otrzyma zatem komunikaty w języku angielskim.
Spróbujmy. Najpierw w środowisku deweloperskim przeglądarki Chrome (Ctrl-Shift-I) sprawdź pliki cookie:
![]() |
Jeśli masz plik cookie o nazwie [lang], usuń go. Następnie w przeglądarce Chrome wywołaj pliki URL i [http://localhost:8080/m25]:
![]() |
Przeglądarka wysłała następujące nagłówki: HTTP:
Widać, że w tych nagłówkach nie ma pliku cookie o nazwie [lang]. W tym przypadku nasz kod wykorzystuje ustawienie regionalne [fr]. Pokazuje to zrzut ekranu. Sprawdźmy inny przypadek:
![]() |
- w przypadku [1] przekazaliśmy parametr [lang=en], aby zmienić ustawienie regionalne na [en];
- w przypadku [2] widoczne jest nowe ustawienie regionalne;
- w [3] komunikat został wyświetlony w języku angielskim;
Przyjrzyjmy się teraz wymianie danych w pliku HTTP:
![]() |
Jak widać powyżej, serwer odesłał plik cookie o nazwie [lang]. Ma to istotną konsekwencję: ustawienie regionalne następnego żądania ponownie wyniesie [en] z powodu pliku cookie o nazwie [lang], który zostanie odesłany przez przeglądarkę. Powinniśmy więc zachować komunikaty w języku angielskim. Sprawdźmy to:
![]() |
Powyżej widać, że ustawienie regionalne pozostało na [en]. Ze względu na plik cookie, który przeglądarka wysyła automatycznie, pozostanie ono niezmienione, dopóki użytkownik nie zmieni go, wysyłając parametr [lang] w następujący sposób:
![]() |
4.21. [/m26]: wstawienie ustawienia regionalnego do szablonu akcji
W poprzednim przykładzie pokazaliśmy sposób na pobranie ustawień regionalnych z żądania:
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
...
// lokalne
Locale locale = RequestContextUtils.getLocale(request);
// czy są jakieś błędy?
Ustawienie regionalne można bezpośrednio wstawić do parametrów akcji. Oto przykład:
@RequestMapping(value = "/m26", method = RequestMethod.GET)
public String m26(Locale locale) {
return String.format("locale=%s", locale.toString());
}
![]() | ![]() |
![]() |
Jak widać powyżej, nie ma weryfikacji poprawności żądanej lokalizacji. Niemniej jednak następujące żądanie przeglądarki powoduje wyjątek po stronie serwera, ponieważ otrzymany plik cookie lokalizacji jest nieprawidłowy.
4.22. [/m27]: sprawdzanie poprawności modelu za pomocą Hibernate Validator
Rozważmy następującą nową akcję:
//walidacja modelu za pomocą 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>();
// czy są jakieś błędy?
if (result.hasErrors()) {
// przeglądanie listy błędów
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// brak błędów
map.put("data", data);
}
return map;
}
Mamy tu kod, który widzieliśmy już kilkakrotnie:
- wiersz 3: akcja [/m27] jest wywoływana za pośrednictwem POST;
- wiersze 8–11: każdy błąd będzie oznaczony jako [champ, message] z:
- pole: błędne pole,
- komunikat: powiązany komunikat o błędzie oraz lista kodów błędów;
- wiersz 14: jeśli nie ma błędów, zwracany jest ciąg jSON zawierający przesłane wartości;
W wierszu 3 stosuje się następujący szablon akcji [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;
// metody getter i setter
...
}
Klasa wykorzystuje ograniczenia walidacyjne pochodzące z dwóch pakietów:
- [javax.validation.constraints] w wierszach 5–13;
- [org.hibernate.validator.constraints] w wierszach 15–19;
Zależności Maven tych dwóch pakietów znajdują się w projekcie:
![]() |
W tym przypadku nie będziemy używać komunikatów zinternacjonalizowanych, lecz komunikaty zdefiniowane wewnątrz reguły za pomocą atrybutu [message]. Aby przetestować tę akcję, użyjemy [Advanced Rest Client]:
![]() |
- w [1-2] – zapytanie POST;
- w [3] nagłówek HTTP [Content-Type], który należy zastosować;
- w [4] link [Add new value] umożliwia dodanie pary [paramètre, value];
- w [5] należy umieścić pole z [ActionModel02], w tym przypadku pole [assertFalse]:
@NotNull(message = "La donnée est obligatoire")
@AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
- w polu [6] wprowadź błędną wartość, aby wyświetlić komunikat o błędzie. Powyżej ograniczenie [@AssertFalse] wymaga, aby pole [assertFalse] miało wartość [false];
![]() |
- w [7] – odpowiedź serwera: uruchomiono ograniczenie [@NotNull] dotyczące pustych pól i wyświetlono powiązany komunikat o błędzie;
- w [8] wyświetlono komunikat dotyczący pola [assertFalse], dla którego nie sprawdzono ograniczenia [@AssertFalse], wraz z kodami tego błędu. Przypominamy, że kody te mogą być powiązane z komunikatami w językach międzynarodowych;
Oto kolejny przykład:
![]() |

Zachęcamy czytelnika do przetestowania różnych przypadków błędów aż do kodu POST, w którym wszystkie dane są prawidłowe:
![]() | ![]() |
Uwaga: format daty jest zgodny z formatem anglosaskim: mm/dd/rrrr.
4.23. [/m28]: przeniesienie komunikatów o błędach do plików zewnętrznych
W klasie [ActionModel02] umieściliśmy komunikaty na stałe. Lepiej jest przenieść je do plików komunikatów. Kierujemy się przykładem akcji [/m25]. Tworzymy następujący nowy szablon akcji [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;
// metody pobierające i ustawiające
...
}
Komunikaty o błędach są przenoszone do plików [messages.properties]:
![]() |
Plik [messages_fr.properties] ma następującą treść:
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
Komunikaty o błędach zostały dodane w wierszach 4–16. Mają one następującą postać:
Kody nie mogą być dowolne. Są to kody wyświetlone w poprzedniej akcji [/m27]. Na przykład:
![]()
W plikach komunikatów w polu [int1014] należy użyć jednego z czterech powyższych kodów.
Plik [messages_en.properties] ma następującą postać:
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
Szablon akcji [ActionModel03] jest wykorzystywany przez następującą akcję:
// ----------------------- eksternalizacja komunikatów o błędach ------------------------
@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>();
// kontekst aplikacji Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// ustawienia regionalne
Locale locale = RequestContextUtils.getLocale(request);
// błędów?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// wyszukiwanie komunikatu o błędzie na podstawie kodów błędów
// komunikat jest wyszukiwany w plikach komunikatów
// kody błędów w formie tabeli
String[] codes = error.getCodes();
// w postaci ciągu znaków
String listCodes = String.join(" - ", codes);
// wyszukiwanie
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// czy znaleziono?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// znaleziono – dodajemy błąd do słownika
map.put(error.getField(), msg);
}
} else {
// brak błędów
map.put("data", data);
}
return map;
}
Ten typ kodu został już omówiony. Jedyną naprawdę istotną rzeczą jest wiersz 23: wyświetlany komunikat o błędzie zależy od ustawień regionalnych żądania.
Oto przykład w języku francuskim:
![]() | ![]() |
a teraz w języku angielskim:
![]() | ![]() |









































































