5. Visualizações Thymeleaf
Voltemos à arquitetura de uma aplicação Spring MVC.
![]() |
Os dois capítulos anteriores descreveram vários aspetos do bloco [1], as ações. Vamos agora discutir:
- o bloco [2] das vistas V;
- o modelo [3] (M) apresentado por estas vistas;
Desde a criação do Spring MVC, a tecnologia utilizada para gerar páginas HTML enviadas aos navegadores dos clientes tem sido o JSP (Java Server Pages). Nos últimos anos, a tecnologia [Thymeleaf] [http://www.thymeleaf.org/] também se tornou disponível. Vamos agora apresentar esta tecnologia.
5.1. O projeto STS
Criamos um novo projeto:
![]() |
![]() |
- em [3], especifique que o projeto requer as dependências [Thymeleaf]. Isto irá adicionar as dependências do framework [Thymeleaf] [5] às dependências [Spring MVC] do projeto anterior;
Agora, vamos desenvolver este projeto da seguinte forma:
![]() |
Vamos inspirar-nos no projeto anterior:
- [istia.st.springmvc.controllers] conterá os controladores;
- [istia.st.springmvc.models] conterá os modelos de ação e de visualização;
- [istia.st.springmvc.main] é o pacote para a classe executável do Spring Boot;
- [templates] conterá as vistas Thymeleaf;
- [i18n] conterá as mensagens internacionalizadas exibidas pelas vistas;
A classe [Application] é a seguinte:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Config.class, args);
}
}
A classe [Config] é a seguinte:
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;
}
}
Esta configuração ativa atualmente a gestão de localizações.
O [ViewController] é o seguinte:
package istia.st.springmvc.actions;
import org.springframework.stereotype.Controller;
@Controller
public class ViewsController {
}
- Linha 5: A anotação [@Controller] substituiu a anotação [@RestController] porque, a partir de agora, as ações não irão gerar a resposta para o cliente. Em vez disso, irão:
- construir um modelo M
- retornar um tipo [String] que será o nome da vista [Thymeleaf] responsável por exibir este modelo. É a combinação desta vista V e deste modelo M que irá gerar o fluxo HTML enviado ao cliente;
O ficheiro [messages.properties] está atualmente vazio.
5.2. [/v01]: Noções básicas do Thymeleaf
Vamos analisar a próxima ação no [ViewsController]:
// thymeleaf basics - 1
@RequestMapping(value = "/v01", method = RequestMethod.GET)
public String v01() {
return "v01";
}
- Linha 3: A ação retorna um tipo [String]. Este será o nome da ação;
- linha 4: esta vista será [v01]. Por predefinição, deve estar localizada na pasta [templates] e ter o nome [v01.html];
A vista [v01.html] é a seguinte:
![]() |
<!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>
Este é um ficheiro HTML. A presença do Thymeleaf é evidente:
- no namespace [th] na linha 2;
- nos atributos [th:text] nas linhas 4 e 8;
Este é um ficheiro HTML válido que pode ser visualizado. Colocamo-lo na pasta [static] [2] com o nome [vue-01.html] e acedemos a ele diretamente através de um navegador:
![]() |
Se examinarmos o código-fonte da página em [2], podemos ver que os atributos [th:text] foram enviados pelo servidor e ignorados pelo navegador. Quando uma visualização é o resultado de uma ação, o Thymeleaf entra em ação e interpreta os atributos [th] antes de enviar a resposta ao cliente.
A tag HTML:
<title th:text="'Les vues'">Spring 4 MVC</title>
é processada da seguinte forma pelo Thymeleaf:
- th:text tem a sintaxe th:text="expressão", em que expressão é uma expressão a ser avaliada. Quando esta expressão é uma cadeia de caracteres, como neste caso, deve ser colocada entre aspas simples;
- o valor de [expressão] substitui o texto da tag HTML, neste caso o texto da tag [title];
Após o processamento, a tag acima torna-se:
<title>Les vues</title>
Vamos chamar a ação de [/v01]:
![]() |
- em [2], vemos o trabalho de substituição realizado pelo Thymeleaf;
Agora, vamos solicitar a URL [http://localhost:8080/v01.html]:
![]() |
Como devemos interpretar isto? A vista [templates/v01.html] foi servida diretamente sem passar por uma ação? Para esclarecer as coisas, criamos a seguinte ação [/v02]:
// thymeleaf basics - 2
@RequestMapping(value = "/v02", method = RequestMethod.GET)
public String v02() {
System.out.println("action v02");
return "vue-02";
}
A visualização [vue-02.html] é uma cópia de [v01.html]:
![]() |
Agora vamos solicitar a URL [http://localhost:8080/vue-02.html]:
![]() |
A URL não foi encontrada. Agora vamos solicitar a URL [http://localhost:8080/v02.html]
![]() |
- Nos registos da consola em [1], vemos que a ação [/v02] foi chamada, o que fez com que a vista [vue-02.html] fosse apresentada em [2];
Agora sabemos que a URL [http://localhost:8080/v02.html] também pode referir-se a um ficheiro [/v02.html] na pasta [static]. O que acontece se este ficheiro existir? Vamos experimentar. Criamos o seguinte ficheiro [v02.html] na pasta [static]:
![]() |
<!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>
depois solicitamos a URL [http://localhost:8080/v02.html]:
![]() |
[1] e [2] mostram que a ação [/v02] foi chamada. Podemos, portanto, concluir que quando a URL solicitada está no formato [/x.html], o Spring / Thymeleaf:
- executa a ação [/x] se esta existir;
- apresenta a página [/static/x.html] se esta existir;
- lança uma exceção 404 Not Found caso contrário;
Para evitar confusão, a partir de agora, as ações e as visualizações não terão os mesmos nomes.
5.3. [/v03]: Internacionalização de Visualizações
A integração entre o Spring e o Thymeleaf permite que o Thymeleaf utilize ficheiros de mensagens do Spring. Considere a seguinte nova ação [/v03]:
// internationalization of views
@RequestMapping(value = "/v03", method = RequestMethod.GET)
public String v03() {
return "vue-03";
}
Exibe a seguinte visualização [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>
Nas linhas 4 e 8, a expressão para o atributo [th:text] é #{title}, cujo valor é a mensagem da chave [title]. Criamos os seguintes ficheiros [messages_fr.properties] e [messages_en.properties]:
[messages_fr.properties]
title=Les vues dans Spring MVC
[messages_en.properties]
title=Views in Spring MVC
Vamos solicitar as URLs [http://localhost:8080/v03.html?lang=fr] e [http://localhost:8080/v03.html?lang=en]:
![]() | ![]() |
Repare que aplicámos o que aprendemos recentemente. Em vez de nos referirmos à ação [v03] como [/v03], referimo-nos a ela como [/v03.html].
5.4. [/v04]: Criação do modelo M para uma vista V
Considere a seguinte nova ação [/v04]:
// creation of the M model of a V view
@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";
}
- Linha 4: O modelo de visualização é injetado nos parâmetros da ação. Por padrão, este modelo inicial está vazio. Veremos que é possível pré-preenchê-lo;
- Linha 4: Um modelo do tipo [Model] é uma espécie de dicionário de elementos do tipo <String, Object>. Na linha 4, adicionamos uma entrada a este dicionário com a chave [person] associada a um valor do tipo [Person];
- linha 5: exibimos o modelo na consola para ver como fica;
- linha 6: exibimos a vista [vue-04.html];
A classe [Person] é a mesma utilizada no capítulo anterior:
![]() |
package istia.st.springmvc.models;
public class Personne {
// identifier
private Integer id;
// name
private String nom;
// age
private int age;
// manufacturers
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);
}
// getters and setters
...
}
A visualização [vue-04.html] é a seguinte:
![]() |
<!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>
- A linha 10 introduz um novo tipo de expressão Thymeleaf, ${var}, em que var é uma chave do modelo M da vista. Recorde-se que a ação [/v04] adicionou uma chave [person] ao modelo, associada a um tipo Person[id, name, age];
- Linha 10: exibe o nome da pessoa no modelo;
- linha 14: exibe a idade da pessoa;
Os ficheiros de mensagens são modificados para adicionar as chaves [person.name] e [person.age] das linhas 9 e 13. O resultado é o seguinte:
![]() |
e a natureza do modelo M pode ser encontrada nos registos da consola [2].
Poder-se-á perguntar por que razão não escrevemos a vista [view-04] da seguinte forma:
<!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>
Esta visualização é perfeitamente válida e produzirá o mesmo resultado que antes. Um dos objetivos do Thymeleaf é garantir que uma página Thymeleaf possa ser exibida mesmo que não passe pelo Thymeleaf. Assim, vamos criar duas novas páginas estáticas:
![]() |
A visualização [view-04b.html] é uma cópia da visualização [view-04.html]. O mesmo se aplica à visualização [view-04a.html], mas removemos o texto estático da página. Se visualizarmos ambas as páginas, obtemos os seguintes resultados:
![]() |
No caso [1], a estrutura da página não aparece, enquanto no caso [2] é claramente visível. Esta é a vantagem de colocar texto estático numa vista Thymeleaf, mesmo que venha a ser substituído por outro texto em tempo de execução.
Agora, vamos analisar um detalhe técnico. Na vista [vue-04.html], formatamos o código utilizando [Ctrl+Shift+F]. Obtemos o seguinte resultado:
<!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>
As tags estão desalinhadas e o código torna-se mais difícil de ler. Se renomearmos [vue-04.html] para [vue-04.xml] e reformatarmos o código, as tags ficarão novamente alinhadas. Por isso, o sufixo [xml] seria mais prático. É possível trabalhar com este sufixo. Para tal, precisamos de configurar o Thymeleaf. Para evitar desfazer o que fizemos, duplicamos o projeto [springmvc-vues] que temos vindo a estudar para um projeto [springmvc-vues-xml]
![]() |
Modificamos o ficheiro [pom.xml] da seguinte forma:
<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>
O nome do projeto é alterado nas linhas 2 e 6. Além disso, alteramos o sufixo das visualizações na pasta [templates]:
![]() |
O documento [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] lista as propriedades de configuração do Spring Boot que podem ser utilizadas no ficheiro [application.properties]:
![]() |
Este documento lista as propriedades que o Spring Boot utiliza durante a configuração automática e que podem ser modificadas através de uma configuração diferente do [application.properties]. Para o Thymeleaf, as propriedades de configuração automática são as seguintes:
# THYMELEAF (<a href="http://github.com/spring-projects/spring-boot/tree/v1.1.9.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java">ThymeleafAutoConfiguration</a>)
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 # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh
Assim, poderíamos simplesmente adicionar a linha
spring.thymeleaf.suffix=.xml
no ficheiro [application.properties]. No entanto, vamos adotar uma abordagem diferente: configuração através de código. Vamos configurar o Thymeleaf na classe [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;
}
}
- As linhas 16–24 configuram um [TemplateResolver] para o Thymeleaf. Este objeto é responsável por localizar o ficheiro correspondente com base num nome de vista fornecido por uma ação;
- As linhas 18 e 19 definem o prefixo e o sufixo a serem adicionados ao nome da vista para localizar o ficheiro. Assim, se o nome da vista for [vue04], o ficheiro procurado será [classpath:/templates/vue04.xml]. [classpath:/templates] é uma sintaxe do Spring que se refere a uma pasta [/templates] localizada na raiz do classpath do projeto;
- Linha 21: para que o cabeçalho HTTP na resposta enviada ao cliente seja:
Content-Type:text/html;charset=UTF-8
- linha 20: indica que a vista está em conformidade com a norma HTML5;
- linha 22: indica que as vistas Thymeleaf podem ser armazenadas em cache;
- linhas 26–31: define o mecanismo de resolução de visualizações para o par Spring/Thymeleaf, utilizando o mecanismo de resolução anterior;
Vamos executar o executável deste novo projeto e solicitar a URL [http://localhost:8080/v04.html?lang=en]:
![]() |
Note que, na URL, a ação [/v04] foi novamente substituída por [v04.html].
5.5. [/v05]: transformar um objeto numa vista Thymeleaf
Criamos a seguinte ação [/v05]:
// creation of the M model of a V - 2 view
@RequestMapping(value = "/v05", method = RequestMethod.GET)
public String v05(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-05";
}
É idêntico à ação [/v04]. A visualização [vue-05.xml] é a seguinte:
![]() |
<!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>
- linhas 8–17: nestas linhas, um objeto Thymeleaf é definido pelo atributo [th:object="${person}"] (linha 8). Este objeto é o objeto com a chave [person] encontrado no modelo:
- linha 11: a expressão Thymeleaf [*{name}] é equivalente a [${object.name}], onde [object] é o objeto Thymeleaf atual. Portanto, aqui, a expressão [*{name}] é equivalente a [${person.name}];
- linha 15: igual ao anterior;
O resultado:
![]() |
5.6. [/v06]: Testes numa vista Thymeleaf
Considere a seguinte ação [/v06]:
// creation of the M model of a V - 3 view
@RequestMapping(value = "/v06", method = RequestMethod.GET)
public String v06(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-06";
}
É idêntico às duas ações anteriores. Apresenta a seguinte vista [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>
- Linha 17: O atributo [th:if] avalia uma expressão booleana. Se esta expressão for verdadeira, a tag é exibida; caso contrário, não é. Assim, aqui, se ${person.age} >= 18, o texto [#{person.majeure}] será exibido, ou seja, a chave de mensagem [person.majeure] nos ficheiros de mensagens;
- linha 18: não é possível escrever [*{age} < 18] porque o sinal < é um caractere reservado. Por isso, deve utilizar o seu equivalente em HTML [<], também conhecido como a entidade HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];
Os ficheiros de mensagens são modificados:
[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
O resultado é o seguinte:
![]() | ![]() |
5.7. [/v07]: Iteração numa vista Thymeleaf
Considere a seguinte ação [/v07]:
// creation of the M model of a V - 4 view
@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";
}
- A ação cria uma lista de três pessoas, adiciona-a ao modelo associado à chave [list] e apresenta a vista [view-07];
A vista [view-07.xml] é a seguinte:
<!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>
- Linha 10: O atributo [th:each] repete a tag em que se encontra, neste caso uma tag <li>. Aqui, tem dois parâmetros: [element : collection], em que [collection] é uma coleção de objetos, neste caso uma lista de pessoas. O Thymeleaf irá percorrer a coleção e gerar tantas tags <li> quantos forem os elementos da coleção. Para cada tag <li>, [element] representará o elemento da coleção associado à tag. Para este elemento, o atributo [th:text] será avaliado. A sua expressão aqui é uma concatenação de cadeias de caracteres para produzir o resultado [id, name, age];
- linha 8: adicionamos a chave [liste.personnes] aos ficheiros de mensagens;
Eis o resultado:
![]() | ![]() |
5.8. [/v08-/v10]: @ModelAttribute
Vamos revisitar algo que vimos ao estudar as ações: o papel da anotação [@ModelAttribute]. Vamos adicionar a seguinte nova ação:
// --------------- Binding and ModelAttribute ----------------------------------
// if the parameter is an object, it is instantiated and possibly modified by the query parameters
// it will automatically become part of the view model with the key [key]
// for @ModelAttribute("xx") parameter, key will equal xx
// for @ModelAttribute parameter, key will be equal to the parameter's lowercase class name
// if @ModelAttribute is absent, then everything happens as if it were present without a key
// note that this automatic presence in the model is not performed if the parameter is not a
@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";
}
- linha 11: a anotação [@ModelAttribute("someone")] adiciona automaticamente o objeto [Person p] ao modelo, associado à chave [someone];
- linha 12: para verificar o modelo;
- linha 13: exibe a vista [vue-08.xml];
A vista [view-08.xml] é a seguinte:
<!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>
- Linha 8: O objeto Thymeleaf é inicializado com o objeto-chave [someone];
O resultado é o seguinte:
![]() |
e na consola, vemos o seguinte registo:
Modèle={someone=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Agora, vamos considerar a seguinte ação [/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";
}
- Linha 1: A presença do parâmetro [Person p] insere automaticamente a pessoa [p] no modelo. Como nenhuma chave é especificada, a chave utilizada é o nome da classe com o primeiro caractere em minúscula. Portanto, [Person p] é equivalente a [@ModelAttribute("person") Person p];
A vista [view.09.xml] é a seguinte:
<!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>
- linha 8: a chave do modelo utilizada é [person];
Eis o resultado:
![]() |
e o registo na consola do servidor:
Modèle={personne=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Agora, vamos considerar a seguinte nova ação [/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";
}
- linhas 1-4: definem um método que cria um elemento-chave [anotherPerson] no modelo para cada pedido, associado ao objeto [new Person(24, "pauline", 55)];
- linhas 6-10: a ação [/v10] não faz nada além de passar o modelo que recebe para a vista [vue-10.xml]. Note que o parâmetro [Model model] só é necessário para a instrução na linha 8. Sem ele, é desnecessário;
A vista [view-10.xml] é a seguinte:
<!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>
O resultado é o seguinte:
![]() |
e o registo da consola é o seguinte:
5.9. [/v11]: @SessionAttributes
Estamos a revisitar algo que vimos ao estudar as ações: o papel da anotação [@SessionAttributes]. Vamos adicionar a seguinte nova ação [/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";
}
Temos algo semelhante ao que acabámos de abordar. A diferença reside numa anotação [@SessionAttributes] colocada na própria classe:
@Controller
@SessionAttributes("jean")
public class ViewsController {
- Linha 2: Especificamos que a chave [jean] do modelo deve ser inserida na sessão;
É por isso que, na linha 7 da ação, injetámos a sessão. Na linha 8, exibimos o valor da sessão associado à chave [jean].
A vista [view-11.xml] é a seguinte:
<!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>
São apresentadas duas pessoas:
- linhas 8–21: a pessoa com a chave [jean] no modelo;
- linhas 23–36: a pessoa com a chave [jean] na sessão;
Os resultados são os seguintes:
![]() |
- em [1], a pessoa com a chave [jean] no modelo;
- em [2], a pessoa com a chave [jean] na sessão;
O registo da consola é o seguinte:
Modèle={uneAutrePersonne=[id=24, nom=pauline, age=55], jean=[id=33, nom=jean, age=10]}, Session[jean]=null
Acima, vemos que a chave [jean] não está na sessão recebida pela ação. Podemos inferir que a chave [jean] foi adicionada à sessão após a execução da ação e antes da renderização da vista.
Agora, vamos considerar o caso em que uma chave é referenciada tanto por [@ModelAttribute] como por [@SessionAttributes]. Vamos criar as duas ações seguintes:
@RequestMapping(value = "/v12a", method = RequestMethod.GET)
@ResponseBody
public void v12a(HttpSession session) {
session.setAttribute("paul", new Personne(51, "paul", 33));
}
// if the key of [@ModelAttribute] is also a key of [@SessionAttributes]
// in this case, the corresponding parameter is initialized with the session value
@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";
}
A ação [/v12a] é utilizada apenas para armazenar o elemento ['paul', new Person(51, "paul", 33)] na sessão. Não faz mais nada. O facto de estar marcada com [@ResponseBody] indica que gera a resposta para o cliente. Uma vez que o seu tipo é [void], não é gerada qualquer resposta.
A ação [/v12b] aceita [@ModelAttribute("paul") Person p] como parâmetro. Se nada mais for feito, um objeto [Person] é instanciado e, em seguida, inicializado com os parâmetros da solicitação, e este objeto não tem nada a ver com o objeto com a chave [paul] colocado na sessão pela ação [/v12a]. Iremos adicionar a chave [paul] aos atributos de sessão da classe:
@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
- A linha 2 tem agora dois atributos de sessão;
Vamos voltar aos parâmetros da ação [/v12b]:
public String v12b(Model model, @ModelAttribute("paul") Personne p) {
Agora, o objeto [Person p] não será instanciado, mas fará referência ao objeto com a chave [paul] na sessão. O resto do processo permanece o mesmo. O objeto com a chave [paul] aparecerá no modelo de visualização que será exibido. É isto que queremos ver na linha 11 da ação [/v12b].
A vista [vue-12.xml] será a seguinte:
<!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>
- linha 8: faz referência à chave [paul] do modelo de visualização;
Isto produz o seguinte resultado (após executar a ação [/v12a], que coloca a chave [paul] na sessão):
![]() |
O registo da consola é o seguinte:
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}
A chave [paul] foi adicionada com sucesso ao modelo com o valor associado à chave [paul] na sessão.
5.10. [/v13]: Gerar um formulário de entrada
Vamos agora discutir a entrada e a validação de formulários. Vamos construir um primeiro formulário utilizando a seguinte ação [/v13]:
// generates a form for entering a person
@RequestMapping(value = "/v13", method = RequestMethod.GET)
public String v13() {
return "vue-13";
}
o que simplesmente apresenta a seguinte visualização [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>
Se colocarmos esta vista na pasta [static] com o nome [view-13.html] e solicitarmos o URL [http://localhost:8080/vue-13.html], obtemos a seguinte página:
![]() |
- Na linha 8 do formulário, encontramos a tag <form> com o atributo [th:action]. Este atributo será avaliado pelo Thymeleaf, e o seu valor substituirá o valor atual do atributo [action], que, portanto, está lá apenas para fins decorativos. Aqui, o valor do atributo [th:action] será [/v14.html];
- Nas linhas 17, 23 e 29, o valor do atributo [th:value] substituirá o do atributo [value]. Aqui, esse valor será a cadeia vazia;
Quando solicitamos a URL [/v13.html], obtemos o seguinte resultado:
![]() |
Vamos dar uma olhada no código-fonte gerado pelo 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>
As linhas 9, 18, 24 e 30 mostram o Thymeleaf a avaliar os atributos [th:action] e [th:value].
5.11. [/v14]: Tratamento dos valores enviados por um formulário
A ação [/v14] é a ação que recebe os valores enviados. É a seguinte:
// processes form values
@RequestMapping(value = "/v14", method = RequestMethod.POST)
public String v14(Personne p) {
return "vue-14";
}
- Linha 3: Os valores enviados são encapsulados num objeto [Person p]. Sabemos que este objeto passa automaticamente a fazer parte do modelo M da vista V que será exibida pela ação, associado à chave [person];
- linha 4: a vista exibida é a vista [vue-14.xml];
A vista [view-14.xml] é a seguinte:
<!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>
- linha 9: recupera o objeto associado à chave [person] do modelo;
- linhas 12, 16 e 20: exibimos as propriedades deste objeto;
Isto produz o seguinte resultado:
![]() | ![]() |
5.12. [/v15-/v16]: validação de um modelo
Usando o exemplo anterior, vamos analisar a seguinte sequência:
![]() |
- em [1], introduzimos valores incorretos para os campos [id] e [age] do tipo [int];
- Em [2], a resposta do servidor indica que ocorreram dois erros;
Vamos utilizar o mesmo formulário, mas, em caso de erros de validação, redirecionaremos para uma página que lista esses erros, para que o utilizador os possa corrigir.
A ação [/v15] é a seguinte:
// ---------------------- form display
@RequestMapping(value = "/v15", method = RequestMethod.GET)
public String v15(SecuredPerson p) {
return "vue-15";
}
Recebe o seguinte tipo [SecuredPerson] como parâmetro:
![]() |
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;
// manufacturers
public SecuredPerson() {
}
public SecuredPerson(int id, String nom, int age) {
this.id=id;
this.nom = nom;
this.age = age;
}
// getters and setters
...
}
Os campos [id, name, age] foram anotados com restrições de validação. A vista [view-15.xml] apresentada pela ação [/v15] é a seguinte:
<!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>
- linhas 10-47: o objeto do modelo de página associado à chave [securedPerson] é recuperado. Após o pedido GET, temos um objeto com os seguintes valores de instanciação: [id=0, name=null, age=0];
- linha 17: o valor do campo [securedPerson.id];
- linha 20: a expressão [${#fields.hasErrors('id')}] determina se houve erros de validação no campo [securedPerson.id]. Se sim, o atributo [th:errors="*{id}"] exibe a mensagem de erro associada;
- este cenário repete-se na linha 29 para o campo [name] e na linha 38 para o campo [age];
- linha 45: a expressão [${#fields.errors('*')}] refere-se a todos os erros nos campos do objeto [securedPerson]. Assim, é o conjunto destes erros que será exibido pelas linhas 44–46;
- linha 16: vemos que os valores do formulário serão enviados para a ação [/v16]. Isto é feito da seguinte forma:
// -------------------- model validation------------------
@RequestMapping(value = "/v16", method = RequestMethod.POST)
public String v16(@Valid SecuredPerson p, BindingResult result) {
// mistakes?
if (result.hasErrors()) {
return "vue-15";
} else {
return "vue-16";
}
}
- Linha 3: A anotação [@Valid SecuredPerson p] impõe a validação dos valores enviados;
- linha 5: verifica se o modelo de ação é inválido ou não;
- linha 6: se for inválido, o formulário [vue-15.xml] é devolvido. Uma vez que este formulário apresenta mensagens de erro, iremos vê-las;
- linha 8: se o modelo de ação for validado, exibimos a seguinte vista [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>
Aqui estão alguns exemplos de execução:
![]() | ![]() |
![]() | ![]() |
![]() |
![]() |
5.13. [/v17-/v18]: Verificação de mensagens de erro
Quando a ação [/v15] é solicitada pela primeira vez, obtém-se o seguinte resultado:
![]() |
Pode preferir um formulário vazio em vez de zeros nos campos [Username, Age]. Para o conseguir, modificamos o modelo de ação da seguinte forma:
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;
// manufacturers
public StringSecuredPerson() {
}
public StringSecuredPerson(String id, String nom, String age) {
this.id = id;
this.nom = nom;
this.age = age;
}
// getters and setters
...
}
- linhas 12 e 19: os campos [id] e [age] são definidos como do tipo [String];
- linha 11: especifica-se que o campo [id] deve ser um número com, no máximo, quatro dígitos, sem decimais;
- linha 18: o mesmo se aplica ao campo [age], que deve ser um número inteiro com, no máximo, dois dígitos;
A ação [/v17] passa a ser a seguinte:
// ---------------------- form display
@RequestMapping(value = "/v17", method = RequestMethod.GET)
public String v17(StringSecuredPerson p) {
return "vue-17";
}
A visualização [vue-17.xml] apresentada pela ação [/v17] é a seguinte:
<!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>
As alterações são feitas nas seguintes linhas:
- linha 10: agora trabalhamos com o objeto-chave [stringSecuredPerson];
- linha 20: percorremos a lista de erros para o campo [id]. Na sintaxe [th:each="err,status : ${#fields.errors('id')}"], a variável [err] percorre a lista. A variável [status] fornece informações sobre cada iteração. É um objeto [index, count, size, current] onde:
- index: é o número do elemento atual,
- current: é o valor do elemento atual,
- count, size: o tamanho da lista que está a ser percorrida;
- linha 20: apenas exibimos o primeiro elemento da lista [th:if="${status.index}==0"] ;
A ação [/v18] que processa o POST da ação [/v17] é a seguinte:
// -------------------- model validation------------------
@RequestMapping(value = "/v18", method = RequestMethod.POST)
public String v18(@Valid StringSecuredPerson p, BindingResult result) {
// mistakes?
if (result.hasErrors()) {
return "vue-17";
} else {
return "vue-18";
}
}
Os ficheiros de mensagens são atualizados da seguinte forma:
[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
Vejamos alguns exemplos:
![]() |
![]() |
Em [1], vemos que ambos os validadores para o campo [idade] foram executados:
@Range(min = 8, max = 14)
@Digits(fraction = 0, integer = 2)
private String age;
Existe uma ordem específica para as mensagens de erro? Para o campo [age], parece que os validadores foram executados na ordem [Digits, Range]. No entanto, se fizermos várias solicitações, podemos ver que esta ordem pode mudar. Portanto, não podemos confiar na ordem dos validadores. Em [2], apenas uma das duas mensagens de erro para o campo [id] é exibida. Em [3], todas as mensagens de erro são mostradas.
5.14. [/v19-/v20]: Utilização de Validadores Diferentes
Considere o seguinte novo modelo de ação:
![]() |
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;
// getters and setters
...
}
Será exibido pela seguinte ação [/v19]:
// ------------------ form display
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- Linha 3: A ação recebe um objeto [Form19 form] como parâmetro. Se a solicitação GET não receber nenhum parâmetro, este objeto será inicializado com os valores padrão do Java;
- linha 4: a vista [vue-19.xml] é apresentada. É a seguinte:
<!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>
Este código apresenta a seguinte visualização:
![]() |
A página apresenta uma tabela com três colunas:
- Coluna 1: o validador do campo de entrada;
- coluna 2: o campo de entrada;
- coluna 3: mensagens de erro para o campo de entrada;
Vamos examinar, por exemplo, o código da visualização [/v19.html] para o validador [@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>
Vemos o código que acabámos de estudar com os formulários [Person]:
- linha 2: a primeira coluna: o nome do validador que está a ser testado;
- linha 4: o atributo Thymeleaf [th:field="*{hhmmss}] irá gerar os atributos HTML [id="hhmmss"] e [name="hhmmss"]. O atributo Thymeleaf [th:value="*{hhmmss}"] irá gerar o atributo HTML [value="valor de [form19.hhmmss]]";
- linha 7: se o valor introduzido no campo [form19.hhmmss] estiver incorreto, a linha 7 exibe as mensagens de erro associadas a este campo;
Os valores enviados são processados pela seguinte ação [/v20]:
// ----------------- form template validation
@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 {
// redirection to [vue-19]
redirectAttributes.addFlashAttribute("form19", formulaire);
return "redirect:/v19.html";
}
}
- linha 3: os valores enviados preencherão os campos do objeto [Form19] se forem válidos;
- linhas 4–6: se os valores enviados forem inválidos, o formulário [view-19] é exibido novamente com mensagens de erro;
- linhas 6–10: se os valores enviados forem válidos, o objeto [Form19] construído com esses valores é disponibilizado para a próxima solicitação, neste caso o redirecionamento. Em seguida, é destruído;
- linha 9: o cliente é redirecionado para a ação [/v19.html]. Isto irá exibir novamente o formulário [vue-19], que contém código como:
<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
O atributo [th:object="${form19}"] irá então recuperar o objeto associado ao atributo Flash [form19] e, assim, voltar a apresentar o formulário tal como foi preenchido.
O código do formulário merece uma explicação mais aprofundada. Considere o seguinte código:
<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>
Isto gera o seguinte código 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>
No código
<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>
Os atributos Thymeleaf nas linhas 1 e 3 [th:field="*{assertFalse}"] colocam um problema. Observámos que este atributo gera os atributos HTML [id=assertFalse] e [name=assertFalse]. A dificuldade surge porque, uma vez que isto é gerado nas linhas 1 e 3, acabamos por ter dois atributos [name] idênticos e dois atributos [id] idênticos. Embora isto seja possível com o atributo [name], não o é com o atributo [id]. Como se pode ver no código HTML gerado, o Thymeleaf gerou dois atributos [id] diferentes: [id=assertFalse1] e [id=assertFalse2]. Isto é positivo. O problema é que não conhecemos estes identificadores e podemos precisar deles. É o caso da tag [label] na linha 2. O atributo [for] de uma tag HTML [label] deve referenciar um atributo [id], neste caso aquele gerado para a tag [input] na linha 1. A documentação do Thymeleaf afirma que a expressão [${#ids.prev('assertFalse')}] recupera o último atributo [id] gerado para o campo [assertFalse].
Agora, vamos analisar o código da lista suspensa do formulário:
<select th:field="*{assertTrue}">
<option value="true">True</option>
<option value="false">False</option>
</select>
Este código gera o código HTML para uma lista suspensa:
O valor enviado será transmitido com o nome [name="assertTrue"].
A vista [vue-19.xml] utiliza uma folha de estilo:
<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>
Linha 4: A folha de estilo utilizada deve ser colocada na pasta [static] do projeto:
![]() |
O seu conteúdo é o seguinte:
@CHARSET "UTF-8";
.col1 {
background: lightblue;
}
.col2 {
background: Cornsilk;
}
.col3 {
background: #e2d31d;
}
.error {
color: red;
}
Agora, vamos ver as datas:
@NotNull
@Future
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInFuture;
@NotNull
@Past
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;
A análise do tráfego de rede no Chrome DevTools (Ctrl+Shift+I) mostra que as datas são enviadas no formato (aaaa-mm-dd):
![]() |
É por isso que as datas foram anotadas com o validador:
@DateTimeFormat(pattern = "yyyy-MM-dd")
que define o formato esperado para os valores de data publicados.
Por fim, o ficheiro de mensagens em francês [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
Vejamos alguns exemplos de execução:
![]() |
![]() |
![]() |
Acima, entre [1] e [2], parece que nada aconteceu. No entanto, se analisarmos o tráfego de rede (Ctrl-Shift-I), vemos que houve duas trocas de dados com o servidor:
![]() |
- em [1], o POST inicial para [/v20];
- em [2], a resposta a esta ação é um redirecionamento;
- em [3], o segundo pedido, desta vez para [/v19];
A ação [/v19] é então executada:
// ------------------ form display
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- linha 3: o parâmetro [Form19 form] é inicializado com o atributo Flash [form19], que foi criado pela ação anterior [/v19] e é um objeto do tipo [Form19] contendo os valores enviados para a ação [/v19];
- Linha 4: A vista [view-19.xml] será apresentada com um objeto [Form19 form] no seu modelo, inicializado com os valores enviados. É por isso que o utilizador vê o formulário exatamente como o enviou;
Porquê um redirecionamento? Por que não enviámos simplesmente para a ação [/v19] acima? Teríamos obtido o mesmo resultado, com algumas diferenças:
- o navegador teria inserido [http://localhost:8080/v20.html] na sua barra de endereços em vez de [http://localhost:8080/v19.html], como fez aqui, porque exibe o último URL chamado;
- se o utilizador atualizar a página (F5), o resultado é completamente diferente:
- no caso de redirecionamento, a URL exibida é [http://localhost:8080/v19.html], obtida através de uma solicitação GET. O navegador irá reexecutar este comando e receberá então um formulário totalmente novo (o atributo Flash é utilizado apenas uma vez),
- no caso de não haver redirecionamento, a URL exibida é [http://localhost:8080/v20.html], obtida através de uma solicitação POST. O navegador irá reexecutar este comando e, portanto, realizar outra solicitação POST com os mesmos valores de antes. Aqui, isto não tem consequências, mas é frequentemente indesejável, pelo que o redirecionamento é geralmente preferido;
5.15. [/v21-/v22]: Tratamento de botões de opção
Considere o seguinte componente [Lists] do Spring:
![]() |
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" };
// getters and setters
...
}
- linha 5: a classe [Lists] será um componente Spring;
- linhas 8–10: listas utilizadas para preencher botões de opção, caixas de seleção e listas suspensas;
Na classe de configuração [Config], diz-se:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
- Linha 2: O pacote [models], onde se encontra o componente [Lists], será analisado pelo Spring;
Criamos as seguintes novas ações:
// ------------------ form with radio buttons
@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";
}
- linhas 2-3: o componente [Lists] é injetado no controlador;
- linha 6: tratamos um formulário [Form21], que iremos descrever. Note que especificámos a sua chave [form] no modelo de visualização. Recorde que, por predefinição, esta teria sido [form21];
- linha 7: injetamos o componente [Lists] no modelo. A vista irá precisar dele;
- linha 8: exibimos a vista [vue-21.xml]. Esta vista exibirá o formulário [Form21], e os valores enviados serão encaminhados para a ação [/v22] nas linhas 12–15;
- linhas 12–15: a ação [/v22] simplesmente redireciona para a ação [/v21], colocando os valores enviados que recebeu num atributo Flash com a chave [form]. É importante que esta chave corresponda à utilizada na linha 6;
O modelo [Form21] é o seguinte:
![]() |
package istia.st.springmvc.models;
public class Form21 {
// posted values
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;
// getters and setters
...
}
A visualização [view-21.xml] é a seguinte:
<!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>
- Linhas 36–40: Observe a utilização do componente [Lists] no modelo para gerar os rótulos das caixas de seleção;
- A coluna 3 exibe o valor enviado via POST ou o valor inicial do formulário durante a solicitação GET inicial;
Este código apresenta a seguinte página:
![]() |
correspondente ao seguinte código 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>
Podemos ver que os valores enviados (atributos name) são enviados para os seguintes campos no modelo [Form21]:
private String marie = "non";
private String deplacement = "4";
Os leitores são encorajados a realizar testes. Note-se que é o atributo [value] dos botões de opção que é enviado.
![]() | ![]() |
5.16. [/v23-/v24]: gestão de caixas de seleção
Adicionamos a seguinte nova ação:
// ------------------ form with checkboxes
@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";
}
- Linha 3: Continuamos a utilizar o modelo [Form21];
A vista [vue-23.xml] é a seguinte:
<!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>
- Linhas 37–41: Repare na utilização do componente [Lists] para gerar os rótulos das caixas de seleção;
Este código apresenta a seguinte página:
![]() |
gerada a partir do seguinte código 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>
Note que os valores enviados (atributos de nome) são enviados para os seguintes campos no [Form21]:
private String[] couleurs;
private String[] bijoux;
Trata-se de matrizes porque, para cada campo, existem várias caixas de seleção rotuladas com o nome do campo. É, portanto, possível que cheguem vários valores enviados com o mesmo nome (o atributo name do formulário). Por isso, é necessária uma matriz para os recuperar.
Voltemos ao código Thymeleaf na coluna 3 da página:
<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>
Os campos referenciados nas linhas 2 e 14 são os seguintes:
private String strCouleurs;
private String strBijoux;
São calculados pela ação [/v24] que processa o POST:
// mapper Jackson / 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";
}
Tenha em atenção que a biblioteca Jackson/JSON está incluída nas dependências do projeto.
- Linha 2: Criamos um tipo [ObjectMapper] que nos permite serializar e deserializar objetos de e para JSON.
- Linha 7: Serializamos a matriz de cores para JSON. O resultado é colocado no campo [strCouleurs];
- Linha 8: Serializamos a matriz de joias para JSON. O resultado é armazenado no campo [strBijoux];
Eis um exemplo de execução:
![]() | ![]() |
Note que é o atributo [value] das caixas de seleção que é enviado.
5.17. [/25-/v26]: gestão de listas
Adicionamos a seguinte ação [/v25]:
// ------------------ form with lists
@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";
}
A vista [vue-25.xml] é a seguinte:
<!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>
- linhas 38-42: gerar uma lista suspensa cujos rótulos são retirados do componente [Lists] que já utilizámos;
A página apresentada é a seguinte:
![]() |
gerado pelo seguinte código 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>
- Linha 44: Note que o Thymeleaf criou um campo oculto. Não compreendo a sua finalidade:
- os valores enviados (atributos value das tags option) serão armazenados nos seguintes campos (atributos name) do [Form21]:
private int couleur2;
private int[] bijoux2;
- linha 38: a lista [jewelry2] é uma lista de escolha múltipla. Por conseguinte, podem ser publicados vários valores associados ao nome [jewelry2]. Para os recuperar, o campo [jewelry2] deve ser uma matriz. Note-se que se trata de uma matriz de inteiros. Isto é possível porque os valores publicados podem ser convertidos para este tipo;
Os valores são enviados para a seguinte ação [/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";
}
Não há aqui nada que não tenhamos visto antes. Aqui está um exemplo de execução:
![]() | ![]() |
5.18. [/v27]: configuração de mensagens
Considere a seguinte ação [/v27]:
// ------------------ set messages
@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";
}
A ação simplesmente define quatro valores no modelo e apresenta a seguinte vista [view-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>
- linha 8: uma mensagem sem parâmetros;
- linha 9: uma mensagem com um parâmetro [$param1] retirado do modelo;
- linha 10: uma mensagem com dois parâmetros [$param2, $param3] retirados do modelo;
- linha 11: uma mensagem com um parâmetro. Este parâmetro é, ele próprio, uma chave de mensagem (indicada por #). A chave é fornecida por [$param4];
O ficheiro de mensagens em francês é o seguinte:
[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
Para indicar a presença de parâmetros na mensagem, utilizamos os símbolos {0}, {1}, ...
A fusão do modelo criado pela ação [/v27] com a visualização [vue-27] produzirá o seguinte código 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>
o que resulta na seguinte visualização:
![]() |
O ficheiro de mensagens em inglês é o seguinte:
[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
A fusão do modelo criado pela ação [/v27] com a vista [vue-27] produzirá o seguinte código 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>
o que resulta na seguinte visualização:
![]() |
Podemos ver que a última mensagem foi totalmente internacionalizada, o que não é o caso das duas anteriores.
5.19. Utilização de uma página mestre
Numa aplicação web, as visualizações partilham frequentemente vários elementos que podem ser agrupados numa página mestre. Aqui está um exemplo:
![]() |
Acima, temos duas páginas semelhantes, nas quais o fragmento [1] foi substituído pelo fragmento [2]. A vista corresponde a uma página mestre com três fragmentos fixos [3-5] e um fragmento variável [6].
5.19.1. O projeto
Estamos a desenvolver um projeto [springmvc-masterpage] seguindo a abordagem descrita na Secção 5.1.
![]() |
O ficheiro [pom.xml] é o seguinte:
<?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/> <!-- lookup parent from repository -->
</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>
Uma das dependências incluídas neste ficheiro é necessária para a página principal:
![]() |
Os pacotes [config] e [main] são idênticos aos de nomes iguais no projeto anterior.
5.19.2. A página principal
![]() |
A página mestre é a seguinte vista [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>
- linha 2: a página mestre deve definir o namespace [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], cujo elemento é utilizado na linha 19;
- linhas 10–12: geram a área [1] abaixo. A tag Thymeleaf [th:include] permite incluir um fragmento definido noutro ficheiro na vista atual. Isto permite reutilizar fragmentos em várias vistas;
- linhas 15–17: geram a área [2] abaixo;
- linhas 19–20: geram a área [3] abaixo. O atributo [layout:fragment] pertence ao namespace [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Indica uma área que pode ser substituída por outra em tempo de execução;
- linhas 24–28: geram a área [4] abaixo;
![]() |
5.19.3. Os fragmentos
Os fragmentos [entete.xml], [menu.xml] e [basdepage.xml] são os seguintes:
[entete.xml]
<!DOCTYPE html>
<html>
<h2>entête</h2>
</html>
[menu.xml]
<!DOCTYPE html>
<html>
<h2>menu</h2>
</html>
[footer.xml]
<!DOCTYPE html>
<html>
<h2>bas de page</h2>
</html>
O fragmento [page1.xml] é o seguinte:
<!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>
- linha 2: o atributo [layout:decorator="layout"] indica que a página atual [page1.xml] está «decorada», ou seja, pertence a uma página mestre. Este é o valor do atributo, neste caso a vista [layout.xml];
- linha 3: isto especifica em que fragmento da página mestre [page1.xml] será inserido. O atributo [layout:fragment="contenu"] indica que [page1.xml] será inserido no fragmento denominado [contenu], ou seja, a zona [3] da página mestre;
- linhas 5–7: o conteúdo do fragmento é um formulário que inclui um botão POST que aponta para a ação [/page2.html];
O fragmento [page2.xml] é semelhante:
<!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. As ações
![]() |
O controlador [Layout.java] é o seguinte:
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";
}
}
- linhas 10–12: a ação [/page1] simplesmente exibe a vista [page1.xml];
- linhas 15-17: o mesmo se aplica à ação [/page2], que exibe a visualização [page2.xml];













































































