Java EE - EPSI POE mars 2017 - David Gayerie Licence Creative Commons

Le conteneur Web

  1. Cycle de vie des servlets
  2. Exercice : comprendre le cycle de vie d'une servlet
  3. Servlet et programmation concurrente
  4. Exercice : servlet et programmation concurrente

Nous avons vu au chapitre précédent que les servlets sont des composants Web qui permettent de répondre à des requêtes utilisateurs. Ces servlets sont packagées dans une application Web qui est elle-même déployée dans un serveur d'application Java EE. Cependant, nous n'avons pas eu à écrire de lignes de code telles que :

MaServlet servlet = new MaServlet();

C'est-à-dire que nous n'avons pas eu à instancier nos servlets et pourtant, elles ont bien été créées et utilisées pour générer les réponses dynamiques.

Un serveur Java EE fournit un conteneur Web (parfois appelé conteneur de servlets). Un conteneur a la charge d'instancier, d'initialiser et de détruire les servlets d'une application. C'est également le conteneur qui fournit une instance de HttpServletRequest et de HttpServletResponse pour chaque requête.

Nous allons voir en détail la gestion du cycle de vie des servlets par le conteneur Web et les conséquences que cela a sur la façon de développer une application Web.

Cycle de vie des servlets

Le conteneur Web gère le cycle de vie des servlets : la création, l'initialisation et la destruction. À chacune de ces étapes, une instance de servlet est informée par un appel à une méthode déclarée dans l'interface Servlet et qui peut être surchargée pour chaque servlet.


public void init(ServletConfig config) throws ServletException {
  // appelée au moment de l'initialisation de la servlet
}

public void destroy() {
  // appelée avant la suppression de la servlet du conteneur
}

De plus la servlet sera prévenue de sa création par un appel à son constructeur. Cela a une conséquence importante pour l'implémentation d'une servlet : une servlet doit obligatoirement avoir un constructeur sans paramètre.

Lors de l'appel à la méthode init(ServletConfig), le conteneur passe en paramètre une instance de ServletConfig qui permet, entre-autres, à la servlet de récupérer des paramètres d'initialisation. Notez que la méthode init(ServletConfig) autorise l'implémentation à jeter une ServletException. Si cela se produit, le conteneur considère que la servlet n'a pas pu s'initialiser correctement et elle ne sera pas déployée dans le conteneur : elle ne sera donc pas accessible !

Exercice : comprendre le cycle de vie d'une servlet

Objectif
Déployer une servlet qui trace les différentes étapes de son cycle de vie.
Modèle Maven du projet à télécharger
webapp-template.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. Modifier ensuite la section <developers> pour indiquer vos nom et email.
Intégration du projet dans Eclipse
L'intégration du projet dans Eclipse suit la même procédure que celle vue lors de l'introduction à Maven

Pour cet exercice, vous allez déployer une servlet de log dont voici le code source :


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("/log")
public class LogServlet extends HttpServlet {

  private static final long serialVersionUID = 7446985734933559486L;

  @Override
  public void init() throws ServletException {
    this.log("################################# init " + getServletName());
  }

  @Override
  public void destroy() {
    this.log("################################# destroy " + getServletName());
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                      throws ServletException, IOException {
    resp.setCharacterEncoding("utf-8");
    resp.setContentType("text/plain");
    resp.getWriter().write(getServletName() + " called successfully");
  }

}

Vérifier dans les logs du serveur (onglet Console sous Eclipse) l'apparition des messages de log produits par la servlet lorsque vous faites les opérations suivantes :

N'hésitez pas à tester plusieurs combinaisons possibles de ces actions.

Que pouvez-vous en déduire concernant la façon dont un conteneur Web gère la création, l'initialisation et la suppression d'une servlet ?

Variation
Si maintenant vous modifiez l'annotation @WebServlet de la façon suivante :
@WebServlet(urlPatterns = "/log", loadOnStartup = 0)

Refaites l'exercice en essayant de constater si cela produit un changement dans le cycle de vie de la servlet.

Servlet et programmation concurrente

Avec l'exercice précédent, nous avons mis en lumière le fait que le conteneur Web ne crée qu'une seule instance de chaque servlet. En fait, le conteneur est libre d'adopter la stratégie qui lui paraît la meilleure. Nous avons également constater que nous pouvons changer le moment où le conteneur instanciera une servlet grâce à l'attribut loadOnStartup (cette option est disponible également dans le fichier de déploiement web.xml avec la balise <load-on-startup>).

En tant que développeur de servlet, il faut donc toujours garder à l'esprit qu'une même instance de servlet sera utilisée pour servir plusieurs requêtes HTTP, y compris des requêtes simultanées. Cela introduit dans le développement de servlet, le problème de la programmation concurrente. Tout changement de l'état interne d'une servlet peut entraîner un bug potentiel pour des requêtes qui sont traitées en parallèle.

Nous verrons que la problèmatique de programmation concurrente est récurrente dans le développement d'application Java EE.

Exercice : servlet et programmation concurrente

Objectif
Comprendre les risques de bug dans un contexte d'exécution concurrent.

Plusieurs implémentations de servlet sont proposées ci-dessous. Toutes posent un problème d'exécution dans un environnement concurrent (elles ne sont pas thread-safe). Cherchez d'où provient le problème et quelles solutions proposeriez-vous pour le corriger.

Cas 1 : la servlet de calcul

import java.io.IOException;

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

public class SumServlet extends HttpServlet {

  private static final long serialVersionUID = -7059227478134291799L;

  private int total;

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
                      throws ServletException, IOException {
    total = 0;
    for (String value : req.getParameterValues("value")) {
      total += Integer.parseInt(value);
    }
    resp.setCharacterEncoding("UTF-8");
    resp.setContentType("text/plain");
    resp.getWriter().write("The total is " + total);
  }

}

Cas 2 : la servlet de temps

import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;

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

public class TimeServlet extends HttpServlet {

  private static final long serialVersionUID = 7446985734933559486L;
  private final DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.LONG);

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                      throws ServletException, IOException {
    resp.setCharacterEncoding("UTF-8");
    resp.setContentType("text/plain");
    resp.getWriter().write(dateInstance.format(new Date()));
  }

}

Cas 3 : la servlet d'inscription d'un utilisateur

import java.io.IOException;

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

public class SubscriptionServlet extends HttpServlet {

  private static final long serialVersionUID = 7446985734933559486L;

  private HttpServletRequest firstStepRequest;

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                      throws ServletException, IOException {
    if ("first".equals(req.getParameter("step"))) {
      this.firstStepRequest = req;
      generateSecondStepPage(resp);
    }
    else {
      String name = firstStepRequest.getParameter("name");
      String age = firstStepRequest.getParameter("age");
      String address = req.getParameter("address");
      String city = req.getParameter("city");
      createSubscription(name, age, address, city);
      generateSubscriptionSuccessPage(resp);
    }
  }

  private void generateSecondStepPage(HttpServletResponse resp) throws IOException {
    // ...
  }

  private void generateSubscriptionSuccessPage(HttpServletResponse resp) 
                                              throws IOException {
    // ...
  }

  private void createSubscription(String name, String age, String address, String city) {
    // ...
  }
}