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.

Un contrôleur
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 :

Configuration du projet Maven pour produire une application Web
<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.

Dépendances pour un projet Maven
<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 :

Configuration du support des JSP
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 :

Configuration du projet Maven pour produire une application Web
<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.

Dépendances pour un projet Maven
<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.

Ajout du support des vues JSP dans l’application
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 :

Un contrôleur retournant un identifiant de vue
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.

le fichier accueil.jsp
<%@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 :

Un contrôleur retournant un identifiant de vue
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.

le fichier accueil.html
<!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.

Le contrôleur pour ajouter la donnée au modèle
 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.

La vue JSP : affichageDate.jsp
 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 :

La vue Thymeleaf : affichageDate.html
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.

Un exemple de contrôleur utilisant un service
 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 ou null 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.

Les données à associer au formulaire
 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 :

Exemple de fichier itemForm.jsp
 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.

Le contrôleur associé au formulaire
 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.

Les données à associer au formulaire
 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 :

Exemple de fichier itemForm.html
 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.

Le contrôleur associé au formulaire
 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 :

Le fichier messages.properties
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 :

Le fichier 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 :

Déclaration d’un bean MessageSource
@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.

Implémentation type pour la prise en charge du BindingResult
 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.

Validation dans la méthode de contrôleur
 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 :

La classe de validation pour Item
 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 :

Dépendance Spring Boot pour Spring Validation
<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 :

Dépendance à Bean Validation pour un projet sans Spring Boot
<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.

Exemple d’utilisation des annotations de validation
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 :

Utilisation de codes pour représenter les erreurs
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 :

Activation de la conversion des codes pour Bean Validation
@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 :

Extrait du fichier messages.properties
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.

Modification de la JSP pour intégrer l’affichage des 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.

Modification du modèle pour intégrer l’affichage des erreurs
<!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 :

Redirection et utilisation des attributs 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.

Méthode de gestion des exceptions pour la validation
 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.