Mise en oeuvre des Servlets et des JSP

Quelques liens utiles

Apache Tomcat :

Servlets et technologie Java Server Page :

Transformer notre application

Depuis plusieurs séances nous travaillons sur une application Spring-boot ligne de commande. Nous allons la transformer en application WEB afin d'héberger nos prochains essais. Suivez les étapes ci-dessous avec minutie :

  • Étape 1 : Les dépendances. Nous allons commencer par enrichir les dépendances de notre projet. Ajouter au fichier pom.xml les lignes ci-dessous :
<!-- == Partie Web de Spring Boot == -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>
<!-- == Pour embarquer le conteneur WEB Tomcat == -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- == Pour que l'application redémarre automatiquement == -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>
  • Étape 2 : Ajuster la configuration de Spring Boot. Ajouter au fichier application.properties les lignes ci-dessous. Elle permet de spécifier le port sur lequel l'application va écouter. le port 8080, qui est un grand classique, s'appelle le webcache.
// Le port sur lequel l'application va écouter
server.port=8080
  • Étape 3 : Enrichir la phase de démarrage de Spring Boot. Ajouter à votre classe MyApp l'annotation @ServletComponentScan. Celle-ci indique à Spring que nous allons utiliser des servlets. Il faut donc les détecter et les configurer.
...

@SpringBootApplication
@ServletComponentScan  // <--- à ajouter
public class MyApp implements CommandLineRunner {

    ...

}
  • Étape 4 : Ajouter des pages JSP à notre projet. Commencez par créer le répertoire src/main/webapp et créez à l'intérieur la page index.jsp ci-dessous :
La page JSP src/main/webapp/index.jsp
<%@page contentType="text/html;charset=UTF-8" %>
<%@page pageEncoding="UTF-8" %>
<!-- ma première page JSP -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Ma première page</title>
</head>
<html><body>
   <p>Hello  1 + 2 = <%= (1+2) %></p>
</body></html>
  • Étape 5 : Ranger les anciennes classes. Pour ne pas tout mélanger, dans votre IDE préféré, nous allons effectuer le refactoring ci-dessous :
    • Dans src/java :
      • Créez un package myapp.ioc.
      • Déplacez les classes qui se trouvent dans myapp vers myapp.ioc (SAUF la classe MyApp).
      • Créez un package myapp.zpartie1 (z pour le mettre à la fin),
      • Déplacez vos packages myapp.ioc, myapp.jdbc et myapp.jpa dans myapp.zpartie1.
      • Il ne doit pas y avoir d'erreur de compilation.
    • Faites les mêmes opérations dans src/test.
  • Étape 6 : Lancer et tester.
    • Lancez votre application. Vous devriez observer dans les traces le lancement de Tomcat qui écoute sur le port 8080.
    • Dans un navigateur, ouvrez l'adresse http://localhost:8080/index.jsp
    • Modifiez un peu la page index.jsp et vérifiez que vous obtenez bien le nouveau résultat dans votre navigateur.
  • Étape 7 : IntelliJ. Pour celles et ceux qui utilisent IntelliJ en JEE, voici un lien utile qui explique comment configurer IntelliJ pour relancer automatiquement le serveur quelques secondes après chaque modification du code :
