5. Widoki Thymeleaf
Wróćmy do architektury aplikacji Spring MVC.
![]() |
W dwóch poprzednich rozdziałach omówiono różne aspekty bloku [1], czyli akcji. Teraz zajmiemy się:
- bloku [2] widoków V;
- blok [3] modelu M wyświetlanego przez te widoki;
Od momentu powstania Spring MVC technologia generowania stron HTML wysyłanych do przeglądarek klienckich opierała się na stronach JSP (Java Server Pages). Od kilku lat można również korzystać z technologii [Thymeleaf] [http://www.thymeleaf.org/]. To właśnie tę technologię przedstawiamy teraz.
5.1. Projekt STS
Tworzymy nowy projekt:
![]() |
![]() |
- w projekcie [3] należy wskazać, że projekt wymaga zależności [Thymeleaf]. Spowoduje to dodanie, oprócz zależności [Spring MVC] z poprzedniego projektu, również zależności frameworka [Thymeleaf] i [5];
Teraz zmodyfikujmy ten projekt w następujący sposób:
![]() |
Czerpiemy inspirację z poprzedniego projektu:
- [istia.st.springmvc.controllers] będzie zawierał kontrolery;
- [istia.st.springmvc.models] będzie zawierał modele akcji i widoków;
- [istia.st.springmvc.main] to pakiet klasy wykonywalnej Spring Boot;
- [templates] będzie zawierał widoki Thymeleaf;
- [i18n] będzie zawierał zinternacjonalizowane komunikaty wyświetlane przez widoki;
Klasa [Application] ma następującą postać:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Config.class, args);
}
}
Klasa [Config] ma następującą postać:
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;
}
}
Ta konfiguracja umożliwia na razie obsługę ustawień regionalnych.
Kontroler [ViewController] ma następujący numer:
package istia.st.springmvc.actions;
import org.springframework.stereotype.Controller;
@Controller
public class ViewsController {
}
- w wierszu 5 adnotacja [@Controller] zastąpiła adnotację [@RestController], ponieważ od tej pory akcje nie będą generować odpowiedzi dla klienta. Będą one:
- utworzyć model M
- zwrócić typ [String], który będzie nazwą widoku [Thymeleaf] odpowiedzialnego za wyświetlenie tego modelu. To połączenie tego widoku V i tego modelu M wygeneruje strumień HTML wysyłany do klienta;
Plik [messages.properties] jest na razie pusty.
5.2. [/v01]: podstawy Thymeleaf
Rozważmy następującą pierwszą akcję w pliku [ViewsController]:
// Podstawy Thymeleaf – 1
@RequestMapping(value = "/v01", method = RequestMethod.GET)
public String v01() {
return "v01";
}
- wiersz 3: akcja zwraca typ [String]. Będzie to nazwa akcji;
- wiersz 4: ten widok będzie miał nazwę [v01]. Domyślnie powinien znajdować się w folderze [templates] i nosić nazwę [v01.html];
Widok [v01.html] wygląda następująco:
![]() |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Les vues'">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2 th:text="'Les vues dans Spring MVC'">Spring 4 MVC</h2>
</body>
</html>
Jest to plik o nazwie HTML. Obecność Thymeleaf widać:
- w przestrzeni nazw [th] w wierszu 2;
- w atrybutach [th:text] w wierszach 4 i 8;
Mamy tu poprawny plik HTML, który można wyświetlić. Umieszczamy go w folderze [static] [2] pod nazwą [vue-01.html] i wywołujemy go bezpośrednio w przeglądarce:
![]() |
Jeśli przyjrzymy się kodowi źródłowemu strony [2], zauważymy, że atrybuty [th:text] zostały przesłane przez serwer, ale zignorowane przez przeglądarkę. Gdy widok jest wynikiem akcji, uruchamia się Thymeleaf i interpretuje atrybuty [th] przed wysłaniem odpowiedzi do klienta.
Tag HTML:
<title th:text="'Les vues'">Spring 4 MVC</title>
jest przetwarzana przez Thymeleaf w następujący sposób:
- th:text ma składnię th:text="wyrażenie”, gdzie wyrażenie jest wyrażeniem do obliczenia. Gdy wyrażenie to jest ciągiem znaków, jak w tym przypadku, należy je ująć w apostrofy;
- wartość [expression] zastępuje tekst znacznika HTML, w tym przypadku tekst znacznika [title];
Po przetworzeniu powyższy tag przybrał następującą postać:
<title>Les vues</title>
Wywołajmy akcję [/v01]:
![]() |
- w [2] widać, jak Thymeleaf dokonał zamiany;
Teraz wywołajmy akcję URL z [http://localhost:8080/v01.html]:
![]() |
Jak należy to interpretować? Czy widok [templates/v01.html] został wygenerowany bezpośrednio, bez przechodzenia przez akcję? Aby wyjaśnić tę kwestię, tworzymy następującą akcję [/v02]:
// Podstawy Thymeleaf – 2
@RequestMapping(value = "/v02", method = RequestMethod.GET)
public String v02() {
System.out.println("action v02");
return "vue-02";
}
Widok [vue-02.html] jest kopią widoku [v01.html]:
![]() |
Teraz wywołajmy widok URL z widoku [http://localhost:8080/vue-02.html]:
![]() |
Nie znaleziono pliku URL. Teraz poprośmy o pliki URL i [http://localhost:8080/v02.html]
![]() |
- w logach konsoli przy [1] widać, że wywołano akcję [/v02], a ta spowodowała wyświetlenie widoku [vue-02.html] w [2];
Wiemy już, że nazwa URL [http://localhost:8080/v02.html] może również odnosić się do pliku [/v02.html] znajdującego się w folderze [static]. Co się stanie, jeśli ten plik istnieje? Spróbujmy. W folderze [static] tworzymy następujący plik [v02.html]:
![]() |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2>Spring 4 MVC</h2>
</body>
</html>
następnie żądamy plików URL i [http://localhost:8080/v02.html]:
![]() |
[1] oraz [2] wskazują, że wywołano akcję [/v02]. Należy zatem pamiętać, że gdy żądana akcja URL ma postać [/x.html], Spring / Thymeleaf:
- wykonuje akcję [/x], jeśli istnieje;
- wyświetla stronę [/static/x.html], jeśli istnieje;
- w przeciwnym razie zgłasza wyjątek 404 Not found;
Aby uniknąć nieporozumień, od tej pory akcje i widoki nie będą miały tych samych nazw.
5.3. [/v03]: internacjonalizacja widoków
Integracja Spring / Thymeleaf umożliwia Thymeleaf korzystanie z plików komunikatów Spring. Rozważmy następującą nową akcję [/v03]:
// internacjonalizacja widoków
@RequestMapping(value = "/v03", method = RequestMethod.GET)
public String v03() {
return "vue-03";
}
Powoduje ona wyświetlenie następującego widoku [vue-03.html]:
![]() |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2 th:text="#{title}">Spring 4 MVC</h2>
</body>
</html>
W wierszach 4 i 8 wyrażeniem atrybutu [th:text] jest #{title}, którego wartością jest komunikat o kluczu [title]. Tworzymy następujące pliki: [messages_fr.properties] i [messages_en.properties]:
[messages_fr.properties]
title=Les vues dans Spring MVC
[messages_en.properties]
title=Views in Spring MVC
Poprośmy o pliki URL, [http://localhost:8080/v03.html?lang=fr] i [http://localhost:8080/v03.html?lang=en]:
![]() | ![]() |
Zauważmy, że wykorzystaliśmy to, czego niedawno się nauczyliśmy. Zamiast oznaczyć akcję [v03] jako [/v03], oznaczyliśmy ją jako [/v03.html].
5.4. [/v04]: utworzenie szablonu M dla widoku V
Rozważmy następującą nową akcję [/v04]:
// tworzenie szablonu M dla widoku V
@RequestMapping(value = "/v04", method = RequestMethod.GET)
public String v04(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
System.out.println(String.format("Modèle=%s", model));
return "vue-04";
}
- wiersz 4: szablon widoku jest wstrzykiwany do parametrów akcji. Domyślnie ten początkowy szablon jest pusty. Zobaczymy, że można go wstępnie wypełnić;
- wiersz 4: szablon typu [Model] jest rodzajem słownika zawierającego elementy typu <String, Object>. W wierszu 4 dodajemy wpis do tego słownika z kluczem [personne] powiązanym z wartością typu [Personne];
- wiersz 5: wyświetlamy model na konsoli, aby zobaczyć, jak wygląda;
- w wierszu 6 wyświetlamy widok [vue-04.html];
Klasa [Personne] to ta, która została wykorzystana w poprzednim rozdziale:
![]() |
package istia.st.springmvc.models;
public class Personne {
// identyfikator
private Integer id;
// nazwa
private String nom;
// wiek
private int age;
// konstruktory
public Personne() {
}
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
public Personne(Integer id, String nom, int age) {
this(nom, age);
this.id = id;
}
@Override
public String toString() {
return String.format("[id=%s, nom=%s, age=%d]", id, nom, age);
}
// metody pobierające i ustawiające
...
}
Widok [vue-04.html] wygląda następująco:
![]() |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="${personne.nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="${personne.age}">56</span>
</p>
</body>
</html>
- w wierszu 10 wprowadzono nowy typ wyrażenia Thymeleaf ${var}, gdzie var jest kluczem modelu M widoku. Przypomnijmy, że akcja [/v04] umieściła w szablonie klucz [personne] powiązany z typem Personne[id, nom, age];
- wiersz 10: wyświetla imię osoby obecnej w modelu;
- wiersz 14: wyświetla jej wiek;
Pliki komunikatów zostały zmodyfikowane w celu dodania kluczy [personne.nom] i [personne.age] z wierszy 9 i 13. Wynik jest następujący:
![]() |
a typ szablonu M można znaleźć w logach konsoli [2].
Można się zastanawiać, dlaczego nie zapisuje się widoku [vue-04] w następujący sposób:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}"></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>
<span th:text="#{personne.nom}" /></span>
<span th:text="${personne.nom}"></span>
</p>
<p>
<span th:text="#{personne.age}"></span>
<span th:text="${personne.age}"></span>
</p>
</body>
</html>
Ten widok jest całkowicie poprawny i da ten sam wynik, co poprzednio. Jednym z celów Thymeleaf jest to, aby strona Thymeleaf mogła zostać wyświetlona, nawet jeśli nie przechodzi przez Thymeleaf. Stwórzmy więc dwie nowe strony statyczne:
![]() |
Widok [vue-04b.html] jest kopią widoku [vue-04.html]. To samo dotyczy widoku [vue-04a.html], z którego usunięto jednak teksty statyczne. Jeśli wyświetlimy obie strony, otrzymamy następujące wyniki:
![]() |
W przypadku widoku [1] struktura strony nie jest widoczna, podczas gdy w przypadku widoku [2] jest ona wyraźnie widoczna. Na tym polega zaleta umieszczania tekstów statycznych w widoku Thymeleaf, nawet jeśli podczas wykonywania zostaną one zastąpione innymi tekstami.
Przyjrzyjmy się teraz szczegółowi technicznemu. W widoku [vue-04.html] formatujemy kod za pomocą [ctrl-Maj-F]. Otrzymujemy następujący wynik:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>
<span th:text="#{personne.nom}">Nom :</span> <span
th:text="${personne.nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span> <span
th:text="${personne.age}">56</span>
</p>
</body>
</html>
Tagi są źle wyrównane, co utrudnia czytanie kodu. Jeśli zmienimy nazwę pliku z [vue-04.html] na [vue-04.xml] i ponownie sformatujemy kod, tagi znów będą wyrównane. Zatem rozszerzenie [xml] byłoby bardziej praktyczne. Praca z tym rozszerzeniem jest możliwa. W tym celu należy skonfigurować Thymeleaf. Aby nie cofać tego, co już zrobiliśmy, duplikujemy omawiany projekt [springmvc-vues], tworząc projekt [springmvc-vues-xml]
![]() |
Modyfikujemy plik [pom.xml] w następujący sposób:
<groupId>istia.st.springmvc</groupId>
<artifactId>springmvc-vues-xml</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springmvc-vues-xml</name>
<description>Les vues dans Spring MVC</description>
Nazwa projektu została zmieniona w wierszach 2 i 6. Ponadto zmieniamy rozszerzenie plików widoków znajdujących się w folderze [templates]:
![]() |
Dokument [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] zawiera listę właściwości konfiguracyjnych Spring Boot, które można wykorzystać w pliku [application.properties]:
![]() |
Niniejszy dokument przedstawia właściwości, z których korzysta Spring Boot podczas autokonfiguracji i które można zmodyfikować, wprowadzając odmienną konfigurację w pliku [application.properties]. W przypadku Thymeleaf właściwości autokonfiguracji są następujące:
# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;dodano charset=<kodowanie>
spring.thymeleaf.cache=true # ustawiono na false w celu umożliwienia odświeżania na gorąco
Wystarczyłoby więc po prostu dodać wiersz
spring.thymeleaf.suffix=.xml
w pliku [application.properties]. Pójdziemy jednak inną drogą, czyli konfiguracją programową. Skonfigurujemy Thymeleaf w klasie [Config]:
package istia.st.springmvc.main;
import java.util.Locale;
...
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
}
- wiersze 16–24 konfigurują obiekt [TemplateResolver] dla Thymeleaf. To właśnie ten obiekt jest ładowany na podstawie nazwy widoku przekazanej przez akcję w celu znalezienia odpowiedniego pliku;
- wiersze 18 i 19 określają prefiks i sufiks, które należy dodać do nazwy widoku w celu znalezienia pliku. Jeśli więc nazwa widoku to [vue04], poszukiwany plik będzie nosił nazwę [classpath:/templates/vue04.xml]. [classpath:/templates] to składnia Springa, która oznacza folder [/templates] umieszczony w katalogu głównym ścieżki Classpath projektu;
- wiersz 21: aby w odpowiedzi wysyłanej do klienta pojawił się nagłówek HTTP:
Content-Type:text/html;charset=UTF-8
- wiersz 20: wskazuje, że widok jest zgodny ze standardem HTML5;
- wiersz 22: wskazuje, że widoki Thymeleaf mogą być buforowane;
- wiersze 26–31: ustawia silnik rozpoznawania widoków pary Spring/Thymeleaf na poprzedni silnik rozpoznawania;
Uruchommy plik wykonywalny tego nowego projektu i wywołajmy URL [http://localhost:8080/v04.html?lang=en]:
![]() |
Zauważamy, że w URL akcja [/v04] mogła zostać ponownie zastąpiona przez [v04.html].
5.5. [/v05]: rozkład obiektu w widoku Thymeleaf
Tworzymy następującą akcję [/v05]:
// utworzenie szablonu M dla widoku V - 2
@RequestMapping(value = "/v05", method = RequestMethod.GET)
public String v05(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-05";
}
Jest ona identyczna z akcją [/v04]. Widok [vue-05.xml] wygląda następująco:
![]() |
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${personne}">
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
- wiersze 8–17: w tych wierszach obiekt Thymeleaf jest definiowany przez atrybut [th:object="${personne}"] (wiersz 8). Obiekt ten jest tutaj obiektem o kluczu [personne], który znajduje się w szablonie:
- wiersz 11: wyrażenie Thymeleaf [*{nom}] jest równoważne wyrażeniu [${objet.nom}], gdzie [objet] jest bieżącym obiektem Thymeleaf. Zatem w tym przypadku wyrażenie [*{nom}] jest równoważne [${personne.nom}];
- wiersz 15: to samo;
Wynik:
![]() |
5.6. [/v06]: testy w widoku Thymeleaf
Rozważmy następującą akcję [/v06]:
// utworzenie modelu M widoku V – 3
@RequestMapping(value = "/v06", method = RequestMethod.GET)
public String v06(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-06";
}
Jest ona identyczna z dwiema poprzednimi akcjami. Wyświetla następujący widok [vue-06.xml]:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${personne}">
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
<p th:if="*{age} >= 18" th:text="#{personne.majeure}">Vous êtes majeur</p>
<p th:if="*{age} < 18" th:text="#{personne.mineure}">Vous êtes mineur</p>
</div>
</body>
</html>
- wiersz 17: atrybut [th:if] ocenia wyrażenie boolowskie. Jeśli wyrażenie to jest prawdziwe, tag jest wyświetlany, w przeciwnym razie nie. Zatem w tym przypadku, jeśli ${personne.age}>=18, wyświetlony zostanie tekst [#{personne.majeure}], tj. komunikat o kluczu [personne.majeure] z plików komunikatów;
- wiersz 18: nie można wpisać [*{age} < 18], ponieważ znak < jest znakiem zastrzeżonym. Należy zatem użyć jego odpowiednika HTML [<], zwanego również encją HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];
Pliki komunikatów zostały zmodyfikowane:
[messages_fr.properties]
title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
[messages_en.properties]
title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.mineure=You are under 18
personne.majeure=You are over 18
Wynik jest następujący:
![]() | ![]() |
5.7. [/v07]: iteracja w widoku Thymeleaf
Rozważmy następującą akcję [/v07]:
// utworzenie modelu M widoku V – 4
@RequestMapping(value = "/v07", method = RequestMethod.GET)
public String v07(Model model) {
model.addAttribute("liste", new Personne[] { new Personne(7, "martin", 17), new Personne(8, "lucie", 32),
new Personne(9, "paul", 7) });
return "vue-07";
}
- akcja tworzy listę trzech osób, umieszcza ją w szablonie powiązanym z kluczem [liste] i wyświetla widok [vue-07];
Widok [vue-07.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h3 th:text="#{liste.personnes}">Liste de personnes</h3>
<ul>
<li th:each="element : ${liste}" th:text="'['+ ${element.id} + ', ' +${element.nom}+ ', ' + ${element.age} + ']'">[id,nom,age]</li>
</ul>
</body>
</html>
- wiersz 10: atrybut [th:each] powtarza tag, w którym się znajduje, w tym przypadku tag <li>. Posiada on tutaj dwa parametry [element : collection], gdzie [collection] jest kolekcją obiektów, w tym przypadku listą osób. Thymeleaf przejrzy tę kolekcję i wygeneruje tyle tagów <li>, ile elementów znajduje się w kolekcji. Dla każdego tagu <li> atrybut [element] będzie reprezentował element kolekcji powiązany z tym tagiem. Dla tego elementu zostanie oceniony atrybut [th:text]. Jego wyrażenie jest tutaj konkatenacją ciągów znaków, dającą wynik [id, nom, age];
- wiersz 8: dodajemy klucz [liste.personnes] do plików komunikatów;
Oto wynik:
![]() | ![]() |
5.8. [/v08-/v10]: @ModelAttribute
Wracamy do kwestii, którą omówiliśmy podczas analizy akcji, a mianowicie do roli adnotacji [@ModelAttribute]. Dodajemy następującą nową akcję:
// --------------- Powiązanie i ModelAttribute ----------------------------------
// jeśli parametr jest obiektem, jest on instancjonowany i ewentualnie modyfikowany przez parametry zapytania
// automatycznie stanie się częścią modelu widoku z kluczem [key]
// dla parametru @ModelAttribute("xx") klucz będzie równy xx
// dla parametru @ModelAttribute klucz będzie równy nazwie klasy parametru zaczynającej się od małej litery
// jeśli @ModelAttribute nie występuje, to wszystko przebiega tak, jakby występował bez klucza
// należy zauważyć, że to automatyczne uwzględnienie w modelu nie ma miejsca, jeśli parametr nie jest obiektem
@RequestMapping(value = "/v08", method = RequestMethod.GET)
public String v08(@ModelAttribute("someone") Personne p, Model model) {
System.out.println(String.format("Modèle=%s", model));
return "vue-08";
}
- wiersz 11: adnotacja [@ModelAttribute("someone")] automatycznie doda obiekt [Personne p] do modelu, powiązany z kluczem [someone];
- wiersz 12: w celu sprawdzenia modelu;
- wiersz 13: wyświetla widok [vue-08.xml];
Widok [vue-08.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${someone}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
- wiersz 8: obiekt Thymeleaf jest inicjowany przy użyciu obiektu o kluczu [someone];
Wynik jest następujący:
![]() |
a w konsoli pojawia się następujący wpis:
Modèle={someone=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Rozważmy teraz następującą akcję [/v09]:
@RequestMapping(value = "/v09", method = RequestMethod.GET)
public String v09(Personne p, Model model) {
System.out.println(String.format("Modèle=%s", model));
return "vue-09";
}
- wiersz 1: obecność parametru [Personne p] spowoduje automatyczne umieszczenie osoby [p] w szablonie. Ponieważ nie podano klucza, jako klucz wykorzystywana jest nazwa klasy z pierwszą literą pisana małą literą. Zatem [Personne p] jest równoważne [@ModelAttribute("personne") Personne p];
Widok [vue.09.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${personne}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
- wiersz 8: używany klucz szablonu to [personne];
Oto wynik:
![]() |
oraz wpis w konsoli serwera:
Modèle={personne=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Rozważmy teraz następującą nową akcję [/v10]:
@ModelAttribute("uneAutrePersonne")
private Personne getPersonne(){
return new Personne(24,"pauline",55);
}
@RequestMapping(value = "/v10", method = RequestMethod.GET)
public String v10(Model model) {
System.out.println(String.format("Modèle=%s", model));
return "vue-10";
}
- wiersze 1–4: definiują metodę, która w szablonie każdego żądania tworzy element klucza [uneAutrePersonne] powiązany z obiektem [new Personne(24,"pauline",55)];
- wiersze 6–10: akcja [/v10] nie wykonuje żadnych czynności poza przekazaniem otrzymanego modelu do widoku [vue-10.xml]. Należy zauważyć, że parametr [Model model] musi być obecny wyłącznie w instrukcji z wiersza 8. Bez niego jest zbędny;
Widok [vue-10.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${uneAutrePersonne}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
Wynik jest następujący:
![]() |
a wpis w dzienniku konsoli wygląda następująco:
5.9. [/v11]: @SessionAttributes
Wracamy do kwestii, którą poruszyliśmy podczas analizy akcji, a mianowicie do roli adnotacji [@SessionAttributes]. Dodajemy następującą nową akcję [/v11]:
@ModelAttribute("jean")
private Personne getJean(){
return new Personne(33,"jean",10);
}
@RequestMapping(value = "/v11", method = RequestMethod.GET)
public String v11(Model model, HttpSession session) {
System.out.println(String.format("Modèle=%s, Session[jean]=%s", model, session.getAttribute("jean")));
return "vue-11";
}
Mamy tu do czynienia z sytuacją analogiczną do tej, którą właśnie analizowaliśmy. Różnica polega na adnotacji [@SessionAttributes] umieszczonej na samej klasie:
@Controller
@SessionAttributes("jean")
public class ViewsController {
- wiersz 2: wskazano, że klucz [jean] z modelu musi zostać umieszczony w sesji;
Dlatego w wierszu 7 akcji wprowadzono sesję. W wierszu 8 wyświetlana jest wartość sesji powiązanej z kluczem [jean].
Widok [vue-11.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${jean}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
<hr />
<div th:object="${session.jean}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
Wyświetlane są dwie osoby:
- w wierszach 8–21: osoba o kluczu [jean] w modelu;
- wiersze 23–36: osoba o kluczu [jean] w sesji;
Wyniki są następujące:
![]() |
- w [1] – osoba o kluczu [jean] w modelu;
- na [2], osoba o kluczu [jean] w sesji;
Dziennik konsoli wygląda następująco:
Modèle={uneAutrePersonne=[id=24, nom=pauline, age=55], jean=[id=33, nom=jean, age=10]}, Session[jean]=null
Z powyższego wynika, że klucz [jean] nie znajduje się w sesji, do której trafia akcja. Można z tego wywnioskować, że klucz [jean] został umieszczony w sesji po wykonaniu akcji i przed wyświetleniem widoku.
Rozważmy teraz przypadek, w którym klucz jest odwołany zarówno przez [@ModelAttribute], jak i przez [@SessionAttributes]. Tworzymy dwie następujące akcje:
@RequestMapping(value = "/v12a", method = RequestMethod.GET)
@ResponseBody
public void v12a(HttpSession session) {
session.setAttribute("paul", new Personne(51, "paul", 33));
}
// w przypadku, gdy klucz [@ModelAttribute] jest również kluczem [@SessionAttributes]
// w takim przypadku odpowiedni parametr jest inicjowany wartością z sesji
@RequestMapping(value = "/v12b", method = RequestMethod.GET)
public String v12b(Model model, @ModelAttribute("paul") Personne p) {
System.out.println(String.format("Modèle=%s", model));
return "vue-12";
}
Akcja [/v12a] służy wyłącznie do umieszczenia elementu ['paul',new Personne(51, "paul", 33)] w sesji. Nie wykonuje żadnych innych czynności. Fakt, że jest ona oznaczona tagiem [@ResponseBody], wskazuje, że to właśnie ona generuje odpowiedź dla klienta. Ponieważ jej typ to [void], żadna odpowiedź nie jest generowana.
Akcja [/v12b] przyjmuje jako parametr [@ModelAttribute("paul") Personne p]. Jeśli nie podejmie się żadnych innych działań, obiekt [Personne] zostanie zainicjowany, a następnie zainicjowany z parametrami żądania, a obiekt ten nie ma nic wspólnego z obiektem klucza [paul] umieszczonym w sesji przez akcję [/v12a]. Dodamy klucz [paul] do atrybutów sesji tej klasy:
@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
- w wierszu 2 znajdują się teraz dwa atrybuty sesji;
Wróćmy do parametrów akcji [/v12b]:
public String v12b(Model model, @ModelAttribute("paul") Personne p) {
Obiekt [Personne p] nie zostanie teraz zainicjowany, ale będzie odwoływał się do obiektu klucza [paul] w sesji. Dalszy przebieg procedury pozostaje bez zmian. Obiekt o kluczu [paul] znajdzie się w szczególności w szablonie widoku, który zostanie wyświetlony. Właśnie to chcemy zobaczyć w wierszu 11 akcji [/v12b].
Widok [vue-12.xml] będzie wyglądał następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${paul}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
- wiersz 8: odwołujemy się do klucza [paul] z modelu widoku;
Daje to następujący wynik (po wykonaniu akcji [/v12a], która umieszcza klucz [paul] w sesji):
![]() |
Dziennik konsoli wygląda następująco:
Modèle={jean=[id=33, nom=jean, age=10], uneAutrePersonne=[id=24, nom=pauline, age=55], paul=[id=51, nom=paul, age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Klucz [paul] został prawidłowo umieszczony w szablonie, a jego wartością jest wartość powiązana z kluczem [paul] w sesji.
5.10. [/v13]: wygenerowanie formularza do wprowadzania danych
Przechodzimy teraz do wprowadzania danych w formularzach i ich walidacji. Tworzymy pierwszy formularz za pomocą następującej akcji [/v13]:
// generuje formularz do wprowadzenia danych osoby
@RequestMapping(value = "/v13", method = RequestMethod.GET)
public String v13() {
return "vue-13";
}
która po prostu wyświetla następujący widok [vue-13.xml]:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="/someURL" th:action="@{/v14.html}" method="post">
<h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
<div th:object="${personne}">
<table>
<thead></thead>
<tbody>
<tr>
<td th:text="#{personne.id}">Id :</td>
<td>
<input type="text" name="id" value="11" th:value="''" />
</td>
</tr>
<tr>
<td th:text="#{personne.nom}">Nom :</td>
<td>
<input type="text" name="nom" value="Tintin" th:value="''" />
</td>
</tr>
<tr>
<td th:text="#{personne.age}">Age :</td>
<td>
<input type="text" name="age" value="17" th:value="''" />
</td>
</tr>
</tbody>
</table>
</div>
<input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
</form>
</body>
</html>
Jeśli umieścimy ten widok w folderze [static] pod nazwą [vue-13.html] i wywołamy URL [http://localhost:8080/vue-13.html], otrzymamy następującą stronę:
![]() |
- W wierszu 8 formularza znajduje się tag <form> z atrybutem [th:action]. Atrybut ten zostanie przetworzony przez Thymeleaf, a jego wartość zastąpi aktualną wartość atrybutu [action], który pełni zatem jedynie funkcję dekoracyjną. W tym przypadku wartość atrybutu [th:action] będzie wynosić [/v14.html];
- w wierszach 17, 23 i 29 wartość atrybutu [th:value] zastąpi wartość atrybutu [value]. W tym przypadku wartością tą będzie pusty ciąg znaków;
Po wysłaniu zapytania o URL [/v13.html] otrzymujemy następujący wynik:
![]() |
Przyjrzyjmy się kodowi źródłowemu wygenerowanemu przez Thymeleaf:
<!DOCTYPE html>
<html>
<head>
<title>Views in Spring MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="/v14.html" method="post">
<h2>Please, enter information and validate</h2>
<div>
<table>
<thead></thead>
<tbody>
<tr>
<td>Identifier:</td>
<td>
<input type="text" name="id" value="" />
</td>
</tr>
<tr>
<td>Name:</td>
<td>
<input type="text" name="nom" value="" />
</td>
</tr>
<tr>
<td>Age:</td>
<td>
<input type="text" name="age" value="" />
</td>
</tr>
</tbody>
</table>
</div>
<input type="submit" value="Validate" />
</form>
</body>
</html>
W wierszach 9, 18, 24 i 30 widać, jak Thymeleaf ocenia atrybuty [th:action] i [th:value].
5.11. [/v14]: obsługa wartości przesłanych przez formularz
Akcja [/v14] jest akcją, która odbiera przesłane wartości. Wygląda ona następująco:
// przetwarza wartości z formularza
@RequestMapping(value = "/v14", method = RequestMethod.POST)
public String v14(Personne p) {
return "vue-14";
}
- wiersz 3: przesłane wartości są zawarte w obiekcie [Personne p]. Wiadomo, że obiekt ten automatycznie staje się częścią modelu M widoku V, który zostanie wyświetlony przez akcję, powiązanego z kluczem [personne];
- wiersz 4: wyświetlany widok to widok [vue-14.xml];
Widok [vue-14.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
<div th:object="${personne}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
- wiersz 9: z modelu pobierany jest obiekt powiązany z kluczem [personne];
- wiersze 12, 16 i 20: wyświetlane są cechy tego obiektu;
Daje to następujący wynik:
![]() | ![]() |
5.12. [/v15-/v16]: walidacja modelu
W oparciu o poprzedni przykład przyjrzyjmy się następującej sekwencji:
![]() |
- w [1] wprowadzono błędne wartości dla pól [id] i [age] typu [int];
- w polu [2] odpowiedź serwera wskazuje, że wystąpiły dwa błędy;
Będziemy korzystać z tego samego formularza, ale w przypadku błędów walidacji wyświetlimy stronę informującą o tych błędach, aby użytkownik mógł je poprawić.
Akcja [/v15] wygląda następująco:
// ---------------------- wyświetlenie formularza
@RequestMapping(value = "/v15", method = RequestMethod.GET)
public String v15(SecuredPerson p) {
return "vue-15";
}
Jako parametr otrzymuje typ [SecuredPerson] o następującej treści:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
public class SecuredPerson {
@Range(min = 1)
private int id;
@Length(min = 4, max = 10)
private String nom;
@Range(min = 8, max = 14)
private int age;
// konstruktory
public SecuredPerson() {
}
public SecuredPerson(int id, String nom, int age) {
this.id=id;
this.nom = nom;
this.age = age;
}
// metody pobierające i ustawiające
...
}
Pola [id, nom, age] zostały opatrzone adnotacjami dotyczącymi ograniczeń walidacji. Widok [vue-15.xml] wyświetlany przez akcję [/v15] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="/someURL" th:action="@{/v16.html}" method="post">
<h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
<div th:object="${securedPerson}">
<table>
<thead></thead>
<tbody>
<tr>
<td th:text="#{personne.id}">Id :</td>
<td>
<input type="text" name="id" value="11" th:value="*{id}" />
</td>
<td>
<span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" style="color: red">Identifiant erroné</span>
</td>
</tr>
<tr>
<td th:text="#{personne.nom}">Nom :</td>
<td>
<input type="text" name="nom" value="Tintin" th:value="*{nom}" />
</td>
<td>
<span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
</td>
</tr>
<tr>
<td th:text="#{personne.age}">Age :</td>
<td>
<input type="text" name="age" value="17" th:value="*{age}" />
</td>
<td>
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
</td>
</tr>
</tbody>
</table>
<input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
</ul>
</div>
</form>
</body>
</html>
- wiersze 10–47: pobierany jest obiekt modelu strony powiązany z kluczem [securedPerson]. Po zakończeniu operacji GET otrzymujemy obiekt wraz z jego wartością instancji [id=0, nom=null, age=0];
- wiersz 17: wartość pola [securedPerson.id];
- wiersz 20: wyrażenie [${#fields.hasErrors('id')}] pozwala ustalić, czy wystąpiły błędy walidacji w polu [securedPerson.id]. Jeśli tak, atrybut [th:errors="*{id}"] wyświetla powiązany komunikat o błędzie;
- ten scenariusz powtarza się w wierszu 29 dla pola [nom] oraz w wierszu 38 dla pola [age];
- wiersz 45: wyrażenie [${#fields.errors('*')}] oznacza zbiór wszystkich błędów dotyczących pól obiektu [securedPerson]. Zatem właśnie ten zbiór błędów zostanie wyświetlony w wierszach 44–46;
- wiersz 16: widać, że wartości z formularza zostaną przesłane do akcji [/v16]. Wygląda ona następująco:
// -------------------- walidacja modelu------------------
@RequestMapping(value = "/v16", method = RequestMethod.POST)
public String v16(@Valid SecuredPerson p, BindingResult result) {
// błędy?
if (result.hasErrors()) {
return "vue-15";
} else {
return "vue-16";
}
}
- w wierszu 3 adnotacja [@Valid SecuredPerson p] wymusza walidację przesłanych wartości;
- wiersz 5: sprawdza, czy szablon akcji jest błędny, czy nie;
- wiersz 6: jeśli jest błędny, zwracany jest formularz [vue-15.xml]. Ponieważ formularz ten wyświetla komunikaty o błędach, zobaczymy je;
- wiersz 8: jeśli szablon akcji został zatwierdzony, wyświetlany jest następujący widok [vue-16.xml]:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2 th:text="#{personne.formulaire.saisies}">Voici vos saisies</h2>
<div th:object="${securedPerson}">
<p>
<span th:text="#{personne.id}">Id :</span>
<span th:text="*{id}">14</span>
</p>
<p>
<span th:text="#{personne.nom}">Nom :</span>
<span th:text="*{nom}">Bill</span>
</p>
<p>
<span th:text="#{personne.age}">Age :</span>
<span th:text="*{age}">56</span>
</p>
</div>
</body>
</html>
Oto przykłady wykonania:
![]() | ![]() |
![]() | ![]() |
![]() |
![]() |
5.13. [/v17-/v18]: kontrola komunikatów o błędach
Przy pierwszym wywołaniu akcji [/v15] otrzymujemy następujący wynik:
![]() |
Być może wolelibyśmy otrzymać pusty formularz zamiast zer w polach [Identifiant, Age]. Aby to osiągnąć, modyfikujemy szablon akcji w następujący sposób:
package istia.st.springmvc.models;
import javax.validation.constraints.Digits;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
public class StringSecuredPerson {
@Range(min = 1)
@Digits(fraction = 0, integer = 4)
private String id;
@Length(min = 4, max = 10)
private String nom;
@Range(min = 8, max = 14)
@Digits(fraction = 0, integer = 2)
private String age;
// konstruktory
public StringSecuredPerson() {
}
public StringSecuredPerson(String id, String nom, String age) {
this.id = id;
this.nom = nom;
this.age = age;
}
// metody pobierające i ustawiające
...
}
- wiersze 12 i 19: pola [id] i [age] zostały zmienione na typ [String];
- wiersz 11: określa się, że pole [id] musi być liczbą składającą się z maksymalnie czterech cyfr, bez miejsc po przecinku;
- wiersz 18: to samo dotyczy pola [age], które musi być liczbą całkowitą składającą się z maksymalnie dwóch cyfr;
Akcja [/v17] przyjmuje następujący kształt:
// ---------------------- wyświetlanie formularza
@RequestMapping(value = "/v17", method = RequestMethod.GET)
public String v17(StringSecuredPerson p) {
return "vue-17";
}
Widok [vue-17.xml] wyświetlany przez akcję [/v17] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form action="/someURL" th:action="@{/v18.html}" method="post">
<h2 th:text="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
<div th:object="${stringSecuredPerson}">
<table>
<thead></thead>
<tbody>
<tr>
<td th:text="#{personne.id}">Id :</td>
<td>
<input type="text" name="id" value="11" th:value="*{id}" />
</td>
<td>
<span th:each="err,status : ${#fields.errors('id')}" th:if="${status.index}==0" th:text="${err}" style="color: red">
Identifiant erroné
</span>
</td>
</tr>
<tr>
<td th:text="#{personne.nom}">Nom :</td>
<td>
<input type="text" name="nom" value="Tintin" th:value="*{nom}" />
</td>
<td>
<span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
</td>
</tr>
<tr>
<td th:text="#{personne.age}">Age :</td>
<td>
<input type="text" name="age" value="17" th:value="*{age}" />
</td>
<td>
<span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Âge erroné</span>
</td>
</tr>
</tbody>
</table>
<input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
</ul>
</div>
</form>
</body>
</html>
Zmiany dotyczą następujących wierszy:
- wiersz 10: obecnie operujemy na obiekcie szablonu klucza [stringSecuredPerson];
- wiersz 20: przeglądana jest lista błędów pola [id]. W składni [th:each="err,status : ${#fields.errors('id')}"] listę tę przegląda zmienna [err]. Zmienna [status] dostarcza informacji o każdej iteracji. Jest to obiekt [index, count, size, current], w którym:
- index: to numer bieżącego elementu,
- current: wartość bieżącego elementu,
- count, size: rozmiar przeglądanej listy;
- wiersz 20: wyświetlany jest tylko pierwszy element listy [th:if="${status.index}==0"];
Akcja [/v18], która przetwarza obiekt POST z akcji [/v17], wygląda następująco:
// -------------------- walidacja modelu------------------
@RequestMapping(value = "/v18", method = RequestMethod.POST)
public String v18(@Valid StringSecuredPerson p, BindingResult result) {
// błędy?
if (result.hasErrors()) {
return "vue-17";
} else {
return "vue-18";
}
}
Pliki komunikatów zmieniają się w następujący sposób:
[messages_fr.properties]
title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
notNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
[messages_en.properties]
title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.id=Identifier:
personne.mineure=You are under 18
personne.majeure=You are over 18
liste.personnes=Persons' list
personne.formulaire.titre=Please, enter information and validate
personne.formulaire.valider=Validate
personne.formulaire.saisies=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=Identifier must be an integer >=1
Range.securedPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.securedPerson.nom=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer >=1
Range.stringSecuredPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.stringSecuredPerson.nom=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Should be an integer with at most four digits
Digits.stringSecuredPerson.age=Should be an integer with at most two digits
Przyjrzyjmy się kilku przykładom:
![]() |
![]() |
W przypadku [1] widać, że uruchomiono oba walidatory pola [age]:
@Range(min = 8, max = 14)
@Digits(fraction = 0, integer = 2)
private String age;
Czy istnieje określona kolejność komunikatów o błędach? W przypadku pola [age] wydaje się, że walidatory zostały uruchomione w kolejności [Digits, Range]. Jednak po wykonaniu kilku zapytań można zauważyć, że kolejność ta może ulec zmianie. Nie można więc polegać na kolejności walidatorów. W przypadku [2] wyświetlany jest tylko jeden z dwóch komunikatów dotyczących pola [id]. W przypadku [3] widoczne są wszystkie komunikaty o błędach.
5.14. [/v19-/v20]: wykorzystanie różnych walidatorów
Rozważmy następujący nowy szablon akcji:
![]() |
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.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;
public class Form19 {
@NotNull
@AssertFalse
private Boolean assertFalse;
@NotNull
@AssertTrue
private Boolean assertTrue;
@NotNull
@Future
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInFuture;
@NotNull
@Past
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;
@NotNull
@Max(value = 100)
private Integer intMax100;
@NotNull
@Min(value = 10)
private Integer intMin10;
@NotNull
@NotEmpty
private String strNotEmpty;
@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
@NotBlank
private String email;
@NotNull
@Length(max = 4, min = 4)
private String str4;
@Range(min = 10, max = 14)
@NotNull
private Integer int1014;
@URL
@NotBlank
private String url;
// metody pobierające i ustawiające
...
}
Zostanie on wyświetlony przez następującą akcję [/v19]:
// ------------------ wyświetlanie formularza
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- wiersz 3: akcja otrzymuje jako parametr obiekt [Form19 formulaire]. Jeśli akcja GET nie otrzyma żadnych parametrów, obiekt ten zostanie zainicjowany wartościami domyślnymi języka Java;
- wiersz 4: wyświetlany jest widok [vue-19.xml]. Wygląda on następująco:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Validations côté serveur</h3>
<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
<table>
<thead>
<tr>
<th class="col1">Contrainte</th>
<th class="col2">Saisie</th>
<th class="col3">Erreur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">@NotEmpty</td>
<td class="col2">
<input type="text" th:field="*{strNotEmpty}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@NotBlank</td>
<td class="col2">
<input type="text" th:field="*{strNotBlank}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('strNotBlank')}" th:errors="*{strNotBlank}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@assertFalse</td>
<td class="col2">
<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@assertTrue</td>
<td class="col2">
<select th:field="*{assertTrue}">
<option value="true">True</option>
<option value="false">False</option>
</select>
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Past</td>
<td class="col2">
<input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Future</td>
<td class="col2">
<input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Max</td>
<td class="col2">
<input type="text" th:field="*{intMax100}" th:value="*{intMax100}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Min</td>
<td class="col2">
<input type="text" th:field="*{intMin10}" th:value="*{intMin10}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Size</td>
<td class="col2">
<input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Pattern(hh:mm:ss)</td>
<td class="col2">
<input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Email</td>
<td class="col2">
<input type="text" th:field="*{email}" th:value="*{email}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Length</td>
<td class="col2">
<input type="text" th:field="*{str4}" th:value="*{str4}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('str4')}" th:errors="*{str4}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@Range</td>
<td class="col2">
<input type="text" th:field="*{int1014}" th:value="*{int1014}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
</td>
</tr>
<tr>
<td class="col1">@URL</td>
<td class="col2">
<input type="text" th:field="*{url}" th:value="*{url}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
Ten kod wyświetla następujący widok:
![]() |
Strona zawiera tabelę z trzema kolumnami:
- kolumna 1: walidator pola wprowadzania danych;
- kolumna 2: pole wprowadzania danych;
- kolumna 3: komunikaty o błędach dotyczące pola wprowadzania danych;
Przyjrzyjmy się na przykład kodowi widoku [/v19.html] dla modułu walidacyjnego [@Pattern]:
<tr>
<td class="col1">@Pattern(hh:mm:ss)</td>
<td class="col2">
<input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Donnée erronée</span>
</td>
</tr>
Znajdujemy tu kod, który właśnie analizowaliśmy w przypadku formularzy typu [Personne]:
- wiersz 2: pierwsza kolumna: nazwa testowanego walidatora;
- wiersz 4: atrybut Thymeleaf [th:field="*{hhmmss}] wygeneruje atrybuty HTML, [id="hhmmss"] oraz [name="hhmmss"]. Atrybut Thymeleaf [th:value="*{hhmmss}"] wygeneruje atrybut HTML [value="valeur de [form19.hhmmss]]";
- wiersz 7: jeśli wartość wprowadzona w polu [form19.hhmmss] jest nieprawidłowa, wówczas w wierszu 7 wyświetlane są komunikaty o błędach związane z tym polem;
Wprowadzone wartości są przetwarzane przez następującą akcję [/v20]:
// ----------------- walidacja modelu formularza
@RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String v20(@Valid Form19 formulaire, BindingResult result, RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
return "vue-19";
} else {
// przekierowanie do [vue-19]
redirectAttributes.addFlashAttribute("form19", formulaire);
return "redirect:/v19.html";
}
}
- wiersz 3: zaksięgowane wartości wypełnią pola obiektu [Form19 formulaire], jeśli są prawidłowe;
- wiersze 4–6: jeśli zaksięgowane wartości są nieprawidłowe, formularz [vue-19] zostanie ponownie wyświetlony wraz z komunikatami o błędach;
- wiersze 6–10: jeśli przesłane wartości są prawidłowe, obiekt [Form19 formulaire] utworzony na podstawie tych wartości jest udostępniany dla następnego żądania, w tym przypadku żądania przekierowania. Następnie obiekt ten jest usuwany;
- wiersz 9: klient jest przekierowywany do akcji [/v19.html]. Akcja ta ponownie wyświetli formularz [vue-19], zawierający kod taki jak:
<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
Atrybut [th:object="${form19}"] pobierze wówczas obiekt powiązany z atrybutem Flash [form19] i w ten sposób ponownie wyświetli formularz w stanie, w jakim został wypełniony.
Kod formularza wymaga jeszcze kilku wyjaśnień. Rozważmy następujący kod:
<tr>
<td class="col1">@assertFalse</td>
<td class="col2">
<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>
</td>
<td class="col3">
<span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
</td>
</tr>
Generuje to następujący kod HTML:
<tr>
<td class="col1">@assertFalse</td>
<td class="col2">
<input type="radio" value="true" id="assertFalse1" name="assertFalse" />
<label for="assertFalse1">True</label>
<input type="radio" value="false" id="assertFalse2" name="assertFalse" />
<label for="assertFalse2">False</label>
</td>
<td class="col3">
</td>
</tr>
W kodzie
<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>
atrybuty Thymeleaf w wierszach 1 i 3 [th:field="*{assertFalse}"] stanowią problem. Stwierdzono, że ten atrybut generuje atrybuty HTML, [id=assertFalse] oraz [name=assertFalse]. Trudność wynika z faktu, że ponieważ są one generowane w wierszach 1 i 3, mamy dwa identyczne atrybuty [name] oraz dwa identyczne atrybuty [id]. Chociaż jest to możliwe w przypadku atrybutu [name], nie jest to możliwe w przypadku atrybutu [id]. Jak widać w wygenerowanym kodzie HTML, Thymeleaf wygenerował dwa różne atrybuty [id]: [id=asserFalse1] i [id=assertFalse2]. To dobrze. Problem polega na tym, że nie znamy tych identyfikatorów, a mogą nam się one przydać. Tak jest w przypadku tagu [label] w wierszu 2. Atrybut [for] tagu HTML [label] musi odwoływać się do atrybutu [id], w tym przypadku ten wygenerowany dla tagu [input] w wierszu 1. Dokumentacja Thymeleaf wskazuje, że wyrażenie [${#ids.prev('assertFalse')}"] pozwala uzyskać ostatni atrybut [id] wygenerowany dla pola [assertFalse].
Rozważmy teraz kod listy rozwijanej w formularzu:
<select th:field="*{assertTrue}">
<option value="true">True</option>
<option value="false">False</option>
</select>
Ten kod generuje kod HTML listy rozwijanej:
Wartość zostanie przesłana pod nazwą [name="assertTrue"].
Widok [vue-19.xml] korzysta z arkusza stylów:
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
W wierszu 4 arkusze stylów, z których korzysta widok, należy umieścić w folderze [static] projektu:
![]() |
Jej zawartość jest następująca:
@CHARSET "UTF-8";
.col1 {
background: lightblue;
}
.col2 {
background: Cornsilk;
}
.col3 {
background: #e2d31d;
}
.error {
color: red;
}
Przyjrzyjmy się teraz datom:
@NotNull
@Future
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInFuture;
@NotNull
@Past
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;
Analiza ruchu sieciowego w narzędziu deweloperskim przeglądarki Chrome (Ctrl-Shift-I) pokazuje, że daty są wysyłane w formacie (rrrr-mm-dd):
![]() |
To właśnie dlatego daty zostały opatrzone adnotacjami w walidatorze:
@DateTimeFormat(pattern = "yyyy-MM-dd")
który określa oczekiwany format wartości wysyłanych dat.
Na koniec plik z komunikatami w języku francuskim [messages_fr.properties]:
title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
NotNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
Future.form19.dateInFuture=La date doit être postérieure à celle d''aujourd'hui
Past.form19.dateInPast=La date doit être antérieure à celle d''aujourd'hui
Size.form19.strBetween4and6=la chaîne doit avoir entre 4 et 6 caractères
Min.form19.intMin10=La valeur doit être supérieure ou égale à 10
Max.form19.intMax100=La valeur doit être inférieure ou égale à 100
Length.form19.str4=La chaîne doit avoir quatre caractères exactement
Email.form19.email=Adresse mail invalide
URL.form19.url=URL invalide
Range.form19.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form19.hhmmss=Tapez l''heure sous la forme hh:mm:ss
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide
Przyjrzyjmy się kilku przykładom wykonania:
![]() |
![]() |
![]() |
Powyżej, między [1] a [2], wydaje się, że nic się nie wydarzyło. Jeśli jednak przyjrzymy się komunikacji sieciowej (Ctrl-Shift-I), widać, że miały miejsce dwie wymiany danych z serwerem:
![]() |
- w przypadku [1] – początkowa wymiana POST do [/v20];
- do [2] – odpowiedź na tę akcję to przekierowanie;
- w [3], drugie żądanie, tym razem do [/v19];
Następnie wykonywana jest akcja [/v19]:
// ------------------ wyświetlenie formularza
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- wiersz 3, parametr [Form19 formulaire] jest inicjowany za pomocą atrybutu Flash klucza [form19], który został utworzony przez poprzednią akcję [/v19] i który był obiektem typu [Form19] o wartościach wartościami przesłanymi do akcji [/v19];
- wiersz 4: widok [vue-19.xml] zostanie wyświetlony z obiektem [Form19 formulaire] w swoim szablonie, zainicjowanym wartościami przesłanymi w akcji. Dlatego użytkownik widzi formularz dokładnie taki, jaki go przesłał;
Dlaczego następuje przekierowanie? Dlaczego po prostu nie wysłano danych do powyższej akcji [/v19]? Uzyskano by ten sam wynik. Z kilkoma różnicami:
- przeglądarka umieściłaby w polu adresu adres [http://localhost:8080/v20.html] zamiast [http://localhost:8080/v19.html], jak to miało miejsce w tym przypadku, ponieważ wyświetla ostatni wywołany adres URL;
- jeśli użytkownik odświeży stronę (F5), wynik będzie zupełnie inny:
- w przypadku przekierowania wyświetlany URL to [http://localhost:8080/v19.html] uzyskany na podstawie GET. Przeglądarka ponownie wykona to ostatnie polecenie i otrzyma wtedy zupełnie nowy formularz (atrybut Flash jest używany tylko raz),
- w przypadku braku przekierowania wyświetlany kod URL to [http://localhost:8080/v20.html] uzyskany na podstawie kodu POST. Przeglądarka ponownie wykona to ostatnie polecenie i w ten sposób utworzy ponownie POST z tymi samymi wartościami przesłanymi w danych POST, co poprzednio. W tym przypadku nie ma to większego znaczenia, ale często jest to niepożądane, dlatego zazwyczaj preferuje się przekierowanie;
5.15. [/v21-/v22]: obsługa przycisków opcji
Rozważmy następujący komponent Spring [Listes]:
![]() |
package istia.st.springmvc.models;
import org.springframework.stereotype.Component;
@Component
public class Listes {
private String[] deplacements = new String[] { "0", "1", "2", "3", "4" };
private String[] libellesDeplacements = new String[] { "vélo", "marche", "train", "avion", "autre" };
private String[] libellesBijoux = new String[] { "émeraude", "rubis", "diamant", "opaline" };
// metody pobierające i ustawiające
...
}
- wiersz 5: klasa [Listes] będzie komponentem Spring;
- wiersze 8–10: listy wykorzystywane do wypełniania przycisków opcji, pól wyboru i list rozwijanych;
W klasie konfiguracyjnej [Config] zapisano:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
- wiersz 2: pakiet [models], w którym znajduje się komponent [Listes], zostanie prawidłowo zindeksowany przez Spring;
Tworzymy następujące nowe akcje:
// ------------------ formularz z przyciskami opcji
@Autowired
private Listes listes;
@RequestMapping(value = "/v21", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v21(@ModelAttribute("form") Form21 formulaire, Model model) {
model.addAttribute("listes", listes);
return "vue-21";
}
@RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String v22(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("form", formulaire);
return "redirect:/v21.html";
}
- wiersze 2–3: komponent [Listes] jest wstrzykiwany do kontrolera;
- wiersz 6: obsługujemy formularz typu [Form21], który opiszemy poniżej. Należy zauważyć, że w modelu widoku określono jego klucz [form]. Przypominamy, że domyślnie byłby to [form21];
- wiersz 7: wstawiamy komponent [Listes] do modelu. Widok będzie go potrzebował;
- wiersz 8: wyświetlamy widok [vue-21.xml]. Widok ten wyświetli formularz [Form21], a przesłane wartości zostaną przekazane do akcji [/v22] z wierszy 12–15;
- wiersze 12–15: akcja [/v22] ogranicza się do przekierowania do akcji [/v21], umieszczając otrzymane wartości POST w atrybucie Flash o kluczu [form]. Ważne jest, aby ten klucz był taki sam jak ten użyty w wierszu 6;
Szablon [Form21] wygląda następująco:
![]() |
package istia.st.springmvc.models;
public class Form21 {
// wartości przesłane
private String marie = "non";
private String deplacement = "4";
private String[] couleurs;
private String strCouleurs;
private String[] bijoux;
private String strBijoux;
private int couleur2;
private int[] bijoux2;
private String strBijoux2;
// metody pobierające i ustawiające
...
}
Widok [vue-21.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Boutons radio</h3>
<form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Etes-vous marié(e)</td>
<td class="col2">
<input type="radio" th:field="*{marie}" value="oui" />
<label th:for="${#ids.prev('marie')}">Oui</label>
<input type="radio" th:field="*{marie}" value="non" />
<label th:for="${#ids.prev('marie')}">Non</label>
</td>
<td class="col3">
<span th:text="*{marie}"></span>
</td>
</tr>
<tr>
<td class="col1">Mode de déplacement</td>
<td class="col2">
<span th:each="mode, status : ${listes.deplacements}">
<input type="radio" th:field="*{deplacement}" th:value="${mode}" />
<label th:for="${#ids.prev('deplacement')}" th:text="${listes.libellesDeplacements[status.index]}">Autre</label>
</span>
</td>
<td class="col3">
<span th:text="*{deplacement}"></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
- wiersze 36–40: należy zwrócić uwagę na wykorzystanie komponentu [Listes] umieszczonego w szablonie w celu wygenerowania etykiet pól wyboru;
- kolumna 3 pozwala sprawdzić wartość wprowadzoną dla POST lub wartość początkową formularza podczas pierwotnego GET;
Ten kod wyświetla następującą stronę:
![]() |
odpowiadającą następującemu kodowi HTML:
<!DOCTYPE HTML>
<html>
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Boutons radio</h3>
<form action="/v22.html" method="post">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Etes-vous marié(e)</td>
<td class="col2">
<input type="radio" value="oui" id="marie1" name="marie" />
<label for="marie1">Oui</label>
<input type="radio" value="non" id="marie2" name="marie" checked="checked" />
<label for="marie2">Non</label>
</td>
<td class="col3">
<span>non</span>
</td>
</tr>
<tr>
<td class="col1">Mode de déplacement</td>
<td class="col2">
<span>
<input type="radio" value="0" id="deplacement1" name="deplacement" />
<label for="deplacement1">vélo</label>
</span>
<span>
<input type="radio" value="1" id="deplacement2" name="deplacement" />
<label for="deplacement2">marche</label>
</span>
<span>
<input type="radio" value="2" id="deplacement3" name="deplacement" />
<label for="deplacement3">train</label>
</span>
<span>
<input type="radio" value="3" id="deplacement4" name="deplacement" />
<label for="deplacement4">avion</label>
</span>
<span>
<input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
<label for="deplacement5">autre</label>
</span>
</td>
<td class="col3">
<span>4</span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
Widać, że wartości przesłane (atrybuty „name”) znajdują się w następujących polach szablonu [Form21]:
private String marie = "non";
private String deplacement = "4";
Zachęcamy czytelnika do przeprowadzenia testów. Należy zwrócić uwagę, że wysyłany jest atrybut [value] przycisków opcji.
![]() | ![]() |
5.16. [/v23-/v24]: zarządzanie polami wyboru
Dodajemy następującą nową akcję:
// ------------------ formularz z polami wyboru
@RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String av20(@ModelAttribute("form") Form21 formulaire, Model model) {
model.addAttribute("listes", listes);
return "vue-23";
}
- wiersz 3: nadal korzystamy z szablonu [Form21];
Widok [vue-23.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Cases à cocher</h3>
<form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Vos couleurs préférées</td>
<td class="col2">
<input type="checkbox" th:field="*{couleurs}" value="0" />
<label th:for="${#ids.prev('couleurs')}">rouge</label>
<input type="checkbox" th:field="*{couleurs}" value="1" />
<label th:for="${#ids.prev('couleurs')}">vert</label>
<input type="checkbox" th:field="*{couleurs}" value="2" />
<label th:for="${#ids.prev('couleurs')}">bleu</label>
</td>
<td class="col3">
<span th:text="*{strCouleurs}"></span>
</td>
</tr>
<tr>
<td class="col1">Pierres préférées</td>
<td class="col2">
<span th:each="label, status : ${listes.libellesBijoux}">
<input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
<label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
</span>
</td>
<td class="col3">
<span th:text="*{strBijoux}"></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
- wiersze 37–41: należy zwrócić uwagę na wykorzystanie komponentu [Listes] do generowania etykiet pól wyboru;
Kod ten wyświetla następującą stronę:
![]() |
pochodząca z następującego kodu HTML:
<!DOCTYPE HTML>
<html>
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Cases à cocher</h3>
<form action="/v24.html" method="post">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Vos couleurs préférées</td>
<td class="col2">
<input type="checkbox" value="0" id="couleurs1" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
<label for="couleurs1">rouge</label>
<input type="checkbox" value="1" id="couleurs2" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
<label for="couleurs2">vert</label>
<input type="checkbox" value="2" id="couleurs3" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
<label for="couleurs3">bleu</label>
</td>
<td class="col3">
<span></span>
</td>
</tr>
<tr>
<td class="col1">Pierres préférées</td>
<td class="col2">
<span>
<input type="checkbox" value="0" id="bijoux1" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
<label for="bijoux1">émeraude</label>
</span>
<span>
<input type="checkbox" value="1" id="bijoux2" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
<label for="bijoux2">rubis</label>
</span>
<span>
<input type="checkbox" value="2" id="bijoux3" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
<label for="bijoux3">diamant</label>
</span>
<span>
<input type="checkbox" value="3" id="bijoux4" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
<label for="bijoux4">opaline</label>
</span>
</td>
<td class="col3">
<span></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
Należy zauważyć, że wartości przesyłane (atrybuty name) są umieszczane w następujących polach komponentu [Form21]:
private String[] couleurs;
private String[] bijoux;
Są to tabele, ponieważ dla każdego pola istnieje kilka pól wyboru o nazwie odpowiadającej nazwie pola. Możliwe jest zatem, że kilka przesłanych wartości będzie miało tę samą nazwę (atrybut name formularza). Aby je pobrać, potrzebna jest więc tabela.
Wróćmy do kodu Thymeleaf w kolumnie 3 strony:
<td class="col3">
<span th:text="*{strCouleurs}"></span>
</td>
</tr>
<tr>
<td class="col1">Pierres préférées</td>
<td class="col2">
<span th:each="label, status : ${listes.libellesBijoux}">
<input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
<label th:for="${#ids.prev('bijoux')}" th:text="${label}">Autre</label>
</span>
</td>
<td class="col3">
<span th:text="*{strBijoux}"></span>
</td>
</tr>
Pola, do których odwołują się wiersze 2 i 14, są następujące:
private String strCouleurs;
private String strBijoux;
Są one obliczane przez akcję [/v24], która obsługuje akcję POST:
// mapper Jacksona / jSON
private ObjectMapper mapper = new ObjectMapper();
@RequestMapping(value = "/v24", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String av21(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
redirectAttributes.addFlashAttribute("form", formulaire);
formulaire.setStrCouleurs(mapper.writeValueAsString(formulaire.getCouleurs()));
formulaire.setStrBijoux(mapper.writeValueAsString(formulaire.getBijoux()));
return "redirect:/v23.html";
}
Należy pamiętać, że biblioteka jackson / jSON znajduje się wśród zależności projektu.
- wiersz 2: tworzymy typ [ObjectMapper], który umożliwia serializację i deserializację obiektów w formacie jSON,
- wiersz 7: serializujemy tablicę kolorów do formatu jSON. Wynik umieszczamy w polu [strCouleurs];
- wiersz 8: tablica biżuterii jest serializowana do formatu jSON. Wynik jest umieszczany w polu [strBijoux];
Oto przykładowe wykonanie:
![]() | ![]() |
Należy zwrócić uwagę, że wysyłany jest atrybut [value] pól wyboru.
5.17. [/25-/v26]: zarządzanie listami
Dodajemy następującą akcję [/v25]:
// ------------------ formularz z listami
@RequestMapping(value = "/v25", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v25(@ModelAttribute("form") Form21 formulaire, Model model) {
model.addAttribute("listes", listes);
return "vue-25";
}
Widok [vue-25.xml] wygląda następująco:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Listes</h3>
<form action="/someURL" th:action="@{/v26.html}" method="post"
th:object="${form}">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Votre couleur préférée</td>
<td class="col2">
<select th:field="*{couleur2}">
<option value="0">rouge</option>
<option value="1">bleu</option>
<option value="2">vert</option>
</select>
</td>
<td class="col3">
<span th:text="*{couleur2}"></span>
</td>
</tr>
<tr>
<td class="col1">Pierres préférées (choix multiple)</td>
<td class="col2">
<select th:field="*{bijoux2}" multiple="multiple" size="3">
<option th:each="label, status : ${listes.libellesBijoux}"
th:text="${label}" th:value="${status.index}">
</option>
</select>
</td>
<td class="col3">
<span th:text="*{strBijoux2}"></span>
</td>
</tr>
</tbody>
</table>
<input type="submit" value="Valider" />
</form>
</body>
</html>
- wiersze 38–42: generowanie listy wielokrotnego wyboru, w której etykiety pochodzą z komponentu [Listes], z którego już korzystaliśmy;
Wyświetlana strona wygląda następująco:
![]() |
wygenerowana przez następujący kod HTML:
<!DOCTYPE HTML>
<html>
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="/css/form19.css" />
</head>
<body>
<h3>Formulaire - Listes</h3>
<form action="/v26.html" method="post">
<table>
<thead>
<tr>
<th class="col1">Texte</th>
<th class="col2">Saisie</th>
<th class="col3">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col1">Votre couleur préférée</td>
<td class="col2">
<select id="couleur2" name="couleur2">
<option value="0" selected="selected">rouge</option>
<option value="1">bleu</option>
<option value="2">vert</option>
</select>
</td>
<td class="col3">
<span>0</span>
</td>
</tr>
<tr>
<td class="col1">Pierres préférées (choix multiple)</td>
<td class="col2">
<select multiple="multiple" size="3" id="bijoux2" name="bijoux2">
<option value="0">émeraude</option>
<option value="1">rubis</option>
<option value="2">diamant</option>
<option value="3">opaline</option>
</select>
<input type="hidden" name="_bijoux2" value="1" />
</td>
<td class="col3">
<span></span>
</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Valider" />
</p>
</form>
</body>
</html>
- wiersz 44: można zauważyć, że Thymeleaf utworzył ukryte pole. Nie zrozumiałem jego roli:
- wartości przesłane (atrybuty value tagów option) zostaną umieszczone w następujących polach (atrybuty name) elementu [Form21]:
private int couleur2;
private int[] bijoux2;
- wiersz 38: lista [bijoux2] jest listą wielokrotnego wyboru. W związku z tym z nazwą [bijoux2] może być powiązanych kilka wartości. Aby je pobrać, pole [bijoux2] musi być tablicą. Należy zauważyć, że jest to tablica liczb całkowitych. Jest to możliwe, ponieważ zapisane wartości można przekonwertować na ten typ;
Wartości są zapisywane w następującej akcji [/v26]:
@RequestMapping(value = "/v26", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String v26(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) throws JsonProcessingException {
redirectAttributes.addFlashAttribute("form", formulaire);
formulaire.setStrBijoux2(mapper.writeValueAsString(formulaire.getBijoux2()));
return "redirect:/v25.html";
}
Nie ma tu nic, czego byśmy już nie widzieli. Oto przykład wykonania:
![]() | ![]() |
5.18. [/v27]: konfiguracja komunikatów
Rozważmy następującą akcję [/v27]:
// ------------------ komunikaty z parametrami
@RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v27(Model model) {
model.addAttribute("param1","paramètre un");
model.addAttribute("param2","paramètre deux");
model.addAttribute("param3","paramètre trois");
model.addAttribute("param4","messages.param4");
return "vue-27";
}
Akcja ta polega jedynie na umieszczeniu czterech wartości w szablonie i wyświetleniu następującego widoku [vue-27.xml]:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{messages.titre}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2 th:text="#{messages.titre}">Spring 4 MVC</h2>
<p th:text="#{messages.msg1(${param1})}"></p>
<p th:text="#{messages.msg2(${param2},${param3})}"></p>
<p th:text="#{messages.msg3(#{${param4}})}"></p>
</body>
</html>
- wiersz 8: komunikat bez parametrów;
- wiersz 9: komunikat z jednym parametrem [$param1] pobranym z szablonu;
- wiersz 10: komunikat z dwoma parametrami [$param2, $param3] pobranymi z szablonu;
- wiersz 11: komunikat z jednym parametrem. Parametr ten sam w sobie jest kluczem komunikatu (obecność znaku #). Klucz ten jest dostarczany przez [$param4];
Plik z komunikatami w języku francuskim wygląda następująco:
[messages_fr.properties]
messages.titre=Messages paramétrés
messages.msg1=Un message avec un paramètre : {0}
messages.msg2=Un message avec deux paramètres : {0}, {1}
messages.msg3=Un message avec une clé de message comme paramètre : {0}
messages.param4=paramètre quatre
Aby wskazać obecność parametrów w komunikacie, stosuje się symbole {0}, {1}, ...
Połączenie szablonu utworzonego przez akcję [/v27] z widokiem [vue-27] da w wyniku następujący kod HTML:
<!DOCTYPE html>
<html>
<head>
<title>Messages paramétrés</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2>Messages paramétrés</h2>
<p>Un message avec un paramètre : paramètre un</p>
<p>Un message avec deux paramètre : paramètre deux, paramètre trois</p>
<p>Un message avec une clé de message comme paramètre : paramètre quatre</p>
</body>
</html>
co daje następujący widok:
![]() |
Plik z komunikatami w języku angielskim wygląda następująco:
[messages_fr.properties]
messages.titre=Parameterized messages
messages.msg1=Message with one parameter: {0}
messages.msg2=Message with two parameters: {0}, {1}
messages.msg3=Message with a message key as a parameter: {0}
messages.param4=parameter four
Połączenie szablonu utworzonego przez akcję [/v27] z widokiem [vue-27] da następujący kod HTML:
<!DOCTYPE html>
<html>
<head>
<title>Parameterized messages</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h2>Parameterized messages</h2>
<p>Message with one parameter: paramètre un</p>
<p>Message with two parameters: paramètre deux, paramètre trois</p>
<p>Message with a message key as a parameter: parameter four</p>
</body>
</html>
co daje następujący widok:
![]() |
Widać, że ostatni komunikat został w pełni zinternacjonalizowany, co nie miało miejsca w przypadku dwóch poprzednich.
5.19. Korzystanie ze strony szablonowej
W aplikacji internetowej często zdarza się, że widoki mają wspólne elementy, które można wyodrębnić do strony szablonowej. Oto przykład:
![]() |
Powyżej mamy dwie podobne strony, na których fragment [1] został zastąpiony fragmentem [2]. Widok przedstawia stronę szablonową zawierającą trzy stałe fragmenty [3-5] oraz jeden fragment zmienny [6].
5.19.1. Projekt
Tworzymy projekt [springmvc-masterpage], postępując zgodnie z procedurą opisaną w paragrafie 5.1.
![]() |
Plik [pom.xml] ma następującą treść:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.springmvc</groupId>
<artifactId>springmvc-masterpage</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springmvc-masterpage</name>
<description>Page maître</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.9.RELEASE</version>
<relativePath/> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.springmvc.main.Main</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Jedna z zależności wynikających z tego pliku jest niezbędna dla strony głównej:
![]() |
Pakiety [config] i [main] są identyczne z pakietami o tych samych nazwach z poprzedniego projektu.
5.19.2. Strona główna
![]() |
Szablon to następujący widok [layout.xml]:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title>Layout</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<table style="width: 400px">
<tr>
<td colspan="2" bgcolor="#ccccff">
<div th:include="entete" />
</td>
</tr>
<tr style="height: 200px">
<td bgcolor="#ffcccc">
<div th:include="menu" />
</td>
<td>
<section layout:fragment="contenu">
<h2>Contenu</h2>
</section>
</td>
</tr>
<tr bgcolor="#ffcc66">
<td colspan="2">
<div th:include="basdepage" />
</td>
</tr>
</table>
</body>
</html>
- wiersz 2: strona szablonowa musi zdefiniować przestrzeń nazw [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], której element jest używany w wierszu 19;
- wiersze 10–12: generują poniższy obszar [1]. Tag Thymeleaf [th:include] pozwala na włączenie do bieżącego widoku fragmentu zdefiniowanego w innym pliku. Umożliwia to wyodrębnienie fragmentów wykorzystywanych w wielu widokach;
- wiersze 15–17: generują poniższy obszar [2];
- wiersze 19–20: generują obszar [3] poniżej. Atrybut [layout:fragment] jest atrybutem przestrzeni nazw [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Wskazuje on obszar, który podczas wykonywania może zostać zastąpiony innym;
- wiersze 24–28: generują poniższe pole [4];
![]() |
5.19.3. Fragmenty
Fragmenty [entete.xml], [menu.xml] i [basdepage.xml] są następujące:
[entete.xml]
<!DOCTYPE html>
<html>
<h2>entête</h2>
</html>
[menu.xml]
<!DOCTYPE html>
<html>
<h2>menu</h2>
</html>
[basdepage.xml]
<!DOCTYPE html>
<html>
<h2>bas de page</h2>
</html>
Fragment [page1.xml] wygląda następująco:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
<section layout:fragment="contenu">
<h2>Page 1</h2>
<form action="/someURL" th:action="@{/page2.html}" method="post">
<input type="submit" value="Page 2" />
</form>
</section>
</html>
- wiersz 2: atrybut [layout:decorator="layout"] wskazuje, że bieżąca strona [page1.xml] jest „ozdobiona”, tzn. należy do strony wzorcowej. Jest to wartość atrybutu, w tym przypadku widok [layout.xml];
- wiersz 3: określa się, w którym fragmencie strony głównej zostanie wstawiona strona [page1.xml]. Atrybut [layout:fragment="contenu"] wskazuje, że [page1.xml] zostanie wstawiony do fragmentu o nazwie [contenu], tj. do obszaru [3] strony wzorcowej;
- wiersze 5–7: zawartością fragmentu jest formularz zawierający przycisk POST prowadzący do akcji [/page2.html];
Fragment [page2.xml] jest analogiczny:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layout">
<section layout:fragment="contenu">
<h2>Page 2</h2>
<form action="/someURL" th:action="@{/page1.html}" method="post">
<input type="submit" value="Page 1" />
</form>
</section>
</html>
5.19.4. Akcje
![]() |
Kontroler [Layout.java] wygląda następująco:
package istia.st.springmvc.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class Layout {
@RequestMapping(value = "/page1")
public String page1() {
return "page1";
}
@RequestMapping(value = "/page2", method=RequestMethod.POST)
public String page2() {
return "page2";
}
}
- wiersze 10–12: akcja [/page1] powoduje jedynie wyświetlenie widoku [page1.xml];
- wiersze 15–17: to samo dotyczy akcji [/page2], która powoduje wyświetlenie widoku [page2.xml];













































































