Les applications Web avec Spring Web MVC¶
Le développement d’une application Spring Web MVC implique l’implémentation de classes représentant les contrôleurs. Dans le modèle MVC, un contrôleur gère les interactions entre l’utilisateur et le système. Dans le cadre d’une application Web, les interactions avec le serveur correspondent aux requêtes HTTP émises par le navigateur client.
Les contrôleurs Spring Web MVC sont dont là pour nous permettre de gérer les requêtes HTTP entrantes tout en nous apportant des facilités de développement pour le binding de données, la validation, la gestion du modèle…
Les contrôleurs¶
Un contrôleur est une classe Java portant l’annotation @Controller. Pour que le contrôleur soit appelé lors du traitement d’une requête, il suffit d’ajouter l’annotation @RequestMapping sur une méthode publique de la classe en précisant la méthode HTTP concernée (par défaut GET) et le chemin d’URI (à partir du contexte de déploiement de l’application) pris en charge par la méthode.
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class ItemController {
@RequestMapping(path="/", method=RequestMethod.GET)
public String getHome() {
// ...
return "index";
}
@RequestMapping(path="/item", method=RequestMethod.POST)
public String addItem() {
// ...
return "itemDetail";
}
}
Dans l’exemple ci-dessus, le contrôleur déclare la méthode getHome
qui traite
les requêtes pour le chemin /
et la méthode addItem
qui traite les
requêtes pour le chemin /item
. La première n’accepte que les requêtes de type
GET
et la seconde que les requêtes de type POST
.
Note
Il existe les annotations @GetMapping, @PutMapping, @PostMapping, @DeleteMapping, @PatchMapping qui fonctionnent comme l’annotation @RequestMapping sauf qu’il n’est pas nécessaire de préciser la méthode HTTP concernée puisqu’elle est mentionnée dans le nom de l’annotation :
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping(path="/")
public String getHome() {
// ...
return "index";
}
@PostMapping(path="/item")
public String addItem() {
// ...
return "itemDetail";
}
}
Note
L’annotation @RequestMapping peut être utilisée directement sur la déclaration de classe pour donner des informations applicables par défaut pour l’ensemble des méthodes de cette classe. Si on donne un chemin, alors ce dernier s’ajoute avant celui déclaré par une méthode.
@Controller
@RequestMapping(path="/admin")
public class ItemController {
@PostMapping(path="/item")
public String addItem() {
// ...
return "itemDetail";
}
}
La méhode addItem
ci-dessus traite les requêtes pour le chemin
/admin/item
.
Notez que toutes les méthodes de contrôleur dans nos exemple retournent une chaîne de caractères. Il s’agit de l’identifiant de la vue à afficher en réponse à l’utilisateur, c’est-à-dire de la page HTML à retourner au navigateur du client. La plupart du temps, la page HTML de réponse va être générée à la volée par un moteur de rendu. Spring Web MVC nous donne la possibilité d’utiliser différentes technologies pour le rendu de la page HTML : JSP, Thymeleaf, Freemarker… Vous allez devoir configurer votre application en fonction de la technologie que vous souhaitez utiliser.
Les vues JSP¶
Pour écrire des vues sous la forme de JSP, nous devons configurer la façon dont Spring va pouvoir trouver la page JSP en fonction de l’identifiant retourné par un contrôleur. Il faut ensuite déployer l’application dans un serveur Java qui embarque un moteur de rendu JSP.
Pour une application avec Spring Boot¶
Vous devez vous assurer que votre application Spring Boot utilise bien un
packaging War. Si vous utilisez Maven pour gérer votre projet, il vous suffit
de changer ou d’ajouter la balise <packaging>
dans votre fichier
pom.xml
pour indiquer un packaging de type war
:
<groupId>dev.gayerie</groupId>
<artifactId>monapplication</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
Vous devez ensuite vous assurer que vous disposez des bonnes dépendances.
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
Vous devez enfin configurer le gestionnaire de vues de Spring Web MVC
en ajoutant les propriétés suivantes dans le fichier application.properties
:
spring.mvc.view.prefix = /WEB-INF/jsp/
spring.mvc.view.suffix = .jsp
Cela signifie que pour trouver la vue, le gestionnaire de vue doit utiliser
l’identifiant fourni par le contrôleur et ajouter /WEB-INF/jsp/
au début
et .jsp
à a fin pour en déduire le chemin de la JSP.
Ainsi, si un contrôleur retourne l’identifiant de vue "index"
, cela
désigne la JSP /WEB-INF/jsp/index.jsp
. Dans votre projet Maven, cela
implique que vous placiez vos fichiers JSP dans le répertoire
src/main/webapp/WEB-INF/jsp
(créez les répertoires manquants si nécessaire).
Pour une application sans Spring Boot¶
Vous devez vous assurer que votre application utilise bien un packaging War.
Si vous utilisez Maven pour gérer votre projet, il vous suffit de changer ou
d’ajouter la balise <packaging>
dans votre fichier pom.xml
pour
indiquer un packaging de type war
:
<groupId>dev.gayerie</groupId>
<artifactId>monapplication</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
Vous devez ensuite vous assurer que vous disposez des bonnes dépendances.
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
Les vues sont gérées par un composant Spring de type ViewResolver qu’il va falloir configurer par programmation. Nous avons vu en introduction à Spring MVC que l’on ajoute une classe implémentant l’interface WebMvcConfigurer. Nous allons pouvoir implémenter la méthode configureViewResolvers de cette interface pour enregistrer le support pour les JSP.
package dev.gayerie;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebMvc
@Configuration
@ComponentScan
public class WebAppConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp().prefix("/WEB-INF/jsp/").suffix(".jsp");
}
}
Utilisation des vues JSP¶
Nous pouvons créer un contrôleur AccueilController
qui possède une
méthode pour les requêtes GET
sur la racine de l’application :
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AccueilController {
@GetMapping(path = "/")
public String accueillir() {
return "accueil";
}
}
La méthode accueillir
se contente de retourner un identifiant de vue
sans réaliser d’autre traitement. Il doit donc exister un fichier JSP
src/main/webapp/WEB-INF/accueil.jsp
dans le projet.
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Bienvenue !
</body>
</html>
Les vues Thymeleaf¶
Note
Si vous ne connaissez pas le moteur de rendu Thymeleaf, vous pouvez vous reporter au chapitre d’introduction à Thymeleaf de ce support.
Nous allons voir comment rendre des pages HTML en utilisant le moteur de rendu Thymeleaf. D’abord, nous devons configurer la façon dont Spring Web MVC va pouvoir trouver les modèles de page HTML en fonction de l’identifiant retourné par un contrôleur.
Pour une application avec Spring Boot¶
Pour une application utilisant Spring Boot, la configuration est très simple.
Il vous suffit d’ajouter une dépendance dans votre projet au module
spring-boot-starter-thymeleaf
. Si vous utilisez Maven, il vous faut
ajouter dans votre fichier pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Par défaut, les modèles Thymeleaf doivent être placés dans le répertoire
src/main/resources/templates
de votre projet. Les fichiers statiques
(non transformés par Thymeleaf) doivent être placés dans le répertoire
src/main/resources/static
de votre projet.
Note
Si vous voulez changer les répertoires par défaut, vous pouvez utiliser
les propriétés spring.thymeleaf.prefix
et spring.web.resources.static-locations
dans le fichier application.properties
.
Il existe de nombreuses autres propriétés qui sont documentées dans la section Templating Properties de la documentation Spring Boot pour paramétrer l’intégration de Thymeleaf.
Pour une application sans Spring Boot¶
L’intégration de Thymeleaf dans une application sans Spring Boot est plus compliquée car il est nécessaire de créer la configuration du moteur du rendu. Si vous voulez vraiment faire cette intégration, alors vous pouvez vous reporter à la documentation officielle de Thymeleaf.
Utilisation des vues Thymeleaf¶
Nous pouvons créer un contrôleur AccueilController
qui possède une
méthode pour les requêtes GET
sur la racine de l’application :
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AccueilController {
@GetMapping(path = "/")
public String accueillir() {
return "accueil";
}
}
La méthode accueillir
se contente de retourner un identifiant de vue
sans réaliser d’autre traitement. Il doit donc exister un fichier HTML
src/main/resources/templates/accueil.html
dans le projet.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Bienvenue !
</body>
</html>
Le modèle¶
Dans une approche MVC, le modèle correspond à l’ensemble des données qui sont nécessaires à la construction de la vue. Pour une application Web, il s’agit donc des données nécessaires pour la construction de la page HTML de réponse. Le contrôleur a la charge de mettre toutes ces données à la disposition de la vue.
Spring Web MVC représente la notion de modèle avec l’interface Model. Cette interface va permettre à un contrôleur d’ajouter des attributs au modèle, c’est-à-dire des objets, en les associant chacun à un nom unique. Ainsi, une vue pourra directement avoir accès à ces objets par leur nom.
Pour obtenir une instance d’un Model, il suffit de l’ajouter comme paramètre à une méthode de contrôleur.
Imaginons un exemple très simple : nous souhaitons afficher l’heure courante du serveur dans une page HTML. Le modèle est simplement constitué d’un objet Java de type Date.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package dev.gayerie;
import java.util.Date;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class DateController {
@GetMapping(path = "/date")
public String afficherDate(Model model) {
model.addAttribute("now", new Date());
return "affichageDate";
}
}
|
La méthode afficherDate
du contrôleur attend en paramètre un objet de type
Model qui sera fourni par Spring Web MVC à l’appel. À la ligne 13, on ajoute
dans le modèle une nouvelle instance de la classe Date sous le nom now
.
Le contrôleur retourne ensuite l’identifiant de la vue affichageDate
. Cette
vue va pouvoir directement faire référence à l’attribut now
du modèle.
1 2 3 4 5 6 7 8 9 10 11 | <%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p><fmt:formatDate value="${now}" pattern="dd MMMM yyyy HH:mm:ss"/></p>
</body>
</html>
|
Ou bien si vous utilisez Thymeleaf :
1 2 3 4 5 6 7 8 9 | <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p data-th-text="${#dates.format(now, 'dd MMMM yyyy HH:mm:ss')}">la date ici</p>
</body>
</html>
|
Une application multi-couches (N-Tiers)¶
Dans le développement d’application Web, on insiste généralement sur la nécessité de réaliser une application en couches (le modèle N-Tiers en anglais). La plupart des applications utilisent un modèle en 3 couches pour distinguer :
- La couche présentation
Il s’agit de l’ensemble des classes qui permettent d’assurer l’interaction avec l’utilisateur. Pour une application Web, on l’appelle plus souvent la couche Web. Tous les contrôleurs que nous implémentons et tous les modèles de vues font partie de la couche présentation.
- La couche métier (ou la couche service)
Il s’agit de l’ensemble des classes qui permettent de réaliser les fonctionnalités de l’application.
- La couche d’accès au données (ou couche de données)
Il s’agit de l’ensemble des classes qui permettent d’interagir avec le système de données de l’entreprise. Pour des applications simples, il s’agit de la couche responsable de l’interaction avec une base de données.
Ces trois couches permettent d’isoler plus facilement chaque partie de l’application. Il existe une dépendance stricte des couches entre-elles. La couche présentation dépend de la couche métier pour réaliser les traitements et la couche métier dépend de la couche de données pour avoir accès aux données et les modifier. Afin de conserver les responsabilités de chaque couche, il n’est pas toléré d’autres dépendances.
Pour une application développée avec Spring Web MVC, les contrôleurs se situent dans la couche présentation. Ils ne sont donc pas responsables de réaliser les traitements de l’application.
En fait, nous avons vu que le Spring Framework fournit des stéréotypes @Service et @Repository précisément pour identifier les beans de la couche métier (pour le premier) et de la couche d’accès aux données (pour le second). Il est très facile de construire une application multi-couches avec Spring Web MVC, il suffit d’isoler les traitements de l’application dans des services que nous pouvons injecter dans nos contrôleurs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package dev.gayerie.controller;
import dev.gayerie.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping("/items")
public String getAllItems(Model model) {
model.addAttribute("items", itemService.getAllItems());
return "listeItems";
}
}
|
Dans notre exemple précédent, on voit que la responsabilité du contrôleur est limitée au minimum. Il doit fournir les données à la vue en les plaçant dans le modèle (ligne 17). Il doit fournir l’identifiant de la vue à afficher (ligne 18). Pour récupérer les données, le contrôleur s’adresse au service qui est injecté comme attribut du contrôleur (lignes 12 et 13).
Note
L’utilisation des packages est une bonne façon de marquer la séparation des couches dans l’application. Dans l’exemple précédent, le contrôleur est déclaré dans le package dev.gayerie.controller et le service dans le package dev.gayerie.service. On peut faire de même pour la couche d’accès aux données en créant un package dev.gayerie.repository.
La signature des méthodes de contrôleur¶
Spring Web MVC autorise une très grande diversité de signatures pour les méthodes d’un contrôleur gérant les requêtes HTTP (appelées handler methods). Nous sommes libres dans le choix du nom de la méthode. Il existe également un large choix concernant le type et le nombre des paramètres ainsi que le type de la valeur de retour de la méthode.
Les paramètres¶
Pour la liste complète des types de paramètre supportés, reportez-vous à la documentation officielle
À titre d’exemple, une méthode gérant les requêtes HTTP peut accepter en paramètres :
La valeur d’un paramètre de la requête HTTP grâce à l’annotation @RequestParam.
package dev.gayerie; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class ItemController { @PostMapping("/item") public String addItem(@RequestParam("itemName") String name) { // ... return "itemDetail"; } }
Note
Depuis Java 8, si le nom du paramètre de la requête est identique au nom du paramètre de la méthode, il n’est pas utile de préciser le nom du paramètre de la requête entre parenthèses :
@PostMapping("/item") public String addItem(@RequestParam String itemName) { // ... return "itemDetail"; }
La valeur d’un en-tête de la requête HTTP grâce à l’annotation @RequestHeader
package dev.gayerie; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; @Controller public class ItemController { @PostMapping("/item") public String addItem(@RequestHeader("host") String host) { // ... return "itemDetail"; } }
Note
Depuis Java 8, si le nom de l’en-tête de la requête est identique au nom du paramètre de la méthode, il n’est pas utile de préciser le nom de l’en-tête de la requête entre parenthèses :
@PostMapping("/item") public String addItem(@RequestHeader String host) { // ... return "itemDetail"; }
Une valeur dans le chemin de la ressource grâce à l’annotation @PathVariable. On utilise des accolades dans la déclaration du chemin pour indiquer la partie dynamique dont on recevra la valeur en paramètre.
package dev.gayerie; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PathVariable; @Controller public class ItemController { @PostMapping("{subpath}/item") public String addItem(@PathVariable("subpath") String subpath) { // ... return "itemDetail"; } }
Note
Depuis Java 8, si le nom de la variable de chemin est identique au nom du paramètre de la méthode, il n’est pas utile de préciser le nom de la variable entre parenthèses :
@PostMapping("{subpath}/item") public String addItem(@PathVariable String subpath) { // ... return "itemDetail"; }
La valeur d’un attribut de requête ou de session grâce aux annotations @RequestAttribute et @SessionAttribute :
package dev.gayerie; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.SessionAttribute; @Controller public class ItemController { @PostMapping("/item") public String addItem(@SessionAttribute("basket") Basket basket) { // ... return "itemDetail"; } }
Note
Depuis Java 8, si le nom de l’attribut est identique au nom du paramètre de la méthode, il n’est pas utile de préciser le nom de l’attribut entre parenthèses :
@PostMapping("/item") public String addItem(@SessionAttribute Basket basket) { // ... return "itemDetail"; }
Toutes les annotations ci-dessus acceptent l’attribut required
qui permet d’indiquer
si le paramètre doit être renseigné. Si l’attribut required
vaut true
(la
valeur par défaut) alors l’absence de valeur entraîne une erreur de type 400
(bad request) et la méthode du contrôleur n’est pas appelée.
Note
Depuis Java 8, il est possible d’utiliser Optional comme type de paramètre afin de préciser que le paramètre est optionnel (sans avoir à se préoccuper de la valeur de l’attribut required).
@PostMapping("/item")
public String addItem(@SessionAttribute Optional<Basket> basket) {
if (basket.isPresent()) {
// ...
}
return "itemDetail";
}
Il est également possible d’attendre en paramètre un objet Java présent dans le modèle grâce à l’annotation @ModelAttribute. Si aucune instance n’existe, l’objet sera automatiquement instancié. De plus, les propriétés de cet objet seront préremplies avec la valeur des paramètres de la requête portant le même nom.
Par exemple, si on déclare la classe Item :
package dev.gayerie;
public class Item {
private String name;
private String code;
private int quantity;
// Getters/setters omis
}
On peut déclarer une méthode dans un contrôleur qui prend en paramètre un objet
de type Item
.
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@PostMapping("/item")
public String addItem(@ModelAttribute Item item) {
// ...
return "itemDetail";
}
}
Lors du traitement de la requête POST sur /item
, un objet de type Item
sera créé avec ses propriétés renseignés avec la valeur des paramètres HTTP
name, code et quantity.
Note
L’annotation @ModelAttribute peut être omise car c’est l’interprétation par défaut d’un paramètre d’une méthode d’un contrôleur.
Enfin, Spring Web MVC reconnaît certains types particuliers pour les paramètres. C’est notamment le cas du type Model que nous avons vu précédemment.
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class MessageController {
@PostMapping("/message")
public String display(Model model) {
model.addAttribute("message", "Hello world");
return "showMessage";
}
}
La valeur de retour¶
Nous avons vu qu’une méthode de gestion de requête d’un contrôleur peut retourner un objet de type String. En effet, cet objet est généralement utilisé pour identifier la vue. Il existe cependant plusieurs types de retour autorisés par Spring Web MVC
Si la méthode retourne une chaîne de caractères alors elle est permet au ViewResolver configurer de déduire la vue qui doit être appelée. C’est ce type de valeur retour que nous avons utilisé depuis le début de ce chapitre.
Si la méthode retourne
void
ounull
alors la méthode est supposée avoir correctement traitée la requête et aucune vue ne sera appelée.Si la méthode retourne un objet de type ModelAndView alors il est utilisé pour déduire l’identifiant de la vue et les données du modèle.
Si la méthode est annotée par @ResponseBody, cela signifie que l’objet retourné constitue la réponse. Il est possible d’utiliser un convertisseur pour le transformer, par exemple, en réponse JSON.
Pour la liste complète des types de valeur de retour supportés, reportez-vous à la documentation officielle.
Taglib JSP et gestion des formulaires¶
Spring Web MVC fournit ses propres bibliothèques de tag (taglibs). Elles offrent des fonctionnalités supplémentaires par rapport aux JSTL et vont notamment nous permettre de gérer plus facilement les formulaires HTML.
Spring’s JSP taglib¶
La première taglib, appelée simplement Spring’s JSP taglib, apporte des
fonctionnalités similaires aux JSTL tout en y ajoutant des évolutions ou des
spécificités propres à Spring Web MVC. Vous pouvez vous reporter à la
documentation officielle
pour voir la liste des tags supportés.
Cette taglib doit être déclarée dans une JSP avec la directive taglib
:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
Par exemple, la balise <spring:url/> offre des fonctionnalités supplémentaires par rapport à son équivalent dans les JSTL. Il est possible de passer des paramètres et de demander un échappement des caractères pour une utilisation dans du code JavaScript :
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<spring:url value="/destination" javaScriptEscape="true" var="destinationUrl">
<spring:param name="name" value="${name}"/>
</spring:url>
<script>
var url = "${destinationUrl}";
alert(url);
</script>
</head>
<body>
</body>
</html>
S’il existe dans le modèle un attribut name avec la valeur julie, alors la JSP ci-dessus produira le code HTML suivant :
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
var url = "\/sailorapp\/destination?name=julie";
alert(url);
</script>
</head>
<body>
</body>
</html>
Form taglib¶
La seconde taglib, appelée form taglib, permet de créer un formulaire HTML lié à un objet présent dans le modèle. Chaque champ du formulaire sera initialisé avec la valeur d’une propriété de cet objet. Puis, lorsque l’utilisateur soumettra les données du formulaire au serveur, elles serviront à remplir les propriétés d’un objet du même type qui sera reçu en paramètre d’une méthode d’un contrôleur.
L’opération qui associe des données envoyées par l’utilisateur avec un objet du modèle est appelée un binding. Cela permet au développeur de l’application de traiter les données d’un formulaire sous la forme d’un objet.
La form taglib doit être déclarée dans une JSP avec la directive taglib
:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
Cette taglib fournit le tag <form:form> ainsi que des tags pour tous les éléments d’un formulaire : <form:input>, <form:checkbox>, <form:button>… Vous pouvez vous reporter à la documentation officielle pour voir la liste complète des tags supportés.
Supposons que nous ayons une classe Item
qui représente les données
qu’un utilisateur peut saisir dans un formulaire pour créer un item.
1 2 3 4 5 6 7 8 9 10 11 | package dev.gayerie;
public class Item {
private String name;
private String code;
private int quantity;
// Getters/setters omis
}
|
Nous pouvons créer la vue suivante pour fournir un formulaire qui permet de saisir les informations d’un item :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form:form servletRelativeAction="/item" modelAttribute="item" acceptCharset="utf-8">
<p><label>Code : </label><form:input path="code"/></p>
<p><label>Nom : </label><form:input path="name"/></p>
<p><label>Quantité : </label><form:input path="quantity"/></p>
<button type="submit">Envoyer</button>
</form:form>
</body>
</html>
|
La vue ci-dessus utilise la form taglib pour créer un formulaire. La balise
<form:form> possède l’attribut servletRelativeAction
pour fournir
le chemin qui sera utilisé pour définir l’action du formulaire. La balise
<form:form> possède également l’attribut modelAttribute
qui nous
permet de préciser qu’il doit exister dans le modèle un objet qui s’appelle
item
(et dans notre cas qui doit être une instance de Item
). Cet
objet va être utilisé pour remplir les données du formulaire. La balise
<form:input> fournit l’attribut path
qui déclare le chemin d’accès
dans l’objet item
à la donnée associée à ce champ. Par exemple, à la
ligne 11, le chemin code
signifie que le champ input doit être associé
à la propriété code
de l’objet item
. Ainsi Spring Web MVC va
utiliser la méthode Item.getCode()
pour connaître la valeur du champ.
Pour que ce formulaire fonctionne correctement, il faut lui associer deux méthodes de contrôleur. Une première méthode correspondant à l’affichage du formulaire qui pourra éventuellement positionner des valeurs par défaut dans les champs. Et une seconde méthode correspondant au traitement du formulaire quand l’utilisateur soumettra les données au serveur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping(path = "/item")
public String displayForm(@ModelAttribute Item item) {
// TODO initialiser le bean de formulaire si nécessaire
return "itemForm";
}
@PostMapping(path = "/item")
public String processForm(@ModelAttribute Item item) {
// TODO traiter le formulaire
return "successProcessItem";
}
}
|
La méthode displayForm
attend en paramètre un objet de type Item
.
Comme ce paramètre est annoté avec @ModelAttribute, Spring Web MVC
créera un objet de ce type pour l’ajouter comme attribut du modèle
sous le nom item
avant de le passer en paramètre de l’appel.
Note
Si vous souhaitez garder le contrôle sur la création de l’objet du formulaire, votre méthode de contrôleur peut ajouter elle-même cet objet au modèle à partir d’une instance de Model reçue en paramètre :
@GetMapping(path = "/item")
public String displayForm(Model model) {
Item item = new Item();
model.addAttribute("item", item);
return "itemForm";
}
Dans la vue, la balise <form:form> utilisera cet objet du modèle pour remplir les champs du formulaire. Le contrôleur a donc l’opportunité de préciser la valeur par défaut des champs :
@GetMapping(path = "/item")
public String displayForm(@ModelAttribute Item item) {
item.setCode("0001");
item.setName("un objet");
item.setQuantity(100);
return "itemForm";
}
La méthode processForm
attend une requête POST
est permettra de traiter
les données envoyées par l’utilisateur. Elle attend également en paramètre un
objet de type Item
. Cet objet sera automatiquement créé et ses champs
seront remplis à partir des données envoyées par le formulaire.
On voit qu’il est plus simple de représenter les données d’un formulaire
sous la forme d’un objet. Lorsqu’on crée spécifiquement une classe pour
représenter ces données, on appelle généralement ce type de classes un
DTO (pour Data Transfer Object). Comme son nom l’indique, un DTO a
pour fonction dans le système de représenter le transfert de données entre
deux composantes (dans notre exemple, entre la page Web et le contrôleur).
Nous pourrions renommer la classe Item
en ItemDto
pour indiquer
clairement au lecteur la responsabilité de cette classe dans le système.
Note
Reportez-vous à la documentation officielle pour des exemples plus détaillés de formulaires avec Spring Web MVC.
Gestion des formulaires avec Thymeleaf¶
Thymeleaf apporte une simplification dans la gestion des formulaires. Il permet de créer un formulaire HTML lié à un objet présent dans le modèle. Chaque champ du formulaire sera initialisé avec la valeur d’une propriété de cet objet. Puis, lorsque l’utilisateur soumettra les données du formulaire au serveur, elles serviront à remplir les propriétés d’un objet du même type qui sera reçu par le contrôleur. L’opération qui associe des données envoyées par l’utilisateur avec un objet du modèle est appelée un binding. Cela permet au développeur de l’application de traiter les données d’un formulaire sous la forme d’un objet.
Supposons que nous ayons une classe Item
qui représente les données
qu’un utilisateur peut saisir dans un formulaire pour créer un item.
1 2 3 4 5 6 7 8 9 10 11 | package dev.gayerie;
public class Item {
private String name;
private String code;
private int quantity;
// Getters/setters omis
}
|
Nous pouvons créer la vue suivante pour fournir un formulaire qui permet de saisir les informations d’un item :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form action="#" method="post" data-th-action="@{/item}" data-th-object="${item}" accept-charset="utf-8">
<p><label>Code : </label><input type="text" data-th-field="*{code}"></p>
<p><label>Nom : </label><input type="text" data-th-field="*{name}"></p>
<p><label>Quantité : </label><input type="number" data-th-field="*{quantity}"></p>
<button type="submit">Envoyer</button>
</form>
</body>
</html>
|
La vue ci-dessus crée un formulaire et l’attribut Thymeleaf object
permet
de sélectionner l’objet item
du modèle. Pour chaque balise
<input>
, on utilise l’attribut Thymeleaf field
pour lui spécifier
le chemin d’accès à la valeur du champ. La syntaxe *{}
signale que l’expression
est relative à l’objet item
. Ainsi, si la propriété code
de l’objet
nommé item
vaut "0001"
, alors la balise :
<input type="text" data-th-field="*{code}">
sera transformée par le moteur Thymeleaf pour produire :
<input type="text" id="code" name="code" value="0001">
Pour que ce formulaire fonctionne correctement, il faut lui associer deux méthodes de contrôleur. Une première méthode correspondant à l’affichage du formulaire qui pourra éventuellement positionner des valeurs par défaut dans les champs. Et une seconde méthode correspondant au traitement du formulaire quand l’utilisateur soumettra les données au serveur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping(path = "/item")
public String displayForm(@ModelAttribute Item item) {
// TODO initialiser le bean de formulaire si nécessaire
return "itemForm";
}
@PostMapping(path = "/item")
public String processForm(@ModelAttribute Item item) {
// TODO traiter le formulaire
return "successProcessItem";
}
}
|
La méthode displayForm
attend en paramètre un objet de type Item
.
Comme ce paramètre est annoté avec @ModelAttribute, Spring Web MVC
créera un objet de ce type pour l’ajouter comme attribut du modèle
sous le nom item
avant de le passer en paramètre de l’appel.
Note
Si vous souhaitez garder le contrôle sur la création de l’objet du formulaire, votre méthode de contrôleur peut ajouter elle-même cet objet au modèle à partir d’une instance de Model reçue en paramètre :
@GetMapping(path = "/item")
public String displayForm(Model model) {
Item item = new Item();
model.addAttribute("item", item);
return "itemForm";
}
Dans la vue, cet objet du modèle sera utilisé pour remplir les champs du formulaire. Le contrôleur a donc l’opportunité de préciser la valeur par défaut des champs :
@GetMapping(path = "/item")
public String displayForm(@ModelAttribute Item item) {
item.setCode("0001");
item.setName("un objet");
item.setQuantity(100);
return "itemForm";
}
La méthode processForm
attend une requête POST
est permettra de traiter
les données envoyées par l’utilisateur. Elle attend en paramètre un
objet de type Item
. Cet objet sera automatiquement créé et ses propriétés
seront remplies à partir des données envoyées par le formulaire.
On voit qu’il est plus simple de représenter les données d’un formulaire
sous la forme d’un objet. Lorsqu’on crée spécifiquement une classe pour
représenter ces données, on appelle généralement ce type de classes un
DTO (pour Data Transfer Object). Comme son nom l’indique, un DTO a
pour fonction dans le système de représenter le transfert de données entre
deux composantes (dans notre exemple, entre la page Web et le contrôleur).
Nous pourrions renommer la classe Item
en ItemDto
pour indiquer
clairement au lecteur la responsabilité de cette classe dans le système.
Externalisation des messages¶
L’externalisation des messages permet de placer des chaînes de caractères dans un fichier externe afin de les référencer par un code. Cette externalisation a au moins deux intérêts dans une application Web :
Cela évite d’écrire directement les messages en dur dans le code, notamment les messages d’erreur ou d’information que l’on veut voir afficher dynamiquement dans une vue.
Cela facilite l’internationalisation (I18N) d’un site Web. Tous les textes sont remplacés par des codes dans les vues et les textes sont insérés dynamiquement en fonction de la langue du navigateur. Pour chaque langue que l’on souhaite gérer, il suffit de fournir un fichier associant les codes aux textes dans la langue souhaitée. Ce processus s’appelle la localisation (L10N).
Java fournit dans son API standard la classe ResourceBundle qui est faite pour réaliser cette externalisation. On utilise un ou plusieurs fichiers de propriétés pour stocker les chaînes de caractères. Si on souhaite supporter plusieurs langues, il suffit de créer un fichier par langue et d’ajouter le code de la langue comme suffixe du nom du fichier.
Nous pouvons avoir un fichier par défaut nommé messages.properties
correspondant à la langue par défaut. Par exemple le français :
welcome.title = Bienvenue dans l'application
welcome.text = Cette application est disponible en plusieurs langues.
Nous pouvons aussi avoir un fichier spécifique pour l’anglais qui devra se
nommer messages_en.properties
:
welcome.title = Welcome in this application
welcome.text = This application is available for different languages.
Si nous plaçons ces fichiers dans le classpath (pour une projet géré par Maven,
cela signifie qu’il faut placer ces fichiers dans src/main/resources
),
nous pouvons écrire le code Java :
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.ENGLISH);
String title = bundle.getString("welcome.title");
String message = bundle.getString("welcome.text");
System.out.println(title);
System.out.println(message);
L’exécution de ce code affichera :
Welcome in this application
This application is available for different languages.
La création d’un ResourceBundle se fait avec la méthode de fabrique getBundle
à laquelle on fournit la racine du nom des fichiers de propriétés et la locale
pour laquelle on désire obtenir les messages. Le ResourceBundle va appliquer
un algorithme décrit ici
pour déterminer quel fichier il doit prendre en compte en fonction de la langue.
Dans notre exemple, il s’agit du fichier pour la langue anglaise donc
messages_en.properties
. Pour toutes les autres langues, le ResourceBundle
utilisera le fichier par défaut messages_fr.properties
, donc les message
en français :
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.FRENCH);
String title = bundle.getString("welcome.title");
String message = bundle.getString("welcome.text");
System.out.println(title);
System.out.println(message);
L’exécution de ce code affichera :
Bienvenue dans l'application
Cette application est disponible en plusieurs langues.
Dans une application basée sur Spring Web MVC, nous n’avons pas à créer le ResourceBundle nous-mêmes. Le framework va s’en charger pour nous et nous permettre d’externaliser les messages dans les fichiers de propriétés.
Pour une application avec Spring Boot¶
Spring Boot active par défaut le support des messages. Il attend dans le classpath
des fichiers commençant par messages
(messages.properties
,
messages_en.properties
, messages_jp.properties
…). Donc, pour
un projet Maven, il suffit de placer les fichiers dans src/main/resources
.
Si vous voulez utiliser un autre préfixe pour le nom des fichiers, il suffit
de définir la propriété spring.messages.basename
dans le fichier
application.properties
Prudence
Spring Boot considère par défaut que les fichiers de messages sont encodés en UTF-8. C’est plutôt une bonne idée sauf que les fichiers de propriétés Java ont un encodage par défaut en Latin-1 (ISO-8859-1). Quand vous ouvrez les fichiers de propriétés dans un éditeur de texte, faites attention à ce que l’encodage soit bien en UTF-8.
Vous pouvez revenir à l’encodage par défaut de la norme Java en ajoutant
dans le fichier application.properties
:
spring.messages.encoding = iso-8859-1
Pour une application sans Spring Boot¶
Pour supporter l’externalisation des messages dans une application Spring, il suffit d’ajouter un bean de type MessageSource dans le contexte d’application. Par exemple, vous pouvez ajouter la méthode suivante dans un bean de configuration :
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("messages");
return source;
}
Dans l’exemple ci-dessus, le bean est de type ResourceBundleMessageSource.
Il utilise le mécanisme du ResourceBundle avec "messages"
comme préfixe
pour le nom des fichiers de propriétés.
Utilisation des messages dans une JSP¶
Pour afficher les messages externalisés dans les vues JSP, il faut utiliser la taglib Spring qui fournit la balise <spring:message/> :
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1><spring:message code="welcome.title"/></h1>
<p><spring:message code="welcome.text"/></p>
</body>
</html>
Utilisation des messages avec Thymeleaf¶
Les messages externalisés sont directement accessibles dans une vue
Thymeleaf en utilisant une expression de message #{ }
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1 data-th-text="#{ welcome.title }"></h1>
<p data-th-text="#{ welcome.text }"></p>
</body>
</html>
Validation des paramètres d’une requête¶
Spring Web MVC nous permet de valider les données envoyées au serveur et, éventuellement, d’afficher à l’utilisateur des messages d’erreur dans les vues. Spring Web MVC supporte plusieurs mécanismes pour gérer la validation : vous pouvez faire la validation par programmation en utilisant l’API ou vous pouvez faire une validation déclarative au moyen de Bean Validation.
La notion de binding et le BindingResult¶
Dans la programmation d’interface graphique, on utilise le terme de binding pour désigner le mécanisme qui permet de mettre à jour l’état d’un objet en fonction des données saisies par l’utilisateur. Dans Spring Web MVC, la validation est un mécanisme qui se produit au moment de la phase de binding. Lors de l’envoi de données par formulaire, nous déclarons dans la méthode du contrôleur un paramètre qui représente les données du formulaire et sur lequel va être effectué le binding. Spring Web MVC fournit également la classe BindingResult qui, comme son nom l’indique, est responsable de stocker le résultat de l’opération de binding et notamment les erreurs qui ont été détectées. Pour obtenir une objet de type BindingResult, il faut le déclarer en paramètre de la méthode de contrôleur juste derrière le paramètre représentant les données de formulaire.
Un objet de type BindingResult possède la méthode hasErrors qui permet de vérifier si des erreurs ont été détectées au moment du binding. Une implémentation classique pour la prise en charge d’un formulaire consiste à vérifier s’il existe des erreurs de binding et, dans ce cas, arrêter immédiatement le traitement en retournant l’identifiant de la vue pour afficher à nouveau le formulaire.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping("/item")
public String displayForm(@ModelAttribute Item item) {
return "itemForm";
}
@PostMapping("/item")
public String processForm(@ModelAttribute Item item,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "itemForm";
}
// ...
return "successProcessItem";
}
}
|
Validation dans la méthode du contrôleur¶
La manière la plus directe de faire de la validation et de placer le code de validation directement dans la méthode du contrôleur. Nous pouvons nous aider de la classe ValidationUtils qui fournit des méthodes statiques pour les cas de validation les plus simples.
1 2 3 4 5 6 7 8 9 10 11 12 13 | @PostMapping("/item")
public String processForm(@ModelAttribute Item item, BindingResult bindingResult) {
ValidationUtils.rejectIfEmpty(bindingResult, "name", "empty");
ValidationUtils.rejectIfEmpty(bindingResult, "code", "empty");
if (item.getQuantity() <= 0) {
bindingResult.rejectValue("quantity", "invalid");
}
if (bindingResult.hasErrors()) {
return "itemForm";
}
// ...
return "successProcessItem";
}
|
Avant de vérifier si l’objet bindingResult
contient des erreurs, on
appelle la méthode ValidationUtils.rejectIfEmpty. Cette méthode ajoute une
erreur dans l’objet de type BindingResult si le champ indiqué par le second
paramètre est vide. Si c’est le cas, une erreur est ajoutée avec un code.
Pour la ligne 3 par exemple, on vérifie le contenu de la propriété name
de
l’item. Si elle est vide alors une erreur avec le code "empty.item.code"
est ajoutée dans le BindingResult. Le code est construit à partir de la
valeur du dernier paramètre suivi du nom de l’objet dans le modèle et suivi
par le chemin du champ à valider.
Note
Le BindingResult est lié à l’objet de type Item
, il n’est donc
pas nécessaire de préciser que le binding porte sur item.
Pour des validations plus complexes (lignes 5 à 7), on peut utiliser la méthode
rejectValue pour ajouter une erreur de binding en précisant le champs concerné
et le code erreur. À la ligne 6, on ajoute une erreur pour la propriété quantity
avec le code erreur "invalid.item.quantity"
. Le code est construit à partir
de la valeur du dernier paramètre suivi du nom de l’objet dans le modèle et suivi
par le chemin du champ à valider.
Création d’un validateur¶
Le code précédent mélange la validation avec le traitement de la requête proprement
dite. Pour des raisons de lisibilité et de ré-utilisabilité, Spring Web MVC
incite à créer des classes implémentant l’interface Validator. Si nous reprenons
l’exemple précédent, nous pouvons créer une classe ItemValidator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package dev.gayerie;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmpty(errors, "name", "empty");
ValidationUtils.rejectIfEmpty(errors, "code", "empty");
if (item.getQuantity() <= 0) {
errors.rejectValue("quantity", "invalid");
}
}
}
|
Une classe implémentant l’interface Validator doit fournir l’implémentation de deux méthodes :
- supports
Retourne
true
si la classe passée en paramètre peut être validée par ce validateur. Cela permet à Spring Web MVC de découvrir dynamiquement à quelles classes un validateur s’applique.- validate
Réalise la validation proprement dite. Le paramètre
errors
permet d’enregistrer toutes les erreurs de validation détectées. Nous avons déplacé et adapté notre code de la section précédente dans l’implémentation de cette méthode.
Note
Remarquer que la classe ItemValidator
possède le stéréotype @Component
pour qu’un bean soit ajouté dans le contexte d’application.
Adaptons le code de notre contrôleur. Nous n’avons plus besoin de conserver
le code de validation qui est maintenant pris en charge par le validateur. Nous
devons tout de même indiquer au framework lors de l’appel à quelles méthodes
nous souhaitons valider le contenu de l’objet Item
. Pour cela, il suffit
d’ajouter l’annotation @Validated sur le paramètre en question (ligne 19
ci-dessous) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping("/item")
public String displayForm(@ModelAttribute Item item) {
return "itemForm";
}
@PostMapping("/item")
public String processForm(@Validated @ModelAttribute Item item,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "itemForm";
}
// ...
return "successProcessItem";
}
}
|
À l’opposé, nous n’utilisons pas l’annotation @Validated sur le paramètre de
la méthode displayForm
. En effet, cette méthode utilise un objet Item
non valide puisqu’il sert juste à initialiser l’affichage du formulaire.
Validation déclarative avec Bean Validation¶
Il existe une API standard en Java pour réaliser une validation déclarative : c’est l’API Bean Validation (JSR-303). Le Spring Framework est capable d’intégrer automatiquement ce standard s’il trouve une implémentation de cette API au lancement de l’application.
Note
Pour une application basée sur Spring Boot 2.4 et suivant, vous devez activer
le support de Bean Validation en déclarant une dépendance. Pour un projet
Maven, il suffit d’ajouter dans le fichier pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Pour une application non Spring Boot, vous devez ajouter une dépendance
à une implémentation de Bean Validation. L’implémentation de référence
est Hibernate Validator. Pour un projet Maven, il suffit d’ajouter dans
le fichier pom.xml
:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.2.Final</version>
</dependency>
Bean Validation repose sur une famille d’annotations qui sont positionnées sur les attributs d’un bean pour indiquer les contraintes à respecter.
package dev.gayerie;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
public class Item {
@NotBlank(message = "Le nom ne peut pas être vide !")
private String name;
@NotBlank(message = "Le code ne peut pas être vide !")
private String code;
@Min(value = 1, message = "La quantité doit être positive !")
private int quantity;
// Getters/setters omis
}
Note
La liste des annotations de Bean Validation est disponible dans la documentation de Java EE.
Nous n’avons pas besoin d’une classe de validation et si nous adaptons le code
de notre contrôleur, nous n’avons pas non plus besoin de conserver le code
pour valider les données. Nous devons tout de même indiquer au framework pour
quelles méthodes nous souhaitons valider le contenu de l’objet Item
. Pour cela,
il suffit d’ajouter l’annotation @Validated sur le paramètre en question
(ligne 19 ci-dessous) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ItemController {
@GetMapping("/item")
public String displayForm(@ModelAttribute Item item) {
return "itemForm";
}
@PostMapping("/item")
public String processForm(@Validated @ModelAttribute Item item,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "itemForm";
}
// ...
return "successProcessItem";
}
}
|
À l’opposé, nous n’utilisons pas l’annotation @Validated sur le paramètre de
la méthode displayForm
. En effet, cette méthode utilise un objet Item
non valide puisqu’il sert juste à initialiser l’affichage du formulaire.
Note
Si vous ne voulez pas écrire les messages d’erreur de validation en dur dans les annotations Bean Validation, vous pouvez utiliser des accolades pour signaler qu’il s’agit d’un code de message :
package dev.gayerie;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
public class Item {
@NotBlank(message = "{empty.item.name}")
private String name;
@NotBlank(message = "{empty.item.code}")
private String code;
@Min(value = 1, message = "{invalid.item.quantity}")
private int quantity;
// Getters/setters omis
}
Nous verrons à la section suivante que ces codes vont permettre d’extraire des fichiers de propriétés les message à afficher dans les vues. Malheureusement, Spring Web MVC n’active pas la conversion automatique des messages de Bean Validation pour obtenir le message à partir du code. Pour que cela fonctionne, il faut ajouter un bean spécifique dans le contexte d’application qui va réaliser cette opération. Ajoutez la méthode suivante dans n’importe quel bean de configuration de votre application :
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
Affichage des erreurs de validation¶
L’objet BindingResult est mis à disposition automatiquement dans le modèle. Cela signifie qu’il est possible d’afficher des messages d’erreur dans une vue à partir des codes fournis. Pour cela, il faut d’abord créer un message pour chaque code.
Si nous reprenons notre exemple, nous pouvons ajouter dans le fichier
messages.properties
(Cf. Externalisation des messages)
les messages associés aux codes :
empty.item.name = Le nom doit être renseigné
empty.item.code = Le code doit être renseigné
invalid.item.quantity = La quantité doit être un nombre positif
Ensuite il faut préciser dans la vue affichant le formulaire à quels endroits les différents messages devront être affichés (s’il y en a).
Dans la JSP, nous pouvons utiliser la balise <form:errors/> et indiquer
avec l’attribut path
le chemin du champ pour lequel nous voulons afficher
les erreurs.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form:form servletRelativeAction="/item" modelAttribute="item">
<p><label>Code : </label><form:input path="code"/> <form:errors path="code"/></p>
<p><label>Nom : </label><form:input path="name"/> <form:errors path="name"/></p>
<p><label>Quantité : </label><form:input path="quantity"/> <form:errors path="quantity"/></p>
<button type="submit">Envoyer</button>
</form:form>
</body>
</html>
Dans le modèle Thymeleaf, nous pouvons utiliser l’objet utilitaire #errors
qui correspond au dictionnaire des erreurs pour vérifier si des erreurs
existent et l’attribut Thymeleaf errors
pour extraire les messages d’erreur.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form action="#" method="post" data-th-action="@{/item}" data-th-object="${item}" accept-charset="utf-8">
<p><label>Code : </label><input type="text" data-th-field="*{code}">
<span data-th-if="${#fields.hasErrors('code')}" data-th-errors="*{code}"></span></p>
<p><label>Nom : </label><input type="text" data-th-field="*{name}">
<span data-th-if="${#fields.hasErrors('name')}" data-th-errors="*{name}"></span></p>
<p><label>Quantité : </label><input type="number" data-th-field="*{quantity}">
<span data-th-if="${#fields.hasErrors('quantity')}" data-th-errors="*{quantity}"></span></p>
<button type="submit">Envoyer</button>
</form>
</body>
</html>
Dans la vue, le code erreur est remplacé par le message venant du fichier
messages.properties
.
POST-redirect-GET¶
L’emploi de la méthode POST
signale que l’utilisateur souhaite modifier des
informations sur le serveur. Cependant, ce type de requêtes est géré de manière
particulière par les navigateurs Web. Si un utilisateur désire rafraîchir une
page obtenue à partir d’une requête POST
(ou sélectionne une page dans son
historique obtenue à partir d’une requête POST
), il verra s’afficher un
message d’avertissement car le navigateur ne peut pas garantir que la réémission
de la requête n’aura pas des effets de bord sur le serveur.
Note
Ce comportement est parfaitement normal et découle du fait que la méthode
POST
est définie comme non-idempotente par la spécification HTTP.
Cela signifie que, si on envoie plusieurs fois la même requête POST
au
serveur, HTTP ne peut pas garantir que le résultat sera identique.
Typiquement, si on émet une requête POST
pour créer un compte utilisateur
sur le serveur, rien ne peut garantir le comportement de la même requête
émise une seconde fois. La seconde requête peut ne produire aucun effet
car le compte existe déjà, créer un deuxième compte, produire une erreur…
Pour éviter que le navigateur Web n’émette à nouveau une requête POST
, il est
recommandé d’utiliser le principe du POST/Redirect/GET.
En réponse à une requête POST
, le serveur doit produire une réponse HTTP de
redirection (c’est-à-dire une réponse avec un code statut 302 ou 303)
pour rediriger le navigateur vers une nouvelle adresse. Ainsi, le navigateur
effectue immédiatement une requête GET
à cette nouvelle adresse et affiche
le résultat à l’utilisateur en oubliant la requête précédente. Ainsi,
le navigateur ne garde plus trace de la requête POST
originale évitant ainsi
à l’utilisateur la possibilité de la réémettre.
Un contrôleur Spring Web MVC peut déclencher automatiquement une réponse de
redirection en préfixant la chaîne de caractères retournée par redirect:
.
Le contenu est alors identifié comme une adresse relative à la racine de
l’application Web (et non comme un identifiant de vue).
De plus, Spring Web MVC introduit la notion d’attributs Flash. Ces attributs sont stockés en session jusqu’à la prochaine requête de l’utilisateur. Ils sont donc faits pour mémoriser très temporairement un contexte d’exécution le temps d’une redirection. Pour utiliser les attributs flash, il suffit d’attendre en paramètre de la méthode d’un contrôleur un argument de type RedirectAttributes. Grâce à ce paramètre, on peut mémoriser un attribut flash avant d’effectuer la redirection. L’attribut flash sera automatiquement ajouté dans le modèle pour le traitement du contrôleur après la redirection.
Ci-dessous un exemple simple utilisant une redirection avec un attribut flash :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class IndexController {
@GetMapping(path="/")
public String home(RedirectAttributes redirectAttributes) {
Item item = new Item();
item.setCode("BV-34");
item.setName("Mon item");
redirectAttributes.addFlashAttribute("item", item);
return "redirect:/autre-page";
}
@GetMapping(path="/autre-page")
public String redirectHome(@ModelAttribute Item item) {
// Le paramètre item correspond à l'instance ajoutée comme attribut flash
return "view";
}
}
|
La gestion des exceptions¶
Spring Web MVC nous permet de transformer des exceptions en erreur HTTP pour le client (voire même créer une réponse complète avec une vue dédiée).
Certaines exceptions fournies par Spring Web MVC sont directement comprises et transformées en erreur par le framework. Par exemple, HttpRequestMethodNotSupportedException permet de signaler une erreur HTTP 405. Ce comportement est géré par la classe DefaultHandlerExceptionResolver. Reportez-vous à la documentation de cette classe pour la liste complète des exceptions supportées.
Mais un contrôleur peut également fournir des méthodes de gestion des exceptions. Ces méthodes permettent de prendre en charge n’importe quel type d’exception. Elles doivent être annotées avec @ExceptionHandler. L’annotation permet de préciser la classe de l’exception que la méthode peut gérer. Une méthode de gestion d’exception accepte des types de paramètres et une valeur de retour similaires aux méthodes de gestion de requête. Une méthode de gestion d’exception attend également en paramètre l’exception à traiter.
package dev.gayerie;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
public class ItemController {
@ExceptionHandler(ItemException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleItemException(ItemException e, Model model) {
model.addAttribute("message", e.getMessage());
return "itemError";
}
@PostMapping("/item")
public String processForm(@ModelAttribute Item item) throws ItemException {
if (item.getQuantity() == 0) {
throw new ItemException("Item not available");
}
// ...
return "successProcessItem";
}
}
Dans l’exemple précédent, on déclare un contrôleur dont la méthode processForm
peut jeter une ItemException
qui est une exception de l’application. Le contrôleur
déclare également une méthode pour gérer les exceptions du même type. Ainsi, si
cette exception est effectivement lancée lors de l’exécution de la méthode
processForm
, Spring Web MVC appellera la méthode handleItemException
et utilisera sa valeur de retour pour en déduire la vue. Notez que la méthode
handleItemException
est annotée avec @ResponseStatus qui permet de modifier
le statut HTTP de la réponse. Dans cet exemple, une exception ItemException
produira une réponse avec le code HTTP 400 (Bad Request).
Note
Normalement le code statut d’une réponse produite par un contrôleur est 200 (OK). Produire un autre code avec l’annotation @ResponseStatus a des effets intéressants. Par exemple, les réponses avec un code statut d’erreur client (code commençant par un 4 comme 404) ou avec un code statut d’erreur serveur (code commençant par un 5) ne sont pas mises en cache par le navigateur.
Astuce
Nous avons vu que pour traiter la validation de formulaire, il fallait déclarer un paramètre de type BindingResult à la méthode du contrôleur. En l’absence de ce paramètre, si un autre paramètre possède l’annotation @Validated et que la validation échoue, alors le framework jette une exception de type MethodArgumentNotValidException avant même d’appeler la méthode du contrôleur.
Vous pouvez écrire une méthode de gestion des exceptions pour traiter cette situation. Il s’agit d’une façon alternative de traiter l’échec de la validation. Les messages d’erreur seront tout de même disponibles dans la vue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package dev.gayerie;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
public class ItemController {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleValidationException(MethodArgumentNotValidException e) {
// En cas d'échec de validation, on retourne sur la vue
// du formulaire.
return "itemForm";
}
@GetMapping("/item")
public String displayForm(@ModelAttribute Item item) {
// ...
return "itemForm";
}
@PostMapping("/item")
public String processForm(@Validated @ModelAttribute Item item) {
// ...
return "successProcessItem";
}
}
|
Méthodes de modèle¶
Il est parfois utile d’ajouter des éléments dans un modèle quelle que soit la requête émise vers un contrôleur. De cette façon, ces éléments seront disponibles dans la vue. Une méthode avec l’annotation @ModelAttribute est automatiquement appelée avant l’appel à la méthode de traitement de la requête et l’objet qu’elle retourne est ajouté au modèle.
package dev.gayerie;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(path="/item/{code}")
public class ItemEditController {
@ModelAttribute
public Item getItem(@PathVariable String code) {
Item item = new Item();
item.setCode(code);
// ...
return item;
}
@GetMapping
public String viewItem(@ModelAttribute Item item) {
// ...
return "showItem";
}
}
Dans l’exemple ci-dessus, la méthode getItem
est systématiquement appelée
avant la méthode de traitement de la requête. Elle récupère le code dans le
chemin de la ressource et elle retourne une instance de la classe Item
.
Cette instance est automatiquement ajoutée dans le modèle (avec le nom item
).
Elle sera donc accessible depuis une vue. Cette instance de la classe Item
peut également être passée directement en paramètre de la méthode de traitement
de la requête comme c’est le cas pour la méthode viewItem
de l’exemple ci-dessus.
Méthodes de binder¶
Nous avons vu que le binding est l’étape qui permet de remplir les propriétés
d’un objet du modèle à partir des paramètres de la requête. Spring Web MVC
fournit des bindings. Par exemple, il existe un binding par défaut pour
transformer une chaîne de caractères en nombre. Cela permet de transformer
un paramètre de la requête si le type Java attendu est un int
.
Il est possible d’ajouter ces propres définitions de binders. Pour cela, il
suffit d’ajouter dans un contrôleur une méthode annotée avec @InitBinder et
ayant au moins un paramètre de type WebDataBinder.
package dev.gayerie;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class DateController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
@PostMapping("/date")
public String updateDate(@RequestParam Date date) {
// ...
return "success";
}
}
Dans l’exemple ci-dessus, le contrôleur contient une méthode annotée avec
@InitBinder. Cette méthode est automatiquement appelée avant l’appel de la
méthode de traitement de la requête. Elle enregistre un SimpleDateFormat pour
permettre de convertir une chaîne de caractères en Date suivant le format
dd-MM-yyyy
. Dans la même classe, la méthode de traitement de requête
updateDate
attend en paramètre un objet de type Date correspondant au
paramètre date
de la requête HTTP. Spring Web MVC utilisera le binding
défini pour réaliser cette transformation.
ControllerAdvice¶
Nous avons vu qu’il est possible de déclarer des méthodes annotées avec @ExceptionHandler, @ModelAttribute et @InitBinder dans un contrôleur. Si nous voulons utiliser les mêmes méthodes pour plusieurs contrôleurs, il est possible de créer une classe annotée avec @ControllerAdvice. Cette classe regroupe toutes les méthodes communes et indique les contrôleurs, le package ou même l’annotation pour lesquels elles s’appliquent.
package dev.gayerie;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice("dev.gayerie")
public class ItemControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
@ExceptionHandler(ItemException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleItemException(ItemException e, Model model) {
model.addAttribute("message", e.getMessage());
return "itemError";
}
}
Dans l’exemple ci-dessus, tous les contrôleurs déclarés dans le package dev.gayerie spécifié par l’annotation @ControllerAdvice bénéficieront automatiquement de la méthode de binding et de la méthode de gestion d’exception comme si ces méthodes avaient directement été déclarées dans les classes des contrôleurs.