MVC et RequestDispatcher

Le modèle MVC

Le modèle MVC (Modèle Vue Contrôleur) est un modèle d’architecture pour guider la conception d’applications qui nécessitent une interaction de l’utilisateur avec le système. Il définit trois grandes catégories de responsabilité :

Le modèle

Les classes appartenant à cette catégorie définissent les données applicatives ainsi que les logiques de traitement propres à l’application.

La vue

Les classes appartenant à cette catégorie gèrent la représentation graphique des données et l’interface utilisateur

Le contrôleur

Les classes appartenant à cette catégorie gèrent les interactions de l’utilisateur et la mise à jour des vues après la modification des données. Les contrôleurs assurent la cohérence entre le modèle et la vue.

Pour une application Web Java

  • le modèle peut être un simple objet Java qui encapsule la logique de l’application

  • la vue est une Java Server Pages (JSP)

  • le contrôleur est une servlet chargée de valider les paramètres de la requête avant de les transmettre au modèle pour le traitement. Une fois ce traitement terminé, la servlet transmet le résultat à la vue.

L’implémentation du modèle MVC pour une application Java EE suppose que l’on puisse découper le traitement de la requête en plusieurs étapes : d’abord la réception par la servlet qui joue le rôle de contrôleur puis le traitement par le modèle et enfin le rendu de la vue par la JSP. Le RequestDispatcher permet de réaliser ce découpage puisque la servlet va pouvoir transférer le traitement de la requête à une JSP.

Le request dispatcher

Le request dispatcher est un objet fourni par le conteneur Web. Il permet d’inclure ou de déléguer des traitements lors de la prise en charge d’une requête HTTP.

Nous avons vu dans un précédent chapitre qu’il existe une classe ServletContext permettant, notamment, de stocker les attributs de portée application. Le ServletContext représente le contexte d’une application Web et est accessible depuis une servlet grâce à la méthode getServletContext().

Une instance de ServletContext permet également de récupérer une instance de RequestDispatcher grâce aux méthodes :

getResquestDispatcher(String path)

Permet de récupérer une instance de RequestDispatcher pour transferer le traitement à la ressource dont le chemin d’URL est passé en paramètre

RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/un/chemin");

Selon la documentation, le chemin passé en paramètre DOIT commencer par /. Cependant, le chemin est interprété relativement au contexte racine de l’application.

getNamedDispatcher(String name)

Permet de récupérer une instance de RequestDispatcher pour transférer le traitement à la servlet dont le nom est passé en paramètre

RequestDispatcher dispatcher = getServletContext().getNamedDispatcher("maServlet");

Une servlet peut être nommée dans le fichier web.xml grâce à la balise <servlet-name> ou avec l’annotation @WebServlet grâce à l’attribut name de l’annotation.

Une fois que nous disposons d’une instance d’un RequestDispatcher nous pouvons appeler une de ses deux méthodes disponibles :

void RequestDispatcher.include(ServletRequest request, ServletResponse response)

Inclut le contenu de la ressource dans le résultat final renvoyé au client. Si le request dispatcher pointe sur un fichier HTML, l’ensemble du fichier sera inséré dans la réponse. Si le request dispatcher pointe sur une servlet, cette dernière est exécutée et sa sortie est insérée dans la réponse.

void RequestDispatcher.forward(ServletRequest request, ServletResponse response)

Trasfère le traitement de la requête à une nouvelle ressource. La différence avec la méthode include est que la servlet qui appelle forward ne doit pas avoir produit de contenu dans la réponse. C’est cette méthode qui va permettre d’implémenter le modèle MVC dans une application Web Java EE.

Avec le request dispatcher, on voit apparaître la possibilité de créer une chaîne de traitement pour une requête. Par exemple, une servlet peut être utilisée pour valider les paramètres transmis par la requête et effectuer un traitement propre à l’application. Puis, via un RequestDispatcher, elle peut déléguer à une autre servlet le soin de générer la réponse en utilisant la méthode forward. C’est dans ce modèle de traitement que les attributs de requête vont être utiles (Cf. le chapitre Les attributs d’une application Web). Chaque servlet impliquée dans le traitement de la requête exploite et produit des données qu’elle peut récupérer ou stocker comme attribut dans la requête.

Pour l’implémentation du modèle MVC, le RequestDispatcher est utilisé pour transmettre le traitement de la requête de la servlet à la JSP.

Exemple de MVC (sans validation)

À titre d’exemple, imaginons que nous voulons réaliser une application qui permet à l’utilisateur de saisir des informations personnelles : son nom et son âge. L’application se contente de vérifier si la personne est majeure et d’afficher un message en conséquence.

L’interaction entre l’utilisateur et le serveur correspond à l’envoi d’une requête HTTP. La servlet SaisieDonneesPersonnellesControleur joue le rôle de contrôleur.

Le contrôleur
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package dev.gayerie;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/donneespersonnelles")
public class SaisieDonneesPersonnellesControleur extends HttpServlet {