Note : Vous remarquerez que nous n'avons rien changé ni supprimé, nous avons seulement enrichi la configuration de notre application.
Note : À ce stade, nous utilisons Spring Boot pour héberger nos (futures) Servlet et pages JSP (c'est beaucoup plus simple). Nous restons donc dans un cadre JEE presque originel. Plus tard, nous utiliserons plus activement Spring Boot MVC mais pas tout de suite.

Mon premier bean (20m)

  • Créez un package myapp.servlet qui va héberger vos futures classes.
  • Modifiez la page précédente afin qu'elle affiche la date du jour et l'heure à chaque exécution (créez pour cela une instance de la classe java.util.Date et affichez la).
    <%@page import="java.util.Date" %>
    
    <%!
    Date now = new Date();
    %>
    
    <p>Aujourd'hui : <%= now %></p>
    
  • Quel comportement anormal observez-vous ? Essayez cette nouvelle version :
    <%@page import="java.util.Date"%>
    
    <p>Aujourd'hui : <%= new Date() %></p>
    
  • Finalement, l'introduction de code Java dans les JSP est maintenant fortement déconseillée. Nous allons préférer la forme ci-dessous basée sur les EL (langage d'expressions) :
    <jsp:useBean id="now" class="java.util.Date" />
    
    <p>Aujourd'hui : ${now}</p>
    
  • Faites tourner les exemples présentés en cours d'affichage et de manipulation d'un produit.
  • Faites varier la portée (le scope) du <jsp:useBean> et observez les différences dans les trois cas (request, session, application).
    • Conseil 1 : Vous pouvez placer du code JSP à l'intérieur de l'action <jsp:useBean>. Ce code est exécuté lorsque le bean est créé.
    • Conseil 2 : Pour bien manipuler les beans de portée session, utilisez plusieurs navigateurs (firefox ou chrome) ou une fenêtre de navigation privée.
    • Conseil 3 : Faites en sorte que votre bean implante l'interface HttpSessionBindingListener et mettez en place des traces pour suivre la vie de ce bean.

Une petite application (1h40)

Étape 1 : La servlet

  • Créez une servlet PersonServlet que ne réalise aucune action (pour l'instant) et qui est associée à l'URL /person (code ci-dessous).
    package myapp.servlet;
    
    import java.io.IOException;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    
    /**
     * Une servlet pour les actions sur les personnes.
     */
    @WebServlet(//
            description = "Les actions sur les personnes", //
            urlPatterns = { "/person" })
    public class PersonServlet extends HttpServlet {
        private static final long serialVersionUID = 1L;
    
        /**
         * Requêtes GET
         */
        protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
        ) throws IOException {
            response.setContentType("text/html;charset=UTF-8");
            var writer = response.getWriter();
            writer.printf("<p>");
            // afficher les informations sur la requête
            writer.printf("method = %s</br>", request.getMethod());
            writer.printf("contextPath = %s</br>", request.getContextPath());
            writer.printf("servletPath = %s</br>", request.getServletPath());
            // afficher les paramètres et leurs valeurs
            writer.printf("'a' parameter = %s</br>", request.getParameter("a"));
            request.getParameterMap().forEach((param, values) -> {
                writer.printf("parameter '%s' = ", param);
                writer.printf("= %s</br>", String.join(", ", values));
            });
        }
    
        /**
         * Requêtes POST (la même chose que GET)
         */
        protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response
        ) throws ServletException, IOException {
            doGet(request, response);
        }
    
    }
    
  • Testez ce composant avec plusieurs requêtes /person, /person?aa=Hello&bb=Salut&aa=Bye afin de bien comprendre son fonctionnement.
  • Testez également la méthode HTTP POST avec la commande ci-dessous :
    curl -v -X POST 'http://localhost:8080/person?a=AA&xx=XX'
    

Étape 2 : les données

  • Créez la classe Person ci-dessous :
    package myapp.servlet;
    
    import lombok.Data;
    import lombok.AllArgsConstructor;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Person {
    
        private String id;
        private String name;
        private String mail;
    
        public Person(Person p) {
            this(p.id, p.name, p.mail);
        }
    
    }
    
  • Modifiez votre projet afin d'ajouter une classe métier orientée vers le traitement des personnes (c'est une classe fictive qui travaille uniquement en mémoire). Préparez ensuite une instance de InMemoryPersonService dans la servlet. Si nous avions Spring, nous aurions pu demander une injection.
    package myapp.servlet;
    
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    
    public class InMemoryPersonService {
    
        final private Map<String, Person> persons;
    
        public InMemoryPersonService() {
            persons = Collections.synchronizedMap(new HashMap<>());
            save(new Person("100", "Paul", "paul@hello.fr"));
            save(new Person("200", "Laure", "laure@univamu.fr"));
        }
    
        public Person find(String id) {
            var p = persons.get(id);
            if (p == null) throw new IllegalArgumentException();
            return new Person(p);
        }
    
        public Collection<Person> findAll() {
            return persons.values().stream().map(p -> new Person(p)).toList();
        }
    
        public void save(Person p) {
            persons.put(p.getId(), new Person(p));
        }
    
        public void remove(String id) {
            persons.remove(id);
        }
    
        public Map<String, String> validate(Person p) {
            var errors = new HashMap<String, String>();
            if (p.getId() == null || p.getId().isBlank()) {
                errors.put("id", "ID incorrect");
            }
            if (p.getMail() == null || !p.getMail().matches("^[a-z0-9.]+@[a-z0-9.]+")) {
                errors.put("mail", "e-mail incorrect");
            }
            if (p.getName() == null || p.getName().isBlank()) {
                errors.put("name", "Le nom est obligatoire");
            }
            return errors;
        }
    
    }
    

