5. Las vistas de Thymeleaf
Volvamos a la arquitectura de una aplicación Spring MVC.
![]() |
En los dos capítulos anteriores se describieron diversos aspectos del bloque [1]: las acciones. Ahora abordaremos:
- el bloque [2] de las vistas V;
- el bloque [3] del modelo M que muestran estas vistas;
Desde la creación de Spring MVC, la tecnología utilizada para generar las páginas HTML enviadas a los navegadores de los clientes era la de las páginas JSP (Java Server Pages). Desde hace algunos años, también se puede utilizar la tecnología [Thymeleaf] [http://www.thymeleaf.org/]. Es esta la que presentamos ahora.
5.1. El proyecto STS
Creamos un nuevo proyecto:
![]() |
![]() |
- En [3], indicamos que el proyecto necesita las dependencias de [Thymeleaf]. Esto agregará, además de las dependencias [Spring MVC] del proyecto anterior, las del marco [Thymeleaf] y [5];
Ahora, modifiquemos este proyecto de la siguiente manera:
![]() |
Nos inspiramos en el proyecto anterior:
- [istia.st.springmvc.controllers] contendrá los controladores;
- [istia.st.springmvc.models] contendrá los modelos de las acciones y las vistas;
- [istia.st.springmvc.main] es el paquete de la clase ejecutable de Spring Boot;
- [templates] contendrá las vistas de Thymeleaf;
- [i18n] contendrá los mensajes internacionalizados que se muestran en las vistas;
La clase [Application] es la siguiente:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
SpringApplication.run(Config.class, args);
}
}
La clase [Config] es la siguiente:
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 configuración permite, por el momento, la gestión de las configuraciones regionales.
El controlador [ViewController] es el siguiente:
package istia.st.springmvc.actions;
import org.springframework.stereotype.Controller;
@Controller
public class ViewsController {
}
- En la línea 5, la anotación [@Controller] ha reemplazado a la anotación [@RestController] porque, a partir de ahora, las acciones no generarán la respuesta al cliente. En su lugar:
- construir un modelo M
- devolver un tipo [String], que será el nombre de la vista [Thymeleaf] encargada de mostrar este modelo. Es la combinación de esta vista V y este modelo M la que generará el flujo HTML enviado al cliente;
El archivo [messages.properties] está vacío por el momento.
5.2. [/v01]: los fundamentos de Thymeleaf
Consideremos la siguiente acción en [ViewsController]:
// Conceptos básicos de Thymeleaf - 1
@RequestMapping(value = "/v01", method = RequestMethod.GET)
public String v01() {
return "v01";
}
- línea 3: la acción devuelve un tipo [String]. Este será el nombre de la acción;
- línea 4: esta vista será [v01]. Por defecto, debe estar ubicada en la carpeta [templates] y llamarse [v01.html];
La vista [v01.html] es la siguiente:
![]() |
<!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>
Se trata de un archivo HTML. La presencia de Thymeleaf se observa:
- en el espacio de nombres [th] de la línea 2;
- en los atributos [th:text] de las líneas 4 y 8;
Tenemos aquí un archivo HTML válido que se puede visualizar. Lo colocamos en la carpeta [static] [2] con el nombre [vue-01.html] y lo abrimos directamente con un navegador:
![]() |
Si revisamos el código fuente de la página en [2], podemos observar que los atributos [th:text] fueron enviados por el servidor y fueron ignorados por el navegador. Cuando una vista es el resultado de una acción, Thymeleaf entra en acción e interpreta los atributos [th] antes de enviar la respuesta al cliente.
La etiqueta HTML:
<title th:text="'Les vues'">Spring 4 MVC</title>
es procesada de la siguiente manera por Thymeleaf:
- th:text tiene la sintaxis th:text="expresión", donde expresión es una expresión que se debe evaluar. Cuando esta expresión es una cadena de caracteres, como en este caso, debe rodearse de comillas simples;
- el valor de [expression] reemplaza el texto de la etiqueta HTML, en este caso el texto de la etiqueta [title];
Tras el procesamiento, la etiqueta anterior quedó así:
<title>Les vues</title>
Solicitemos la acción [/v01]:
![]() |
- En [2], se observa el trabajo de sustitución realizado por Thymeleaf;
Ahora solicitemos la acción URL a partir de [http://localhost:8080/v01.html]:
![]() |
¿Cómo se debe interpretar esto? ¿Se ha servido la vista [templates/v01.html] directamente sin pasar por una acción? Para aclarar las cosas, creamos la siguiente acción [/v02]:
// Conceptos básicos de Thymeleaf - 2
@RequestMapping(value = "/v02", method = RequestMethod.GET)
public String v02() {
System.out.println("action v02");
return "vue-02";
}
La vista [vue-02.html] es una copia de [v01.html]:
![]() |
Ahora solicitemos la acción URL y [http://localhost:8080/vue-02.html]:
![]() |
No se encontró el archivo URL. Ahora solicitemos el URL y el [http://localhost:8080/v02.html]
![]() |
- en los registros de la consola, en [1], vemos que se llamó a la acción [/v02], y esta hizo que se mostrara la vista [vue-02.html] en [2];
Ahora sabemos que URL [http://localhost:8080/v02.html] también puede referirse a un archivo [/v02.html] en la carpeta [static]. ¿Qué sucede si este archivo existe? Lo intentamos. Creamos en la carpeta [static] el siguiente archivo [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>
luego solicitamos el URL [http://localhost:8080/v02.html]:
![]() |
[1] y [2] muestran que se invocó la acción [/v02]. Por lo tanto, hay que recordar que cuando la acción URL solicitada tiene la forma [/x.html], Spring / Thymeleaf:
- ejecuta la acción [/x] si existe;
- sirve la página [/static/x.html] si existe;
- lanza una excepción 404 Not found en caso contrario;
Para evitar confusiones, a partir de ahora, las acciones y las vistas no tendrán los mismos nombres.
5.3. [/v03]: internacionalización de las vistas
La integración de Spring y Thymeleaf permite que Thymeleaf utilice los archivos de mensajes de Spring. Consideremos la siguiente acción nueva [/v03]:
// internacionalización de las vistas
@RequestMapping(value = "/v03", method = RequestMethod.GET)
public String v03() {
return "vue-03";
}
Esta acción muestra la siguiente vista [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>
En las líneas 4 y 8, la expresión del atributo [th:text] es #{title}, cuyo valor es el mensaje de clave [title]. Creamos los siguientes archivos [messages_fr.properties] y [messages_en.properties]:
[messages_fr.properties]
title=Les vues dans Spring MVC
[messages_en.properties]
title=Views in Spring MVC
Solicitemos los siguientes: URL, [http://localhost:8080/v03.html?lang=fr] y [http://localhost:8080/v03.html?lang=en]:
![]() | ![]() |
Notemos que hemos aplicado lo que aprendimos recientemente. En lugar de designar la acción [v03] como [/v03], la hemos designado como [/v03.html].
5.4. [/v04]: creación de la plantilla M de una vista V
Consideremos la siguiente acción nueva [/v04]:
// Creación de la plantilla M de una vista 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";
}
- línea 4: el modelo de la vista se inyecta en los parámetros de la acción. Por defecto, este modelo inicial está vacío. Veremos que es posible prellenarlo;
- línea 4: una plantilla de tipo [Model] es una especie de diccionario de elementos de tipo <String, Object>. Línea 4: agregamos una entrada en este diccionario con la clave [personne] asociada a un valor de tipo [Personne];
- línea 5: mostramos el modelo en la consola para ver cómo se ve;
- línea 6: mostramos la vista [vue-04.html];
La clase [Personne] es la que se utilizó en el capítulo anterior:
![]() |
package istia.st.springmvc.models;
public class Personne {
// identificador
private Integer id;
// nombre
private String nom;
// edad
private int age;
// constructores
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 y setters
...
}
La vista [vue-04.html] es la siguiente:
![]() |
<!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>
- En la línea 10, se introduce un nuevo tipo de expresión Thymeleaf ${var}, donde var es una clave del modelo M de la vista. Recordemos que la acción [/v04] incluyó en la plantilla una clave [personne] asociada a un tipo Persona[id, nom, age];
- línea 10: muestra el nombre de la persona que figura en el modelo;
- línea 14: muestra su edad;
Los archivos de mensajes se modifican para agregar las claves [personne.nom] y [personne.age] de las líneas 9 y 13. El resultado es el siguiente:
![]() |
y se encuentra el tipo de plantilla M en los registros de la consola [2].
Cabe preguntarse por qué no se escribe la vista [vue-04] de la siguiente manera:
<!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 vista es perfectamente válida y dará el mismo resultado que la anterior. Uno de los objetivos de Thymeleaf es que la página de Thymeleaf se pueda mostrar incluso si no pasa por Thymeleaf. Por lo tanto, creemos dos nuevas páginas estáticas:
![]() |
La vista [vue-04b.html] es una copia de la vista [vue-04.html]. Lo mismo ocurre con la vista [vue-04a.html], pero se han eliminado los textos estáticos de la página. Si visualizamos ambas páginas, obtenemos los siguientes resultados:
![]() |
En el caso de [1], la estructura de la página no aparece, mientras que en el caso de [2] sí es visible. Este es el beneficio de incluir textos estáticos en una vista de Thymeleaf, aunque durante la ejecución vayan a ser reemplazados por otros textos.
Ahora, veamos un detalle técnico. En la vista [vue-04.html], le damos formato al código mediante [ctrl-Maj-F]. Obtenemos el siguiente 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>
Las etiquetas están mal alineadas y el código se vuelve más difícil de leer. Si cambiamos el nombre de [vue-04.html] a [vue-04.xml] y reformateamos el código, las etiquetas vuelven a estar alineadas. Por lo tanto, el sufijo [xml] sería más práctico. Es posible trabajar con este sufijo. Para ello, hay que configurar Thymeleaf. Para no deshacer lo que hemos hecho, duplicamos el proyecto [springmvc-vues] que hemos estudiado en un proyecto [springmvc-vues-xml]
![]() |
Modificamos el archivo [pom.xml] de la siguiente manera:
<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>
El nombre del proyecto se cambia en las líneas 2 y 6. Además, cambiamos el sufijo de las vistas que se encuentran en la carpeta [templates]:
![]() |
El documento [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] enumera las propiedades de configuración de Spring Boot que se pueden utilizar en el archivo [application.properties]:
![]() |
Este documento presenta las propiedades que Spring Boot utiliza al realizar la autoconfiguración y que se pueden modificar al establecer una configuración diferente en [application.properties]. Para Thymeleaf, las propiedades de autoconfiguración son las siguientes:
# 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 # ; se agrega «charset=<encoding>»
spring.thymeleaf.cache=true # configurado en falso para la actualización en tiempo real
Por lo tanto, bastaría con agregar la línea
spring.thymeleaf.suffix=.xml
en [application.properties]. Sin embargo, vamos a seguir otro camino: el de la configuración mediante programación. Configuraremos Thymeleaf en la clase [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;
}
}
- las líneas 16-24 configuran un [TemplateResolver] para Thymeleaf. Este es el objeto que se carga a partir de un nombre de vista proporcionado por una acción, para encontrar el archivo correspondiente;
- las líneas 18 y 19 establecen el prefijo y el sufijo que se deben agregar al nombre de la vista para encontrar el archivo. Así, si el nombre de la vista es [vue04], el archivo que se buscará será [classpath:/templates/vue04.xml]. [classpath:/templates] es una sintaxis de Spring que hace referencia a una carpeta [/templates] ubicada en la raíz del Classpath del proyecto;
- línea 21: para que en la respuesta enviada al cliente aparezca el encabezado HTTP:
Content-Type:text/html;charset=UTF-8
- línea 20: indica que la vista cumple con la norma HTML5;
- línea 22: indica que las vistas de Thymeleaf se pueden almacenar en caché;
- líneas 26-31: establece el motor de resolución de vistas de la combinación Spring/Thymeleaf con el motor de resolución anterior;
Ejecutemos el ejecutable de este nuevo proyecto y solicitemos el URL [http://localhost:8080/v04.html?lang=en]:
![]() |
Notamos que en el URL, la acción [/v04] pudo ser reemplazada nuevamente por [v04.html].
5.5. [/v05]: factorización de un objeto en una vista de Thymeleaf
Creamos la siguiente acción [/v05]:
// creación del modelo M de una vista V - 2
@RequestMapping(value = "/v05", method = RequestMethod.GET)
public String v05(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-05";
}
Es idéntica a la acción [/v04]. La vista [vue-05.xml] es la siguiente:
![]() |
<!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>
- líneas 8-17: dentro de estas líneas se define un objeto Thymeleaf mediante el atributo [th:object="${personne}"] (línea 8). Este objeto es, en este caso, el objeto de clave [personne] que se encuentra en la plantilla:
- línea 11: la expresión de Thymeleaf [*{nom}] es equivalente a [${objet.nom}], donde [objet] es el objeto Thymeleaf actual. Por lo tanto, aquí la expresión [*{nom}] es equivalente a [${personne.nom}];
- línea 15: lo mismo;
El resultado:
![]() |
5.6. [/v06]: las pruebas en una vista de Thymeleaf
Consideremos la siguiente acción [/v06]:
// creación del modelo M de una vista V - 3
@RequestMapping(value = "/v06", method = RequestMethod.GET)
public String v06(Model model) {
model.addAttribute("personne", new Personne(7, "martin", 17));
return "vue-06";
}
Es idéntica a las dos acciones anteriores. Muestra la siguiente 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>
- línea 17: el atributo [th:if] evalúa una expresión booleana. Si esta expresión es verdadera, se muestra la etiqueta; de lo contrario, no se muestra. Por lo tanto, aquí si ${personne.age}>=18, se mostrará el texto [#{personne.majeure}], es decir, el mensaje con clave [personne.majeure] en los archivos de mensajes;
- línea 18: no se puede escribir [*{age} < 18] porque el signo < es un carácter reservado. Por lo tanto, hay que utilizar su equivalente HTML [<], también conocido como entidad HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];
Se modifican los archivos de mensajes:
[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
El resultado es el siguiente:
![]() | ![]() |
5.7. [/v07]: iteración en una vista de Thymeleaf
Consideremos la siguiente acción [/v07]:
// creación del modelo M de una vista 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";
}
- la acción crea una lista de tres personas, la coloca en la plantilla asociada a la clave [liste] y muestra la vista [vue-07];
La vista [vue-07.xml] es la siguiente:
<!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>
- línea 10: el atributo [th:each] repite la etiqueta en la que se encuentra, en este caso una etiqueta <li>. Aquí tiene dos parámetros: [element : collection], donde [collection] es una colección de objetos, en este caso una lista de personas. Thymeleaf recorrerá la colección y generará tantas etiquetas <li> como elementos haya en la colección. Para cada etiqueta <li>, [element] representará el elemento de la colección asociado a la etiqueta. Para este elemento, se evaluará el atributo [th:text]. Su expresión es, en este caso, una concatenación de cadenas para obtener el resultado [id, nom, age];
- línea 8: se agrega la clave [liste.personnes] a los archivos de mensajes;
Este es el resultado:
![]() | ![]() |
5.8. [/v08-/v10]: @ModelAttribute
Volvemos a algo que vimos al estudiar las acciones: la función de la anotación [@ModelAttribute]. Agregamos la siguiente acción nueva:
// --------------- Enlace y ModelAttribute ----------------------------------
// si el parámetro es un objeto, se instancia y, si es necesario, se modifica mediante los parámetros de la consulta
// formará parte automáticamente del modelo de la vista con la clave [key]
// para el parámetro @ModelAttribute("xx"), la clave será igual a xx
// para el parámetro @ModelAttribute, la clave será igual al nombre de la clase del parámetro, comenzando con una letra minúscula
// si @ModelAttribute no está presente, entonces todo ocurre como si estuviera presente sin clave
// Cabe señalar que esta presencia automática en el modelo no se lleva a cabo si el parámetro no es un objeto
@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";
}
- línea 11: la anotación [@ModelAttribute("someone")] agregará automáticamente el objeto [Personne p] al modelo, asociado a la clave [someone];
- línea 12: para verificar el modelo;
- línea 13: muestra la vista [vue-08.xml];
La vista [vue-08.xml] es la siguiente:
<!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>
- línea 8: el objeto Thymeleaf se inicializa con el objeto de clave [someone];
El resultado es el siguiente:
![]() |
y en la consola aparece el siguiente registro:
Modèle={someone=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Consideremos ahora la siguiente acción [/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";
}
- línea 1: la presencia del parámetro [Personne p] colocará automáticamente a la persona [p] en la plantilla. Como no se especifica ninguna clave, la clave utilizada es el nombre de la clase con su primer carácter en minúscula. Por lo tanto, [Personne p] es equivalente a [@ModelAttribute("personne") Personne p];
La vista [vue.09.xml] es la siguiente:
<!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>
- línea 8: la clave de modelo utilizada es [personne];
Este es un resultado:
![]() |
y el registro en la consola del servidor:
Modèle={personne=[id=4, nom=x, age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
Ahora, consideremos la siguiente acción nueva [/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";
}
- líneas 1-4: definen un método que crea, en el modelo de cada solicitud, un elemento de clave [uneAutrePersonne] asociado al objeto [new Personne(24,"pauline",55)];
- líneas 6-10: la acción [/v10] no hace nada más que pasar el modelo que recibe a la vista [vue-10.xml]. Cabe señalar que el parámetro [Model model] solo es necesario para la instrucción de la línea 8. Sin él, es innecesario;
La vista [vue-10.xml] es la siguiente:
<!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>
El resultado es el siguiente:
![]() |
y el registro de la consola es el siguiente:
5.9. [/v11]: @SessionAttributes
Volvemos a algo que vimos al analizar las acciones: la función de la anotación [@SessionAttributes]. Agregamos la siguiente nueva acción [/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";
}
Tenemos algo similar a lo que acabamos de analizar. La diferencia radica en una anotación [@SessionAttributes] colocada en la propia clase:
@Controller
@SessionAttributes("jean")
public class ViewsController {
- línea 2: se indica que la clave [jean] del modelo debe colocarse en la sesión;
Por eso, en la línea 7 de la acción, se ha inyectado la sesión. En la línea 8, se muestra el valor de la sesión asociada a la clave [jean].
La vista [vue-11.xml] es la siguiente:
<!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>
Se muestran dos personas:
- líneas 8-21: la persona con clave [jean] en el modelo;
- líneas 23-36: la persona con clave [jean] en la sesión;
Los resultados son los siguientes:
![]() |
- en [1], la persona con clave [jean] en el modelo;
- en [2], la persona con clave [jean] en la sesión;
El registro de la consola es el siguiente:
Modèle={uneAutrePersonne=[id=24, nom=pauline, age=55], jean=[id=33, nom=jean, age=10]}, Session[jean]=null
En lo anterior, se observa que la clave [jean] no se encuentra en la sesión que recibe la acción. De ello se deduce que la clave [jean] se agregó a la sesión después de la ejecución de la acción y antes de que se mostrara la vista.
Ahora, consideremos el caso en el que una clave es referenciada tanto por [@ModelAttribute] como por [@SessionAttributes]. Creamos las siguientes dos acciones:
@RequestMapping(value = "/v12a", method = RequestMethod.GET)
@ResponseBody
public void v12a(HttpSession session) {
session.setAttribute("paul", new Personne(51, "paul", 33));
}
// caso en el que la clave de [@ModelAttribute] también es una clave de [@SessionAttributes]
// en este caso, el parámetro correspondiente se inicializa con el valor de la sesión
@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";
}
La acción [/v12a] solo sirve para colocar en la sesión el elemento ['paul',new Personne(51, "paul", 33)]. No hace nada más. El hecho de que esté etiquetada por [@ResponseBody] indica que es ella la que genera la respuesta al cliente. Como su tipo es [void], no se genera ninguna respuesta.
La acción [/v12b] admite como parámetro a [@ModelAttribute("paul") Personne p]. Si no se hace nada más, se instancia un objeto [Personne] y luego se inicializa con los parámetros de la solicitud, y este objeto no tiene nada que ver con el objeto clave [paul] que la acción [/v12a] colocó en la sesión. Vamos a agregar la clave [paul] a los atributos de sesión de la clase:
@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
- En la línea 2, ahora hay dos atributos de sesión;
Volvamos a los parámetros de la acción [/v12b]:
public String v12b(Model model, @ModelAttribute("paul") Personne p) {
Ahora, el objeto [Personne p] no se instanciará, sino que hará referencia al objeto clave [paul] en la sesión. A partir de ahí, el procedimiento sigue siendo el mismo. El objeto clave [paul] aparecerá, en particular, en el modelo de la vista que se mostrará. Esto es lo que queremos ver en la línea 11 de la acción [/v12b].
La vista [vue-12.xml] será la siguiente:
<!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>
- línea 8: se hace referencia a la clave [paul] del modelo de la vista;
Esto da el siguiente resultado (después de ejecutar la acción [/v12a], que coloca la clave [paul] en la sesión):
![]() |
El registro de la consola es el siguiente:
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}
La clave [paul] se ha insertado correctamente en el modelo con el valor asociado a la clave [paul] en la sesión.
5.10. [/v13]: generar un formulario de entrada
Ahora abordamos el ingreso de datos en los formularios y su validación. Creamos un primer formulario con la siguiente acción [/v13]:
// genera un formulario para ingresar los datos de una persona
@RequestMapping(value = "/v13", method = RequestMethod.GET)
public String v13() {
return "vue-13";
}
que simplemente muestra la siguiente vista [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>
Si colocamos esta vista en la carpeta [static] con el nombre [vue-13.html] y solicitamos la URL [http://localhost:8080/vue-13.html], obtenemos la siguiente página:
![]() |
- En la línea 8 del formulario, encontramos la etiqueta <form> con el atributo [th:action]. Este atributo será evaluado por Thymeleaf y su valor reemplazará el valor actual del atributo [action], que, por lo tanto, solo está ahí como decorativo. Aquí, el valor del atributo [th:action] será [/v14.html];
- en las líneas 17, 23 y 29, el valor del atributo [th:value] reemplazará al del atributo [value]. En este caso, ese valor será la cadena vacía;
Al solicitar el URL [/v13.html], se obtiene el siguiente resultado:
![]() |
Veamos el código fuente generado por 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>
En las líneas 9, 18, 24 y 30, se observa la evaluación de los atributos [th:action] y [th:value] realizada por Thymeleaf.
5.11. [/v14]: gestionar los valores enviados por un formulario
La acción [/v14] es la que recibe los valores enviados. Es la siguiente:
// procesa los valores del formulario
@RequestMapping(value = "/v14", method = RequestMethod.POST)
public String v14(Personne p) {
return "vue-14";
}
- línea 3: los valores enviados se encapsulan en un objeto [Personne p]. Sabemos que este objeto forma parte automáticamente del modelo M de la vista V que mostrará la acción, asociado a la clave [personne];
- línea 4: la vista que se muestra es la vista [vue-14.xml];
La vista [vue-14.xml] es la siguiente:
<!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>
- línea 9: se recupera en el modelo el objeto asociado a la clave [personne];
- líneas 12, 16 y 20: se muestran las características de este objeto;
Esto da el siguiente resultado:
![]() | ![]() |
5.12. [/v15-/v16]: validación de un modelo
Con el ejemplo anterior, veamos la siguiente secuencia:
![]() |
- en [1], se ingresan valores erróneos para los campos [id] y [age] de tipo [int];
- en [2], la respuesta del servidor nos indica que se han producido dos errores;
Vamos a utilizar el mismo formulario, pero en caso de errores de validación, redirigiremos a una página que indique dichos errores para que el usuario pueda corregirlos.
La acción [/v15] es la siguiente:
// ---------------------- visualización de un formulario
@RequestMapping(value = "/v15", method = RequestMethod.GET)
public String v15(SecuredPerson p) {
return "vue-15";
}
Recibe como parámetro un tipo [SecuredPerson] como se muestra a continuación:
![]() |
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;
// constructores
public SecuredPerson() {
}
public SecuredPerson(int id, String nom, int age) {
this.id=id;
this.nom = nom;
this.age = age;
}
// getter y setter
...
}
Los campos [id, nom, age] se han marcado con restricciones de validación. La vista [vue-15.xml] que muestra la acción [/v15] es la siguiente:
<!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>
- líneas 10-47: se recupera el objeto del modelo de la página asociado a la clave [securedPerson]. Al finalizar la acción GET, se obtiene un objeto con su valor de instanciación [id=0, nom=null, age=0];
- línea 17: el valor del campo [securedPerson.id];
- línea 20: la expresión [${#fields.hasErrors('id')}] permite determinar si hubo errores de validación en el campo [securedPerson.id]. Si es así, el atributo [th:errors="*{id}"] muestra el mensaje de error asociado;
- este escenario se repite en la línea 29 para el campo [nom] y en la línea 38 para el campo [age];
- línea 45: la expresión [${#fields.errors('*')}] se refiere al conjunto de errores en los campos del objeto [securedPerson]. Por lo tanto, es el conjunto de estos errores el que se mostrará en las líneas 44 a 46;
- línea 16: se observa que los valores del formulario se enviarán a la acción [/v16]. Esta es la siguiente:
// -------------------- validación de un modelo------------------
@RequestMapping(value = "/v16", method = RequestMethod.POST)
public String v16(@Valid SecuredPerson p, BindingResult result) {
// ¿Errores?
if (result.hasErrors()) {
return "vue-15";
} else {
return "vue-16";
}
}
- línea 3: la anotación [@Valid SecuredPerson p] obliga a validar los valores enviados;
- línea 5: comprueba si la plantilla de la acción es incorrecta o no;
- línea 6: si es incorrecto, se devuelve el formulario [vue-15.xml]. Como este muestra los mensajes de error, los veremos;
- línea 8: si el modelo de la acción se valida, entonces se muestra la siguiente 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>
A continuación se muestran algunos ejemplos de ejecución:
![]() | ![]() |
![]() | ![]() |
![]() |
![]() |
5.13. [/v17-/v18]: verificación de mensajes de error
Al ejecutar por primera vez la acción [/v15], se obtiene el siguiente resultado:
![]() |
Es posible que se prefiera un formulario vacío en lugar de ceros en los campos [Identifiant, Age]. Para lograrlo, modificamos la plantilla de la acción de la siguiente manera:
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;
// constructores
public StringSecuredPerson() {
}
public StringSecuredPerson(String id, String nom, String age) {
this.id = id;
this.nom = nom;
this.age = age;
}
// getter y setter
...
}
- líneas 12 y 19: los campos [id] y [age] se cambian al tipo [String];
- línea 11: se indica que el campo [id] debe ser un número de hasta cuatro dígitos, sin decimales;
- línea 18: lo mismo para el campo [age], que debe ser un número entero de hasta dos dígitos;
La acción [/v17] queda así:
// ---------------------- visualización de un formulario
@RequestMapping(value = "/v17", method = RequestMethod.GET)
public String v17(StringSecuredPerson p) {
return "vue-17";
}
La vista [vue-17.xml] que muestra la acción [/v17] es la siguiente:
<!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>
Los cambios se producen en las siguientes líneas:
- línea 10: ahora se trabaja con el objeto del modelo de clave [stringSecuredPerson];
- línea 20: se recorre la lista de errores del campo [id]. En la sintaxis [th:each="err,status : ${#fields.errors('id')}"], es la variable [err] la que recorre la lista. La variable [status] proporciona información sobre cada iteración. Es un objeto [index, count, size, current] en el que:
- índice: es el número del elemento actual,
- current: el valor de ese elemento actual,
- count, size: el tamaño de la lista recorrida;
- línea 20: solo se muestra el primer elemento de la lista [th:if="${status.index}==0"];
La acción [/v18], que procesa el POST de la acción [/v17], es la siguiente:
// -------------------- validación de un modelo------------------
@RequestMapping(value = "/v18", method = RequestMethod.POST)
public String v18(@Valid StringSecuredPerson p, BindingResult result) {
// ¿Errores?
if (result.hasErrors()) {
return "vue-17";
} else {
return "vue-18";
}
}
Los archivos de mensajes evolucionan de la siguiente manera:
[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
Veamos algunos ejemplos:
![]() |
![]() |
En [1], vemos que se han ejecutado los dos validadores del campo [age]:
@Range(min = 8, max = 14)
@Digits(fraction = 0, integer = 2)
private String age;
¿Existe un orden en los mensajes de error? Para el campo [age], parece que los validadores se ejecutaron en el orden de [Digits, Range]. Sin embargo, si se realizan varias consultas, se puede observar que este orden puede cambiar. Por lo tanto, no se puede confiar en el orden de los validadores. En [2], solo se muestra uno de los dos mensajes del campo [id]. En [3], se ven todos los mensajes de error.
5.14. [/v19-/v20]: uso de diferentes validadores
Consideremos el siguiente nuevo modelo de acción:
![]() |
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 y setters
...
}
Se mostrará mediante la siguiente acción [/v19]:
// ------------------ visualización de un formulario
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- línea 3: la acción recibe como parámetro un objeto [Form19 formulaire]. Si GET no recibe parámetros, este objeto se inicializará con los valores predeterminados de Java;
- línea 4: se muestra la vista [vue-19.xml]. Esta es la siguiente:
<!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 muestra la siguiente vista:
![]() |
La página presenta una tabla de tres columnas:
- columna 1: el validador del campo de entrada;
- columna 2: el campo de entrada;
- columna 3: los mensajes de error del campo de entrada;
Veamos, por ejemplo, el código de la vista [/v19.html] para el 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>
Aquí encontramos el código que acabamos de estudiar con los formularios de tipo [Personne]:
- línea 2: la primera columna: el nombre del validador probado;
- línea 4: el atributo Thymeleaf [th:field="*{hhmmss}] generará los atributos HTML, [id="hhmmss"] y [name="hhmmss"]. El atributo Thymeleaf [th:value="*{hhmmss}"] generará el atributo HTML [value="valeur de [form19.hhmmss]]";
- línea 7: si el valor ingresado para el campo [form19.hhmmss] es incorrecto, entonces la línea 7 muestra los mensajes de error asociados a este campo;
Los valores registrados se procesan mediante la siguiente acción [/v20]:
// ----------------- validación del modelo del formulario
@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 {
// redirección a [vue-19]
redirectAttributes.addFlashAttribute("form19", formulaire);
return "redirect:/v19.html";
}
}
- línea 3: los valores registrados llenarán los campos del objeto [Form19 formulaire] si son válidos;
- líneas 4-6: si los valores registrados no son válidos, se vuelve a mostrar el formulario [vue-19] con los mensajes de error;
- líneas 6-10: si los valores enviados son válidos, el objeto [Form19 formulaire] creado con estos valores se pone a disposición de la siguiente solicitud, en este caso la de redirección. Posteriormente se elimina;
- línea 9: se redirige al cliente a la acción [/v19.html]. Esta volverá a mostrar el formulario [vue-19], que contiene código como:
<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
El atributo [th:object="${form19}"] recuperará entonces el objeto asociado al atributo Flash [form19] y, de este modo, volverá a mostrar el formulario tal como se ingresó.
El código del formulario merece algunas explicaciones adicionales. Consideremos el siguiente 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>
Esto genera el siguiente 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>
En el 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>
los atributos de Thymeleaf de las líneas 1 y 3, [th:field="*{assertFalse}"], presentan un problema. Se ha señalado que este atributo genera los atributos HTML, [id=assertFalse] y [name=assertFalse]. La dificultad radica en que, al generarse esto en las líneas 1 y 3, tenemos dos atributos [name] idénticos y dos atributos [id] idénticos. Si bien esto es posible con el atributo [name], no lo es con el atributo [id]. Como se ve en el código HTML generado, Thymeleaf ha generado dos atributos [id] diferentes: [id=asserFalse1] y [id=assertFalse2]. Lo cual es algo positivo. El problema es que no conocemos estos identificadores y es posible que los necesitemos. Este es el caso de la etiqueta [label] de la línea 2. El atributo [for] de una etiqueta HTML [label] debe hacer referencia a un atributo [id], en este caso, el generado para la etiqueta [input] de la línea 1. La documentación de Thymeleaf indica que la expresión [${#ids.prev('assertFalse')}"] permite obtener el último atributo [id] generado para el campo [assertFalse].
Ahora veamos el código de la lista desplegable del formulario:
<select th:field="*{assertTrue}">
<option value="true">True</option>
<option value="false">False</option>
</select>
Este código genera el código HTML de una lista desplegable:
El valor enviado se registrará con el nombre [name="assertTrue"].
La vista [vue-19.xml] utiliza una hoja 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>
En la línea 4, la hoja de estilo utilizada debe colocarse en la carpeta [static] del proyecto:
![]() |
Su contenido es el siguiente:
@CHARSET "UTF-8";
.col1 {
background: lightblue;
}
.col2 {
background: Cornsilk;
}
.col3 {
background: #e2d31d;
}
.error {
color: red;
}
Ahora, revisemos las fechas:
@NotNull
@Future
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInFuture;
@NotNull
@Past
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;
Al revisar el tráfico de red en la herramienta de desarrollo de Chrome (Ctrl-Mayús-I), se observa que las fechas se envían en el formato (aaaa-mm-dd):
![]() |
Por eso se han marcado las fechas con el validador:
@DateTimeFormat(pattern = "yyyy-MM-dd")
que establece el formato esperado para el valor enviado de las fechas.
Para terminar, el archivo de mensajes en 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
Veamos algunos ejemplos de ejecución:
![]() |
![]() |
![]() |
Arriba, entre [1] y [2], parece que no pasó nada. Sin embargo, si observamos el tráfico de red (Ctrl-Mayús-I), vemos que hubo dos intercambios de datos con el servidor:
![]() |
- en [1], el POST inicial hacia [/v20];
- a [2]; la respuesta a esta acción es una redirección;
- en [3], la segunda solicitud, esta vez hacia [/v19];
A continuación, se ejecuta la acción [/v19]:
// ------------------ visualización de un formulario
@RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String v19(Form19 formulaire) {
return "vue-19";
}
- línea 3, el parámetro [Form19 formulaire] se inicializa con el atributo Flash de la clave [form19], que había sido creado por la acción anterior [/v19] y que era un objeto de tipo [Form19] con los siguientes valores: los valores enviados a la acción [/v19];
- línea 4: se mostrará la vista [vue-19.xml] con un objeto [Form19 formulaire] en su modelo, inicializado con los valores enviados. Por eso, el usuario ve el formulario tal como lo envió;
¿Por qué una redirección? ¿Por qué no se envió simplemente a la acción [/v19] mencionada anteriormente? Se habría obtenido el mismo resultado. Salvo por algunas diferencias:
- el navegador habría mostrado en su barra de direcciones [http://localhost:8080/v20.html] en lugar de [http://localhost:8080/v19.html] como lo hizo aquí, ya que muestra la última acción URL que se llamó;
- si el usuario actualiza la página (F5), el resultado es completamente diferente:
- en el caso de la redirección, el URL que se muestra es el [http://localhost:8080/v19.html] obtenido a partir de un GET. El navegador volverá a ejecutar este último comando y obtendrá entonces un formulario completamente nuevo (el atributo Flash solo se utiliza una vez),
- en el caso de que no haya redirección, el URL que se muestra es el [http://localhost:8080/v20.html] obtenido a partir de un POST. El navegador volverá a ejecutar este último comando y, por lo tanto, generará nuevamente un POST con los mismos valores enviados que antes. En este caso no tiene consecuencias, pero a menudo es indeseable y, por lo tanto, en general se prefiere la redirección;
5.15. [/v21-/v22]: manejo de botones de radio
Consideremos el siguiente componente 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" };
// métodos getter y setter
...
}
- línea 5: la clase [Listes] será un componente de Spring;
- líneas 8-10: listas que se utilizan para alimentar botones de opción, casillas de selección y menús desplegables;
En la clase de configuración [Config], se indica lo siguiente:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
- línea 2: el paquete [models], donde se encuentra el componente [Listes], será explorado correctamente por Spring;
Creamos las siguientes acciones nuevas:
// ------------------ formulario con botones de opción
@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";
}
- líneas 2-3: el componente [Listes] se inyecta en el controlador;
- línea 6: gestionamos un formulario de tipo [Form21] que describiremos a continuación. Cabe señalar que hemos especificado su clave [form] en el modelo de la vista. Recordemos que, por defecto, habría sido [form21];
- línea 7: inyectamos el componente [Listes] en el modelo. La vista lo necesitará;
- línea 8: se muestra la vista [vue-21.xml]. Esta vista mostrará el formulario [Form21] y los valores enviados se pasarán a la acción [/v22] de las líneas 12 a 15;
- líneas 12-15: la acción [/v22] simplemente redirige a la acción [/v21], colocando los valores enviados que recibió en un atributo Flash con la clave [form]. Es importante que esta clave sea la misma que la utilizada en la línea 6;
El modelo [Form21] es el siguiente:
![]() |
package istia.st.springmvc.models;
public class Form21 {
// valores enviados
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;
// métodos getter y setter
...
}
La vista [vue-21.xml] es la siguiente:
<!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>
- líneas 36-40: cabe destacar el uso del componente [Listes] incluido en el modelo, para generar los textos de las casillas de selección;
- la columna 3 permite conocer el valor registrado para un POST, o el valor inicial del formulario durante el GET inicial;
Este código muestra la siguiente página:
![]() |
que corresponde al siguiente 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>
Se observa que los valores enviados (atributos «name») se encuentran en los siguientes campos del modelo [Form21]:
private String marie = "non";
private String deplacement = "4";
Se invita al lector a realizar pruebas. Cabe destacar que es el atributo [value] de los botones de opción el que se envía.
![]() | ![]() |
5.16. [/v23-/v24]: administrar casillas de selección
Agregamos la siguiente acción nueva:
// ------------------ formulario con casillas de selección
@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";
}
- línea 3: seguimos utilizando la plantilla [Form21];
La vista [vue-23.xml] es la siguiente:
<!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>
- líneas 37-41: cabe destacar el uso del componente [Listes] para generar los textos de las casillas de selección;
Este código muestra la siguiente página:
![]() |
procedente del siguiente 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>
Cabe señalar que los valores enviados (atributos name) se registran en los siguientes campos de [Form21]:
private String[] couleurs;
private String[] bijoux;
Se trata de tablas porque, para cada campo, hay varias casillas de selección con el nombre del campo. Por lo tanto, es posible que lleguen varios valores enviados con el mismo nombre (atributo name del formulario). Por eso se necesita una tabla para recuperarlos.
Volvamos al código Thymeleaf de la columna 3 de la 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>
Los campos a los que se hace referencia en las líneas 2 y 14 son los siguientes:
private String strCouleurs;
private String strBijoux;
Se calculan mediante la acción [/v24], que gestiona la acción POST:
// mapeador 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";
}
Hay que recordar aquí que la biblioteca jackson / jSON se encuentra entre las dependencias del proyecto.
- línea 2: se crea un tipo [ObjectMapper] que permite serializar y deserializar objetos en jSON,
- línea 7: se serializa la tabla de colores a jSON. El resultado se coloca en el campo [strCouleurs];
- línea 8: se serializa en jSON la tabla de joyas. El resultado se coloca en el campo [strBijoux];
A continuación se muestra un ejemplo de ejecución:
![]() | ![]() |
Cabe destacar que lo que se envía es el atributo [value] de las casillas de selección.
5.17. [/25-/v26]: administrar listas
Agregamos la siguiente acción [/v25]:
// ------------------ formulario con listas
@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";
}
La vista [vue-25.xml] es la siguiente:
<!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>
- líneas 38-42: generación de una lista de opción múltiple en la que los nombres se toman del componente [Listes] que ya hemos utilizado;
La página que se muestra es la siguiente:
![]() |
generada por el siguiente 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>
- línea 44: se puede observar que Thymeleaf ha creado un campo oculto. No he entendido cuál es su función:
- los valores enviados (atributos value de las etiquetas option) se colocarán en los siguientes campos (atributos name) de [Form21]:
private int couleur2;
private int[] bijoux2;
- línea 38: la lista [bijoux2] es de opción múltiple. Por lo tanto, se pueden enviar varios valores asociados al nombre [bijoux2]. Para recuperarlos, el campo [bijoux2] debe ser una matriz. Cabe señalar que se trata de una matriz de números enteros. Esto es posible ya que los valores publicados pueden convertirse a este tipo;
Los valores se registran en la siguiente acción [/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";
}
Aquí no hay nada que no hayamos visto antes. A continuación, un ejemplo de ejecución:
![]() | ![]() |
5.18. [/v27]: configuración de los mensajes
Consideremos la siguiente acción [/v27]:
// ------------------ mensajes configurados
@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";
}
La acción simplemente inserta cuatro valores en la plantilla y muestra la siguiente vista [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>
- línea 8: un mensaje sin parámetros;
- línea 9: un mensaje con un parámetro [$param1] tomado de la plantilla;
- línea 10: un mensaje con dos parámetros [$param2, $param3] tomados de la plantilla;
- línea 11: un mensaje con un parámetro. Este parámetro es en sí mismo una clave de mensaje (presencia de #). La clave la proporciona [$param4];
El archivo de mensajes en francés es el siguiente:
[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 la presencia de parámetros en el mensaje, se utilizan los símbolos {0}, {1}, ...
La fusión de la plantilla creada por la acción [/v27] con la vista [vue-27] generará el siguiente 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>
lo que da como resultado la siguiente vista:
![]() |
El archivo de mensajes en inglés es el siguiente:
[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
La fusión de la plantilla creada por la acción [/v27] con la vista [vue-27] generará el siguiente 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>
lo que da como resultado la siguiente vista:
![]() |
Se observa que el último mensaje se ha internacionalizado de principio a fin, lo cual no ocurre con los dos anteriores.
5.19. Uso de una página maestra
En una aplicación web, es común que las vistas compartan ciertos elementos que se pueden agrupar en una página maestra. A continuación, un ejemplo:
![]() |
Arriba, tenemos dos páginas similares en las que el fragmento [1] ha sido reemplazado por el fragmento [2]. La vista corresponde a una página maestra que tiene tres fragmentos fijos [3-5] y un fragmento variable [6].
5.19.1. El proyecto
Estamos construyendo un proyecto [springmvc-masterpage] siguiendo el procedimiento del párrafo 5.1.
![]() |
El archivo [pom.xml] es el siguiente:
<?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/> <!-- búsqueda de elemento padre en el repositorio -->
</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>
Una de las dependencias que incluye este archivo es necesaria para la página maestra:
![]() |
Los paquetes [config] y [main] son idénticos a los que llevan los mismos nombres en el proyecto anterior.
5.19.2. La página maestra
![]() |
La página maestra es la siguiente 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>
- línea 2: la página maestra debe definir el espacio de nombres [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], del cual se utiliza un elemento en la línea 19;
- líneas 10-12: generan el campo [1] que se muestra a continuación. La etiqueta Thymeleaf [th:include] permite incluir en la vista actual un fragmento definido en otro archivo. Esto permite reutilizar los fragmentos utilizados en varias vistas;
- líneas 15-17: generan el área [2] que se muestra a continuación;
- líneas 19-20: generan el área [3] que se muestra a continuación. El atributo [layout:fragment] es un atributo del espacio de nombres [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Indica un área que, en tiempo de ejecución, puede ser reemplazada por otra;
- líneas 24-28: generan el campo [4] que se muestra a continuación;
![]() |
5.19.3. Los fragmentos
Los fragmentos [entete.xml], [menu.xml] y [basdepage.xml] son los siguientes:
[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>
El fragmento [page1.xml] es el siguiente:
<!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>
- línea 2: el atributo [layout:decorator="layout"] indica que la página actual [page1.xml] está «decorada», es decir, que pertenece a una página maestra. Esta es el valor del atributo, en este caso la vista [layout.xml];
- línea 3: se indica en qué fragmento de la página maestra se insertará [page1.xml]. El atributo [layout:fragment="contenu"] indica que [page1.xml] se insertará en el fragmento denominado [contenu], es decir, el área [3] de la página maestra;
- líneas 5-7: el contenido del fragmento es un formulario que ofrece un botón de POST hacia la acción [/page2.html];
El fragmento [page2.xml] es similar:
<!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. Las acciones
![]() |
El controlador [Layout.java] es el siguiente:
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";
}
}
- líneas 10-12: la acción [/page1] simplemente muestra la vista [page1.xml];
- líneas 15-17: lo mismo ocurre con la acción [/page2], que muestra la vista [page2.xml];













































