  private static final String VUE_FORMULAIRE = "/WEB-INF/jsp/saisieDonneesPersonnelles.jsp";
  // ...

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    getServletContext().getRequestDispatcher(VUE_FORMULAIRE).forward(req, resp);
  }

  // ...

}

La méthode doGet traite les requêtes de type GET. Elle n’a pas de traitement particulier à réaliser sinon de rediriger vers la vue en utilisant le RequestDispatcher. La vue correspond à un fichier JSP.

Note

Le chemin du fichier JSP est /WEB-INF/jsp/saisieDonneesPersonnelles.jsp. Le fichier se trouve dans la partie privée de l’application puisqu’il est placé dans un sous-répertoire à /WEB-INF. Il est possible de transférer le traitement à une partie privée de l’application. Cela empêche l’accès direct à la vue depuis un navigateur Web.

La vue du formulaire
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<%@page pageEncoding="UTF-8" contentType="text/html" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Java EE - Exemple MVC</title>
  </head>
  <body>
    <form action="" accept-charset="utf-8" method="post">
            <label>Nom :</label>
            <input type="text" name="nom">
            <label>Age :</label>
            <input type="number" name="age">
            <input type="submit">
    </form>
        <div>
            <a href='<c:url value="/"/>'>Retour à l'accueil</a>
        </div>
  </body>
</html>

Lorsque l’utilisateur remplit le formulaire et soumet les données en cliquant sur le bouton, une nouvelle requête est émise vers la même adresse mais avec la méthode POST.

Astuce

En laissant vide l’attribut action de l’élément form, le navigateur soumettra le formulaire à l’adresse courante.

Nous pouvons compléter notre contrôleur pour gérer le traitement d’une requête POST :

Le contrôleur complet
 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
package io.github.spoonless.mvc;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/donneespersonnelles")
public class SaisieDonneesPersonnellesControleur extends HttpServlet {

  private static final String VUE_FORMULAIRE = "/WEB-INF/jsp/saisieDonneesPersonnelles.jsp";
  private static final String VUE_RESULTAT = "/WEB-INF/jsp/donneesPersonnelles.jsp";

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    getServletContext().getRequestDispatcher(VUE_FORMULAIRE).forward(req, resp);
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("utf-8");
    String nom = req.getParameter("nom");
    String age = req.getParameter("age");
    DonneesPersonnelles donneesPersonnelles = new DonneesPersonnelles(nom, age);
    req.setAttribute("donneesPersonnelles", donneesPersonnelles);
    getServletContext().getRequestDispatcher(VUE_RESULTAT).forward(req, resp);
  }

}

À la ligne 27, le contrôleur crée un objet de type DonneesPersonnelles qui correspond au modèle. À la ligne 28, le contrôleur enregistre cet objet comme attribut de requête pour le rendre disponible à la vue. Et, à la ligne 29, le contrôleur utilise le RequestDispatcher pour transférer la traitement à la vue.

Le modèle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package dev.gayerie;

public class DonneesPersonnelles {

  private String nom;
  private Integer age;

  public DonneesPersonnelles(String nom, String age) {
    this.nom = nom;
    this.age = Integer.valueOf(age);
  }

  public String getNom() {
    return nom;
  }

  public Integer getAge() {
    return age;
  }

  public boolean isMajeur() {
    return age != null && age >= 18;
  }
}

Dans notre exemple, le modèle reste très simple. Il a tout de même la responsabilité de transformer les données et de savoir si un utilisateur est, ou non, majeur.

La vue après traitement du formulaire
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@page pageEncoding="UTF-8" contentType="text/html" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Java EE - Exemple MVC</title>
  </head>
  <body>
        <div>Vos données personnelles&nbsp;:</div>
        <ul>
                <li>Nom : <c:out value="${donneesPersonnelles.nom}"/></li>
                <li>Age : <fmt:formatNumber value="${donneesPersonnelles.age}"/></li>
        </ul>
        <c:if test="${donneesPersonnelles.majeur}">
                <div>Vous êtes majeur(e) !</div>
        </c:if>
        <div>
            <a href='<c:url value="/"/>'>Retour à l'accueil</a>
        </div>
  </body>
</html>

Exemple de MVC (avec validation)

Nous pouvons complexifier l’exemple précédent en ajoutant une étape de validation des données. La plupart du temps, la validation est de la responsabilité du modèle. Le contrôleur se contente d’attendre le traitement du modèle pour savoir s’il faut transférer le traitement à une vue de succès ou à une vue d’échec. La vue d’échec correspond, la plupart du temps, à la vue du formulaire dans laquelle on affiche un ou plusieurs messages d’erreur.

Le contrôleur
 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 java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/donneespersonnelles")
public class SaisieDonneesPersonnellesControleur extends HttpServlet {