Étape 3 : lister

Nous allons traiter la requête GET /person afin de lister les personnes.

  • Créer une page JSP lister.jsp qui a pour but de lister une collection de personnes rangée dans la zone request :
    <%@page contentType="text/html;charset=UTF-8" %>
    <%@page pageEncoding="UTF-8" %>
    <%@page import="java.util.List"%>
    <%@page import="myapp.servlet.Person"%>
    
    <jsp:useBean id="persons" type="List<Person>" scope="request" />
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <!-- pour utiliser bootstrap -->
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
        rel="stylesheet">
    </head>
    <body>
    
    <div class="container">
    <h1>Liste des personnes</h1>
    
    <table class='table table-striped'>
        <%
        for (Person person: persons) {
            // nous rangeons la personne dans la zone "page" afin
            // que les expressions ci-dessous fonctionnent
            pageContext.setAttribute("person", person);
            %>
            <tr>
                <td>${person.id}</td>
                <td>${person.name}</td>
                <td>${person.mail}</td>
            </tr>
            <%
        }
    %>
    </table>
    </div>
    <!-- à faire à la fin pour utiliser bootstrap -->
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
    </body>
    </html>
    
  • Dans la servlet, modifier doGet afin de récupérer la liste des personnes, la mettre dans la zone request et appeler la page JSP qui va l'afficher :
    request.setAttribute("persons", manager.findAll());
    // Appeler une page JSP depuis une servlet
    request.getRequestDispatcher("lister.jsp").forward(request, response);
    
  • Essayez sur un navigateur la requête http://localhost:8080/person.
Moralité : Les requêtes sont systématiquement traitées par les servlets et les pages JSP affichent les données préparées par les servlets. C'est une bonne pratique de ne jamais passer directement par les pages JSP, sans, au préalable, être passé par une servlet.
Remarque : La zone request de durée de vie très courte est utilisée comme liaison entre la servlet (qui prépare la données) et la page JSP (qui exploite la donnée).

Étape 3b : simplifier

Dans la page lister.jsp il y a des déclarations que nous allons retrouver dans toutes les pages. Nous allons les regrouper dans une en-tête :

  • Créer la page JSP header.jsp :
    <%@page contentType="text/html;charset=UTF-8" %>
    <%@page pageEncoding="UTF-8" %>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
      rel="stylesheet">
    </head>
    <body>
    <div class="container">
    
  • Nous pouvons maintenant simplifier lister.jsp en incluant dynamiquement la page header.jsp :
    Nouvelle version simplifiée de lister.jsp
    <%@include file="header.jsp" %>
    <%@page import="java.util.List"%>
    <%@page import="myapp.servlet.Person"%>
    
    <jsp:useBean id="persons" type="List<Person>" scope="request" />
    
    ... laisser la suite ...
    
  • Faites la même chose avec une page footer.jsp.

Étape 4 : modifier

Nous allons maintenant traiter la requête GET /person?id=100 afin de modifier la personne en question.

  • Modifier lister.jsp afin d'ajouter un lien de modification :
    <td><a href="person?id=${person.id}">Modifier</a></td>
    
  • Créez une page JSP edition.jsp afin de produire un formulaire HTML d'édition des caractéristiques d'une personne placée dans la zone request. La soumission de ce formulaire va générer la requête POST /myapp/person.
    <%@include file="header.jsp"%>
    
    <h1>Modifier une persone</h1>
    
    <form method="post" action="person">
        <div class="mb-3">
            <label class="form-label">ID :</label>
            <input class="form-control" type="text" value="${person.id}" />
            <span class="badge text-bg-danger">${errors.id}</span>
        </div>
        <div class="mb-3">
            <label class="form-label">Name : </label>
            <input name="name" class="form-control"
                type="text" value="${person.name}" />
            <span class="badge text-bg-danger">${errors.name}</span>
        </div>
        <div class="mb-3">
            <label class="form-label">Mail : </label>
            <input name="mail" class="form-control" 
                type="text" value="${person.mail}" />
            <span class="badge text-bg-danger">${errors.mail}</span>
        </div>
        <input name="ok" class="mb-3 btn btn-primary" type="submit"
            value="Valider" />
    </form>
    
    <%@include file="footer.jsp"%>
    
  • Dans la Servlet : modifier doGet afin de
    • tester la présence du paramètre id,
    • charger la personne,
    • la placer en zone request,
    • appeler la JSP edition.jsp.
  • Tester le bon fonctionnement du lien de modification.
Note : Vous remarquerez que dans cette page il n'y a plus de useBean. En effet, ce dernier est optionnel si nous utilisons uniquement les expressions. La valeur de ${person} et ${errors} est cherchée dans les zones page, request, session et application. Nous y reviendrons.

Étape 6 : traiter

Nous allons maintenant traiter la requête POST /person.

  • Modifier la méthode doPost afin de
    • créer une instance de Person,
    • affecter cette instance avec les paramètres de la requête HTTP (id, name, mail),
    • sauver cette instance avec le manager,
    • effectuer une redirection vers GET /person avec response.sendRedirect("person");
  • Tester le bon fonctionnement.
  • Vérifier que les injections JavaScript fonctionnent (malheureusement). Nous réglerons ce problème plus tard.
Problème : Vérifier que le changement d'ID provoque la création d'une nouvelle personne. Afin d'éviter ce problème, faites en sorte de placer en session (voir ci-dessous) la valeur de l'ID sur la requête GET pour pouvoir le vérifier dans la requête POST.
// pour placer une valeur en session identifiée par "name"
request.getSession().setAttribute("name", value)

Étape 7 : valider et enregistrer

  • Dans la Servlet (méthode doPost) : Ajoutez une phase de validation des données (méthode validate du service).
    • Si les données ne sont pas valides, faites en sorte de revenir au formulaire en proposant les anciennes valeurs.
    • Si les données sont valides, enregistrez l'instance indexée par l'ID de la personne.
  • La page edition.jsp est déjà prévue pour faire apparaitre les messages d'erreur (en rouge) à coté des champs fautifs. Pour utiliser cette possibilité, vous devez placer dans la zone request la Map des erreurs.

Étape 8 : ajouter

  • Ajoutez à votre page lister.jsp le lien ci-dessous afin de pouvoir ajouter une nouvelle personne :
    <p><a href="person?ajout=1">Ajouter une personne</a></p>
    
  • Modifier la méthode doGet pour traiter ce cas.
  • Important : Nous avons déjà placé en session l'ID de la personne modifiée. Faites en sorte de ne rien mettre en session si il s'agit d'un ajout. Modifier la méthode doPost pour traiter les ajouts.

Étape 9 : supprimer

  • Modifiez votre page lister.jsp et votre servlet pour ajouter et traiter le cas de la suppression :
<a href="deletePerson?id=123456">Supprimer cette personne</a>
  • Prévoyez que votre servlet traite également l'URL /deletePerson en modifiant l'annotation :
@WebServlet(//
        description = "Les actions sur les personnes", //
        urlPatterns = { "/person", "/deletePerson" })
  • La servlet utilisera la méthode request.getServletPath() pour savoir sous quel nom elle a été appelée.

Étape 10 : utiliser les routes

  • Afin de rendre les URL plus simples, remplacer /person?ajout=1 par /addPerson.