  private static final String VUE_FORMULAIRE = "/WEB-INF/jsp/saisieDonneesPersonnelles.jsp";
  private static final String VUE_RESULTAT = "/WEB-INF/jsp/donneesPersonnelles.jsp";

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    getServletContext().getRequestDispatcher(VUE_FORMULAIRE).forward(req, resp);
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.setCharacterEncoding("utf-8");
    try {
      String nom = req.getParameter("nom");
      String age = req.getParameter("age");
      DonneesPersonnelles donneesPersonnelles = new DonneesPersonnelles(nom, age);
      req.setAttribute("donneesPersonnelles", donneesPersonnelles);
      getServletContext().getRequestDispatcher(VUE_RESULTAT).forward(req, resp);
    } catch (DonneesInvalidesException e) {
      req.setAttribute("message", e.getMessage());
      getServletContext().getRequestDispatcher(VUE_FORMULAIRE).forward(req, resp);
    }
  }

}
Le modèle
 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
package dev.gayerie;

public class DonneesPersonnelles {

  private String nom;
  private Integer age;

  public DonneesPersonnelles(String nom, String age) throws DonneesInvalidesException {
   if (isBlank(nom) || isBlank(age)) {
      throw new DonneesInvalidesException("Certaines donnees ne sont pas renseignees !");
    }
    this.nom = nom;
    try {
     this.age = Integer.valueOf(age);
    } catch(NumberFormatException e) {
     throw new DonneesInvalidesException("L'age n'est pas un nombre !");
    }
  }

  private static boolean isBlank(String valeur) {
   return valeur == null || "".equals(valeur);
  }

  public String getNom() {
   return nom;
  }

  public Integer getAge() {
   return age;
  }

  public boolean isMajeur() {
   return age != null && age >= 18;
  }
}
L’exception pour permettre au modèle de signaler un problème
1
2
3
4
5
6
7
8
9
package dev.gayerie;

public class DonneesInvalidesException extends Exception {

  public DonneesInvalidesException(String message) {
    super(message);
  }

}

Pour la vue de formulaire, on pré-remplie le formulaire avec les données du l’attribut donneesPersonnelles. Cet attribut est passé à la vue en cas d’échec de la validation. Cela permet d’afficher à nouveau à l’utilisateur les données qu’il a saisies.

La vue du 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
<%@page pageEncoding="UTF-8" contentType="text/html" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Java EE - Exemple MVC</title>
  </head>
  <body>
    <div>
            <c:out value="${message}"/>
    </div>
    <form action="" accept-charset="utf-8" method="post">
            <label>Nom :</label>
            <input type="text" name="nom" value='<c:out value="${donneesPersonnelles.nom}"/>'>
            <label>Age :</label>
            <input type="number" name="age" value="${donneesPersonnelles.age}">
            <input type="submit">
    </form>
        <div>
            <a href='<c:url value="/"/>'>Retour à l'accueil</a>
        </div>
  </body>
</html>
La vue après traitement du formulaire
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@page pageEncoding="UTF-8" contentType="text/html" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Java EE - Exemple MVC</title>
  </head>
  <body>
        <div>Vos données personnelles&nbsp;:</div>
        <ul>
                <li>Nom : <c:out value="${donneesPersonnelles.nom}"/></li>
                <li>Age : <fmt:formatNumber value="${donneesPersonnelles.age}"/></li>
        </ul>
        <c:if test="${donneesPersonnelles.majeur}">
                <div>Vous êtes majeur(e) !</div>
        </c:if>
        <div>
            <a href='<c:url value="/"/>'>Retour à l'accueil</a>
        </div>
  </body>
</html>

Note

Vous pouvez télécharger le projet complet : exemple-mvc.zip.

Exercice

Formulaire MVC

Objectif

Reprenez l’exemple précédent et ajoutez les fonctionnalités suivantes :

  • Dans le formulaire, l’utilisateur doit pouvoir préciser sa civilité : Monsieur, Madame, Mademoiselle. Le choix s’affiche sous la forme d’une liste (select en HTML). Attention, la liste des civilités ne doit pas être en dur dans la vue mais elle doit être fournie par un modèle. La civilité est affichée dans la vue après le traitement par le serveur.

  • Dans le formulaire, l’utilisateur doit saisir un email. Le format de l’email est validé par le serveur. Pour faire simple, on peut se contenter de vérifier que le caractère @ est présent dans l’email.

  • Modifier l’implémentation : si l’utilisateur a fait plusieurs erreurs de saisie dans le formulaire, elles doivent être toutes clairement signalées lors de l’affichage du formulaire à côté du champ concerné.

Pour réaliser ces améliorations, vous respecterez une architecture MVC.

Modèle Maven du projet à télécharger

exemple-mvc.zip

Mise en place du projet

Éditer le fichier pom.xml du template et modifier la balise artifactId pour spécifier le nom de votre projet.

Intégration du projet dans Eclipse

L’intégration du projet dans Eclipse suit la même procédure que celle vue dans Import du projet Maven dans Eclipse.