Accueil > Le langage JSP et les Servlets Java > Conseils Pratiques pour Servlet, Partie 1

Conseils Pratiques pour Servlet, Partie 1

Par Les Auteurs Java d’Oreilly

Traduit par Olivier, le 26/12/2002

Dans le premier de ces trois articles sur les conseils pratiques pour Servlet, tirés de Java Enterprise Best Practices, vous apprendrez comment travailler efficacement avec les Servlets.

Depuis leur introduction en 1996, les servlets ont dominé le paysage Java dans le domaine serveur et sont devenues une façon standard d’interfacer Java avec le Web. Elles sont une technologies fondamentale sur laquelle les développeurs Java construisent les applications web et, de façon croissante, les services web. Ce chapitre vous donnera des conseils pratiques pour le développement et le déploiement d’applications web basées sur les servlets.

Travailler de Manière Efficace avec les Servlets

Nous commencerons en prenant connaissance de quelques frameworks pour servlets. Les frameworks (comme Struts du groupe Apache) deviennent de plus en plus populaires car ils améliorent de façon efficace la productivité du développeur en lui fournissant un squelette sur lequel construire son application. Dans la première section, nous examinerons ce que les frameworks pour servlets offrent, et je vous donnerai un rapide aperçu des frameworks les plus populaires. Après cela, nous passerons d’une approche haut niveau à une approche bas niveau dans une discussion sur comment optimiser les performances des servlets en utilisant des caractères préencodés. Ensuite, nous attaquerons le sujet épineux du chargement de fichiers de configuration et nous présenterons un code pour rendre la tâche plus aisée, et après je donnerai quelques petits conseils sur quand vous devriez utiliser (ou non) HttpSession et SingleThreadModel. Vers la fin du chapitre, j’expliquerai comment gérer de manière efficace le cache pour améliorer les performances. Puis je répondrai à la question la plus fréquemment posée : “Comment puis-je envoyer un fichier au client de telle manière qu’il voie apparaître la boîte de dialogue ‘Sauvegarder Sous’ ?”. Comme vous le verrez, la réponse est liée à une bonne configuration des en-têtes HTTP.

Choisir le Bon Framework pour Servlet

Quand on implémente une application web, il est bon de se rappeler que la technologie servlet est ouverte. Il était facile de l’oublier lors de ses premiers balbutiements, l’API Servlet étant tout ce que nous avions pour la programmation Java côté serveur. Si l’API Servlet manquait de quelque chose, il nous fallait l’implémenter nous-même. C’était un peu comme au Moyen-Age quand les temps étaient durs et que les vrais programmeurs codaient leurs servlets à la main. Les spécifications n’étaient pas écritent. On se croyait chanceux d’avoir out.println().

Aujourd’hui, les choses ont changé. De nombreux développeurs sont venus grossir les rangs, et avec eux nous avons vu venir une multitude de technologies se créer pour rendre le développement d’applications web plus simple et plus efficace. Le premier domaine d’innovation est apparu au niveau de la couche de présentation. Des technologies telles que JavaServer Pages (JSP), WebMacro et Velocity sont des alternatives plus productives que le out.println() d’origine. Ces technologies rendent le développement, déploiement et maintenance dynamique du contenu du site plus facile que jamais. Vous trouverez une discussion complète de ces technologies et d’autres dans mon livre Java Servlet Programming, Second Edition (O’Reilly).

Aujourd’hui nous voyons arriver une nouvelle vague d’innovation sous la couche de présentation, au niveau du framework (voir Figure 3-1). Ces nouveaux frameworks proposent un échaffaudage solide avec lequel les applications web peuvent être construites, passant du développement rapide de pages à la construction rapide d’applications. Les frameworks utilisent les meilleurs designs des experts et les rendent accessibles pour la réutilisation. De bons frameworks vous aident à améliorer la modularisation et la maintenabilité de vos applications. Les frameworks apportent également des technologies disparates en un seul module et fournissent des composants qui s’appuient sur ces technologies pour résoudre des problèmes communs. Si vous choisissez le bon framework vous pouvez augmenter considérablement votre productivité. Par conséquent, je vous conseille d’utiliser un framework et vous donne quelques informations utiles pour faire le bon choix.


Conseils pour choisir un framework

Quand vous choisissez un framework, il est important de considérer ce qu’il offre. Voici quelques services proposés par les frameworks. Tous les frameworks ne proposent pas ces services et cette liste ne doit pas non plus être considérée comme exhaustive.

Intégration à un langage de modèle
Certains frameworks s’intègrent avec un langage de modèle particulier. D’autres sont plus flexibles et supportent plusieurs modèles, mais sont souvent optimisés pour un langage particulier. Si vous avez une préférence pour un langage particulier assurez-vous que le framework le supporte bien.

Support (idéalement imposition) de la séparation designeur / developpeur
Un des objectifs communs du développement d’application web est de séparer de manière efficace les zones de travail du designeur et du développeur. Choisir le bon langage de modèle aide ici, mais le choix du framework peut avoir encore plus d’impact. Certains permettent cette séparation, d’autres l’imposent.

Sécurité
Le modèle de gestion des accès et de sécurité fourni par défaut avec les servlets fonctionne bien pour les tâches simples mais n’est pas extensible pour des besoins plus perfectionnés. Certains frameworks proposent un modèle de sécurité alternatif, et nombreux sont ceux qui supporte l’adjonction de modèles de sécurité tiers. Si vous avez besoin d’une sécurité avancée, le bon framework peut aider.

Validation des Formes
Les frameworks proposent en général des outils pour valider les données des formes HTML, permettant au framework d’effectuer une validation des paramètres avant même que la servlet ne voit les données par exemple. Certains permettent la création facile de “formes assistantes” avec des boutons Suivant / Précédent et une conservation des valeurs.

Gestion des Erreurs
Certains frameworks offrent une gestion d’erreurs avancée ou personnalisable telle que l’envoi d’email d’alerte, écriture des erreurs dans un fichier particulier, formatage automatique des erreurs pour l’utilisateur et / ou l’administrateur.

Persistance / intégration aux bases de données
Un des services les plus puissants offerts par les frameworks est leur forte et élégante intégration avec les bases de données par exemple. Ces frameworks permettent à l’utilisateur de penser objets plutôt qu’en SQL.

Internationalisation
L’internationalisation (i18n) est toujours difficile, mais certains frameworks en simplifient le processus.

Intégration à un environnement de développement
Certains frameworks ont leur propre environnement de développement intégré (IDE) ou / et peuvent être utilisés avec un IDE tiers.

Mécanisme pour supporter les services web
Avec l’intérêt croissant pour les services web, il est courant de voir les nouveaux frameworks se concentrer autour des services web, ou des frameworks existants s’enrichir de ces fonctionnalités.

Au-delà des fonctionnalités, un second critère est important lorsque vous évaluez un framework : sa licence. Mon conseil est de vous cantonner aux projets open source ou aux standards implémentés par de multiples sociétés. Cela pour protéger votre investissement. Open source et standards vous évitent de n’avoir à dialoguer qu’avec un seul partenaire et vous assure qu’un terme au support de votre framework sur lequel dépend votre application ne peut pas être mis par une seule entité.

Un troisième critère est de déterminer à qui est destiné le framework (site de news, portail, commerce…etc…). Différents types de sites ont différent besoins, et les frameworks ont tendance à être optimisés pour certains segments de marché. Vous pourriez trouver utile de rechercher quels frameworks sont utilisés par d’autres sites implémentant des applications semblables à la vôtre.

Frameworks Populaires

Bien qu’il serait merveilleux de faire ici une comparaison complète entre les frameworks, ce n’est pas le but de ce livre. Ce que nous pouvons faire en revanche c’est comparer les frameworks les plus populaires aujourd’hui : Java 2 Enterprise Edition (J2EE) BluePrints, Apache Struts, JavaServer Faces et Apache Turbine.

Vous vous dites en vous-mêmes, “Oublions la comparaison et dites-moi simplement quel est le meilleur !” Malheureusement, il n’y a pas de solution universelle ; tout dépend de votre application et de votre préférence personnelle. Voici un cas où travailler avec Java suit le slogan pour Perl : “Il existe plus d’une façon de le faire”.

J2EE BluePrints
J2EE BluePrints se décline plus comme un guide qu’un framework. Ecrit par les ingénieurs de Sun, le livre offre des conseils, des modèles et des exemples de code montrant comment exploiter au mieux J2EE et les technologies constituantes. Par exemple, le livre décrit comment implémenter un framework Modèle-Vue-Contrôleur (MVC) (NdT : Voir notre article sur le sujet) qui encapsule la logique web côté serveur en trois parties : un modèle représentant les données centrales, la vue gérant l’affichage des données, et le contrôleur gérant la modification des données. Pour supporter ce modèle MVC, le livre suggère d’utiliser une classe Action dans le style du modèle “Commande” :

L’application en exemple définit une classe abstraite Action qui représente un type d’opération. Un contrôleur peut rechercher des sous-classes concrètes d’Action par nom et leur déléguer les requêtes.

Le livre fournit des exemples de code pour implémenter une Action mais n’est pas un support suffisant pour développer une application réelle. Pour cela, le livre propose aux lecteurs de se retourner vers Struts du groupe Apache (NdT : Voir notre article sur le sujet) .

Struts
Struts pourrait bien être le framework pour servlet le plus populaire. Il suit le modèle MVC discuté dans les J2EE BluePrints (ce que je peux dire, c’est que les idées sont venues des deux sources) :

Struts est très configurable et propose une longue liste de services (et qui continue à grandir), incluant un Contrôleur d’entrée, des classes d’actions et d’association, des classes utilitaires pour XML, initialisation automatique des JavaBeans côté serveur, validation des formes HTML et support pour l’internationalisation. Il comprend également un ensemble de tags pour accéder au contenu de l’application serveur, créer le code HTML, effectuer des opérations logiques, créer des modèles. Certaines companies ont commencé à adopter Struts et prêchent en sa faveur. Struts est très populaire et est suffisamment robuste pour pouvoir être utilisé dans de larges applications.

Avec Struts, les requêtes sont acheminées à travers une servlet contrôleur. Les objets Action gèrent les requêtes, et ces actions utilisent des composants tels que des JavaBeans pour effectuer la logique métier. Struts crée de façon élégante un mécanisme de distribution s’appuyant sur les servlets avec une configuration externe, découplant les URLs et les opérations serveurs. Quasiment toutes les requêtes passent au travers de la même servlet, les requêtes client spécifient dans la requête l’action qu’elles voudraient déclencher (comme se logger, ajouter au panier, payer), et le contrôleur Struts envoie la requête vers une Action pour être exécutée. Les JSP sont utilisées pour la couche de présentation, bien que Struts puisse également utiliser Velocity du groupe Apache et d’autres technologies. Struts est un projet open source et fût développé sous le modèle de développement ouvert et collaboratif d’Apache.

JavaServer Faces
Les JavaServer Faces (JSF) est un effort de la Java Community Process (JSR-127) mené par Sun qui est toujours en voie de développement. Il vient juste d’atteindre l’étape de Community Review au moment de l’écriture de cet article, mais son concept prend de plus en plus d’importance dans les esprits. Le document d’introduction des JSF contient les lignes pour définir un framework standard pour applications web, mais le produit semble plus se concentrer sur l’objectif limité de définition d’un cycle de traitement de requêtes pour les requêtes qui sont décomposées en plusieurs phases (commes les Assistants par exemple). Un des objectif de JSF est de pouvoir facilement s’intégrer à Struts.

Turbine du groupe Apache
Turbine est peut-être un des plus vieux frameworks pour servlet, existant depuis 1999. Il propose des services pour gérer l’analyse et la validation des paramètres, création de pool de connection, plannification de tâches, gestion de cache, abstraction de la base de données et même XML-RPC. Plusieurs de ses composants peuvent être utilisés de façon autonome tel que l’outil Torque pour l’abstraction de la base de données. Turbine les regroupe ensemble et propose une plate-forme solide pour la construction d’applications web, de la même manière que J2EE marche pour les applications d’entreprises.

Turbine comme d’autres frameworks est basé sur le modèle MVC et l’abstraction des événements d’action. Cependant en plus des autres, il offre des fonctionnalités supplémentaires au niveau de la couche Vue et s’est surnommé “Modèle 2+1″ pour signifier qu’il était meilleur que le MVC standard “Model 2″. Les Vues de Turbine supportent de nombreux moteurs de modèle mais Velocity reste le plus approprié.

Nous pourrions présenter d’autres frameworks si nous avions la place. Si vous voulez en savoir plus, cherchez sur Google les mot clés suivants : TeaServlet, Apache Cocoon, Enhydra Barracuda, JCorporate Expresso et Japple.

Utilisation de caractères pré-encodés

Une des première chose que vous apprenez lorsque vous commencez à programmer des servlets et d’utiliser un PrintWriter pour écrire des caractères et un OutputStream pour écrire des bytes. Et bien que se soit un conseil valide pour le style, il est aussi un peu simpliste. Voici la vérité: ce n’est pas parce que vous allez écrire des caractères qu’il faut que vous utilisiez PrintWriter!

Un PrintWriter à ses inconvénients: plus exactement, il doit encoder en interne tous les caractères char en une séquence de byte. Lorsque vos données sont déjà encodées –c’est le cas pour le contenu d’un fichier, d’URL ou de base de données, ou même d’une String en mémoire– il est préférable d’utiliser les streams. De cette manière, vous pouvez utiliser un transfert direct de byte à byte. Excepté pour les rares cas où vous avez un conflit d’ensemble de caractère (charset) entre l’encodage de la chaîne stockée et l’encodage requis, il n’est pas besoin de décoder le contenu en une String pour l’encoder de nouveau en bytes pour le client. Utilisez les caractères pré-encodés et vous pourrez gagner du temps processeur.

En exemple, la servlet dans l’exemple 3-1 utilise un reader pour lire le texte d’un fichier et un writer pour écrire le texte au niveau du client. Bien qu’il utilise le couple sacro-saint de classes Reader/Writer pour le texte, il entraîne une conversion inutile.

Exemple 3-1 : Caractères en entrée, caractères en sortie

import java.io.*;
import java.util.prefs.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WastedConversions extends HttpServlet {

  // Random file, for demo purposes only
  String name = "content.txt";

  public void doGet(HttpServletRequest req, HttpServletResponse res)
                              throws ServletException, IOException {

    String file = getServletContext(  ).getRealPath(name);

    res.setContentType("text/plain");
    PrintWriter out = res.getWriter(  );

    returnFile(file, out);
  }

  public static void returnFile(String filename, Writer out)
                             throws FileNotFoundException, IOException {
    Reader in = null;
    try {
      in = new BufferedReader(new FileReader(filename));
      char[  ] buf = new char[4 * 1024];  // 4K char buffer
      int charsRead;
      while ((charsRead = in.read(buf)) != -1) {
        out.write(buf, 0, charsRead);
      }
    }
    finally {
      if (in != null) in.close(  );
    }
  }
}

La servlet de l’exemple 3-2 est plus appropriée pour retourner le contenu d’un fichier texte. Cette servlet reconnait que le contenu du fichier commence par des bytes et peut donc être envoyé directement sous forme de bytes, tant que l’encodage correspond à celui attendu par le client.

Exemple 3-2 : Bytes en entrée, bytes en sortie

import java.io.*;
import java.util.prefs.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class NoConversions extends HttpServlet {

  String name = "content.txt";  // Demo file to send

  public void doGet(HttpServletRequest req, HttpServletResponse res)
                               throws ServletException, IOException {
    String file = getServletContext(  ).getRealPath(name);

    res.setContentType("text/plain");
    OutputStream out = res.getOutputStream(  );

    returnFile(file, out);
  }

  public static void returnFile(String filename, OutputStream out)
                             throws FileNotFoundException, IOException {
    InputStream in = null;
    try {
      in = new BufferedInputStream(new FileInputStream(filename));
      byte[  ] buf = new byte[4 * 1024];  // 4K buffer
      int bytesRead;
      while ((bytesRead = in.read(buf)) != -1) {
        out.write(buf, 0, bytesRead);
      }
    }
    finally {
      if (in != null) in.close(  );
    }
  }
}

Le gain en performance par l’utilisation de caractères encodés dépend de votre serveur. En testant ces deux servlets avec un fichier de 2Mo accédé localement, on obtient un gain de 20% avec Tomcat 3.x. Avec Tomcat 4.x, le gain monte jusqu’à 50%. Bien que ces chiffres semblent impressionnants, on considère que l’application ne fait rien d’autre que transférer des fichiers texte. Les chiffres en situation réelle dépendent de la logique métier de votre servlet. Cette technique (illustrée dans la Figure 3-2) est plus applicable pour des applications très consommatrice en bande passante ou en temps processeur.

Figure 3-2. Utilisation des caractères pré-encodés
Figure 3-2. Utilisation des caractères pré-encodés

Le principe “utilisez les caractères pré-encodés” s’applique quand une grande majorité de votre contenu source est pré-encodé, comme c’est le cas avec le contenu de fichiers, URLs et même bases de données. Par exemple, utiliser la méthode ResultSet getAsciiStream() au lieu de getCharacterStream() peut éviter la conversion pour les chaînes ASCII—en lecture depuis la base de données comme en écriture vers le client. Il est également possible de réduire de moitié la consommation de bande passante entre le serveur et la base de données parce que les chaînes ASCII peuvent être moitié plus petites que les chaînes UCS-2. Le gain observé dépendra, bien sûr, de votre base de données et comment elle stocke et transfert les données.

Certain développeurs de servlet vont jusqu’à pré-encoder leurs String static avec String.getByte() pour qu’elles ne soient encodées qu’une seule fois. Est-ce que le gain de performances justifie une pratique si extrême est une affaire de goût. Je vous le conseille uniquement quand les performances restent un problème même après l’emploi d’une solution plus simple.

Mélanger bytes et caractères en sortie est plus simple qu’il n’y parait. L’exemple 3-3 montre comment mélanger les types en sortie, en utilisant ServletOutputStream et sa combinaison de méthodes write(byte[ ]) et println(String)

Exemple 3-3 : ValueObjectProxy.java

import java.io.*;
import java.sql.*;
import java.util.Date;
import javax.servlet.*;
import javax.servlet.http.*;

public class AsciiResult extends HttpServlet {

  public void doGet(HttpServletRequest req, HttpServletResponse res)
                               throws ServletException, IOException {
    res.setContentType("text/html");
    ServletOutputStream out = res.getOutputStream(  );

    // ServletOutputStream comporte des méthodes println(  ) pour écrire des chaînes.
    // L'appel de println(  ) ne fonctionne que pour les encodages des caractères mono-octet.
    // Si vous avez besoin d'un encodage multi-octets, assurez-vous de configurer le charset
	// dans Content-Type et d'utiliser, par exemple, out.write(str.getBytes("Shift_JIS")
	// pour le Japonais.
    out.println("Content current as of");
    out.println(new Date(  ).toString(  ));

    // On récupère ici le ResultSet d'une base de données.

    try {
      InputStream ascii = resultSet.getAsciiStream(1);
      returnStream(ascii, out);
    }
    catch (SQLException e) {
      throw new ServletException(e);
    }
  }

  public static void returnStream(InputStream in, OutputStream out)
                             throws FileNotFoundException, IOException {
    byte[  ] buf = new byte[4 * 1024];  // 4K buffer
    int bytesRead;
    while ((bytesRead = in.read(buf)) != -1) {
      out.write(buf, 0, bytesRead);
    }
  }
}

Bien que mélanger bytes et caractères puisse augmenter les performances parce que les bytes sont transférés directement, je vous recommande de peu utiliser cette technique parce qu’elle n’est pas très claire pour le lecteur du code et peut entraîner des erreurs si vous n’êtes pas complètement familier avec le mode de fonctionnement des jeux de caractères (charset). Si vous avez besoin d’utiliser autre chose que des caractères ASCII, soyez sûrs de ce que vous faites. Ecrire en sortie des caractères non-ASCII n’est pas réservé aux novices.

Charger des Fichiers de Configuration depuis le Classpath

Depuis l’API 1.0 jusqu’à l’API 2.3, les servlets manquent d’un mécanisme standard pour gérer les fichiers de configuration externes. Bien que de nombreuses librairies nécessitent des fichiers de configuration, les servlets n’offrent pas de méthode communément acceptée de les retrouver. Quand une servlet fonctionne sous J2EE, elle peut utiliser JNDI, qui offre un certain nombres d’informations sur la configuration. Mais pour les serveurs web de base, le problème du fichier de configuration demeure.

La meilleure solution (ou devrais-je plutôt dire la moins pire) est de localiser les fichiers en recherchant sur le classpath et/ou le ressource path. Cela permet à l’administrateur du site de placer les fichiers de configuration communs à toutes les applications dans le classpath du serveur, ou de placer les fichiers de configuration spécifiques à chacune des applications dans WEB-INF/classes trouvé dans le ressource path. Cela est également valable pour localiser les fichiers de configuration placés dans les fichiers WAR et/ou déployés sur d’autres serveurs web. En fait, l’utilisation de fichiers de configuration présente plusieurs avantages, même quand JNDI est disponible. Le fournisseur de composants peut proposer des fichiers de configurations “exemples” ou “par défaut”. Un fichier de configuration peut-être utilisé pour le serveur entier. Et enfin, les fichiers de configuration sont très faciles à comprendre pour le développeur et le déployeur.

L’exemple 3-4 présente la technique de recherche pour une classe nommée Resource. Un nom de resource étant spécifié, le constructeur de Resource cherche au niveau du classpath et du ressource path pour tenter de localiser la ressource. Quand celle-ci est trouvée, son contenu, le répertoire où elle réside ainsi que la date de dernière modification sont disponibles (s’ils existent). La date de dernière modification permet à l’application de savoir quand recharger les données de configuration. La classe utilise un code spécial pour convertir les resources file: URL en objets File. C’est pratique car les URLs, même les file: URL, n’exposent pas toujours des informations telle que la date de modification. En recherchant au niveau du classpath et du ressource path, cette classe peut trouver les ressources accessibles à toutes les applications du serveur web et les ressources spécifiques à chaque application. Le code source pour cette classe peut-être téléchargé depuis http://www.servlets.com.

Exemple 3-4 : Un localisateur de Resource standard

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * Une classe permettant de localiser des ressources, récupérer leurs contenus,
 * et déterminer leur date de dernière modification.
 * Pour trouver la ressources, la classe cherche d'abord dans le CLASSPATH,
 * puis dans Resource.class.getResource("/" + name). Si la ressource trouve
 * un "file:" URL, le chemin d'accès au fichier sera traité comme un fichier.
 * Sinon, le chemin d'accès est traité comme une URL et comporte alors des
 * informations limitées de dernière modification.
 */
public class Resource implements Serializable {

  private String name;
  private File file;
  private URL url;

  public Resource(String name) throws IOException {
    this.name = name;
    SecurityException exception = null;

    try {
      // Recherche avec CLASSPATH. Si trouvé, "file" est positionné et l'appel
      // retourne "true".  Une erreur SecurityException peut survenir.
      if (tryClasspath(name)) {
        return;
      }
    }
    catch (SecurityException e) {
      exception = e;  // Sauvegardé pour plus tard.
    }

    try {
      // Recherche en utilisant la méthode getResource(  ) de classloader.
      // Si un fichier est trouvé, "file" est positionné ; si une URL est trouvée,
      // "url" est positionnée.
      if (tryLoader(name)) {
        return;
      }
    }
    catch (SecurityException e) {
      exception = e;  // Save for later.
    }

    // Si vous arrivez ici, quelque chose s'est mal passé.
    // Retournez l'exception.
    String msg = "";
    if (exception != null) {
      msg = ": " + exception;
    }

    throw new IOException("La Ressource '" + name + "' n'a pu être trouvée dans " +
      "le CLASSPATH (" + System.getProperty("java.class.path") +
      "), et n'a pu être localisée par le classloader responsable de " +
      "l'application web (WEB-INF/classes)" + msg);
  }

  /**
   * Renvoie le nom de la ressource, telle que passée au constructeur
   */
  public String getName(  ) {
    return name;
  }

  /**
   * Renvoie un flux de données pour lire le contenu de la ressource
   */
  public InputStream getInputStream(  ) throws IOException {
    if (file != null) {
      return new BufferedInputStream(new FileInputStream(file));
    }
    else if (url != null) {
      return new BufferedInputStream(url.openStream(  ));
    }
    return null;
  }

  /**
   * Retourne la date de dernière modification de la ressource. Si la ressource
   * a été trouvée en utilisant une URL, cette méthode ne marchera que si la
   * connexion à l'URL supporte les informations de dernière modification.
   * S'il n'y a pas de support, Long.MAX_VALUE est retourné. -1 devrait peut-être
   * être retrourné, mais vous devriez retourner MAX_VALUE en supposant que si
   * vous ne pouvez déterminer la date, la ressource est récente.
   */
  public long lastModified(  ) {
    if (file != null) {
      return file.lastModified(  );
    }
    else if (url != null) {
      try {
        return url.openConnection(  ).getLastModified(  );  // Je vous salue Marie
      }
      catch (IOException e) { return Long.MAX_VALUE; }
    }
    return 0;  // can't happen
  }

  /**
   * Retourne le répertoire contenant la ressource, ou 'null' si la ressource
   * n'est pas directement disponible sur le système de fichiers.
   * Cette valeur peutr être utilisée pour localiser le fichier de configuration sur le disque,
   * ou pour écrire des fichiers dans le même répertoire.
   */
  public String getDirectory(  ) {
    if (file != null) {
      return file.getParent(  );
    }
    else if (url != null) {
      return null;
    }
    return null;
  }

  // Retourne 'true' si trouvé
  private boolean tryClasspath(String filename) {
    String classpath = System.getProperty("java.class.path");
    String[  ] paths = split(classpath, File.pathSeparator);
    file = searchDirectories(paths, filename);
    return (file != null);
  }

  private static File searchDirectories(String[  ] paths, String filename) {
    SecurityException exception = null;
    for (int i = 0; i < paths.length; i++) {
      try {
        File file = new File(paths[i], filename);
        if (file.exists(  ) && !file.isDirectory(  )) {
          return file;
        }
      }
      catch (SecurityException e) {
        // Les erreurs de sécurité peuvent généralement être ignorées,
        // mais si tout échoue dans la recherche, on rapporte
        // la dernière erreur de sécurité.
        exception = e;
      }
    }
    // N'ai rien pu trouvé
    if (exception != null) {
      throw exception;
    }
    else {
      return null;
    }
  }

  // Découpe une chaîne en morceaux en fonction d'un délimiteur.
  // Utilise les classes JDK 1.1 pour la compatibilité descendante.
  // JDK 1.4 comporte maintenant une méthode de découpe split(  ).
  private static String[  ] split(String str, String delim) {
    // Utilise un vecteur pour conserver les chaînes découpées.
    Vector v = new Vector(  );

    // Utilise un StringTokenizer pour effectuer la découpe.
    StringTokenizer tokenizer = new StringTokenizer(str, delim);
    while (tokenizer.hasMoreTokens(  )) {
      v.addElement(tokenizer.nextToken(  ));
    }

    String[  ] ret = new String[v.size(  )];
    v.copyInto(ret);
    return ret;
  }

  // Retourne 'true' si trouvé
  private boolean tryLoader(String name) {
    name = "/" + name;
    URL res = Resource.class.getResource(name);
    if (res =  = null) {
      return false;
    }

    // Essaie de convertir une URL en File.
    File resFile = urlToFile(res);
    if (resFile != null) {
      file = resFile;
    }
    else {
      url = res;
    }
  }

  private static File urlToFile(URL res) {
    String externalForm = res.toExternalForm(  );
    if (externalForm.startsWith("file:")) {
      return new File(externalForm.substring(5));
    }
    return null;
  }

  public String toString(  ) {
    return "[Resource: File: " + file + " URL: " + url + "]";
  }
}

L’exemple 3-4 présente d’une manière suffisamment réaliste comment la classe peut-être utilisée. Imaginez que le composant de votre librairie pour serlvet ait besoin de charger un bloc de données brutes depuis le système de fichiers. Le fichier peut avoir n’importe quel nom, mais ce nom doit exister dans le fichier de configuration principal : library.properties. Comme le traitement des données brutes peut parfois prendre du temps, la librarie conserve une version sérialisée des données dans un second fichier appelé library.ser pour réduire le temps de chargement. Le fichier cache, s’il existe, est placé dans le même répertoire que le fichier de configuration principal. L’exemple 3-5 présente le code implémentant cette logique, en utilisant la classe Resource.

Exemple 3-5 : Charger les Informations de Configuration depuis une Ressource

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class LibraryLoader {

  static final String CONFIG_FILE = "library.properties";
  static final String CACHE_FILE =  "library.ser";

  public ConfigData load(  ) throws IOException {
    // Trouve le fichier de configuration et rapporte son contenu en tant que Properties.
    Resource config = new Resource(CONFIG_FILE);
    Properties props = new Properties(  );
    InputStream in = null;
    try {
      in = config.getInputStream(  );
      props.load(in);
    }
    finally {
      if (in != null) in.close(  );
    }

    // Détermine le répertoire source du fichier de configuration et
    // recherche un fichier cache proche contenant une représentation
    // complète de l'état de votre programme. Si vous trouvez un fichier cache
    // et qu'il est actuel, chargez et retournez cette donnée.
    if (config.getDirectory(  ) != null) {
      File cache = new File(config.getDirectory(  ), CACHE_FILE);
      if (cache.exists(  ) &&
            cache.lastModified(  ) >= config.lastModified(  )) {
        try {
          return loadCache(new FileInputStream(cache));
        }
        catch (IOException ignored) { }
      }
    }

    // Vous arrivez ici s'il n'y a pas de fichier cache ou s'il est périmé
    // et que vous devez effectuer un rechargement complet. Localise le nom du
    // fichier de données brutes à partir du fichier de configuration
    // et retourne son contenu en utilisant Resource.
    Resource data = new Resource(props.getProperty("data.file"));
    return loadData(data.getInputStream(  ));
  }

  private ConfigData loadCache(InputStream in) {
    // Lit le fichier, peut-être en tant qu'objet séquentiel.
    return null;
  }

  private ConfigData loadData(InputStream in) {
    // Lit le fichier, peut-être en tant qu'XML.
    return null;
  }

  class ConfigData {
    // Un exemple de classe qui pourrait détenir les données de configuration
  }
}

Le code de chargement n’a pas besoin de savoir où est placée la ressource. La classe Resource recherche au niveau du classpath et du ressource path et l’en extrait du WAR si nécessaire.

Utilisez les Sessions comme Cache Local

Les sessions de Servlet, telles qu’elles sont implémentées par HttpSession, offrent un mécanisme simple et pratique de stocker de l’information concernant l’utilisateur. Bien que les sessions soient des outils utiles, il est important de connaitre leurs limites. Dans le cadre d’une application réelle, il est préférable de ne pas les utiliser comme espace de stockage, bien qu’il soit tentant de le faire. Il est mieux de les utiliser comme un cache local—un espace où stocker les informations, qui si elles sont perdues, peuvent être ignorées sans inconvénient ou retrouvées.

Pour en comprendre la raison, nous allons rapidement revoir comment fonctionnent les sessions. Les sessions utilisent généralement des cookies pour identifier les utilisateurs. Lors de la première requête d’un client au serveur, le serveur initialise un cookie spécial sur le client qui conserve un numéro d’identification unique généré par le serveur. Lors d’une requête ultérieure, le serveur peut utiliser le cookie pour reconnaitre si la requête provient du même client. Le serveur conserve une hashtable qui associe la clé (le numéro d’identification du cookie) à sa valeur (l’objet HttpSession). Quand une servlet appelle request.getSession(), le server obtient le numéro d’identification du cookie, cherche l’HttpSession correspondante, et la renvoie. Pour gérer la mémoire, après une certaine période d’inactivité (typiquement 30 minutes) ou à la demande du programmeur, la session expire et le garbage collector (NdT : Collecteur de déchets) libère la mémoire de ces données.

Les données de session sont par nature temporaires et volatiles. Les données de session seront perdues quand une session expire, quand un client ferme son navigateur, change de navigateur, change de machine, ou quand une servlet invalide la session pour délogger l’utilisateur. Par conséquent, les sessions doivent être utilisées pour stocker des informations temporaires non importantes—parce qu’elles n’ont pas un caractère permanent ou qu’elles sont accessibles depuis une zone de stockage plus fiable.

Quand les informations ont un caractère persistant, je vous recommande d’utiliser une base de données, un EJB s’appuyant sur une base de données ou toute autre forme stockage de données. Ces derniers sont plus sûrs, plus portables, plus fiables et sont adaptés aux backups. Si une donnée doit être associée à un utilisateur sur une longue période, même s’il change de machine, utilisez un mécanisme de login fiable qui permette à l’utilisateur de se reconnecter et de lui réassocier ses données. Les sessions de servlet peuvent aider dans chaque cas, mais leur rôle devrait se limiter à celui de cache local, comme nous le verrons dans la prochaine section.

Architecture d’un panier

Regardons comment architecturer une session de suivi de transaction pour une application e-commerce (comme Amazon.fr). Voici quelques spécifications pour le panier :

  • Les utilisateurs connectés ont un environnement personnalisé.
  • La connection est maintenue entre deux femertures de navigateur.
  • Les utilisateurs peuvent se déconnecter et ne pas perdre le contenu de leur panier.
  • Les éléments ajoutés au panier persistent pendant une durée d’un mois.
  • Des invités peuvent ajouter des éléments au panier (mais le contenu n’est pas forcément accessible aux invités pour le long terme ou entre deux fermetures de navigateur).
  • L’achat nécessite un mot de passe pour raison de sécurité.

Les sessions de servlets seules ne peuvent satisfaire complètement à ces spécifications. Avec le bon serveur, vous pourriez faire durer vos sessions pendant un mois, mais vous perdriez vos informations lorsque l’utilisateur change de machine. L’utilisation de sessions pour stocker vos informations, entrainera des complications lorsque vous voudrez supprimer certains éléments (mais pas toute la session) après un mois, tout en vous assurant en même temps de ne pas stocker dans la session quelque chose qui ne devrait pas être conservé de manière définitive et vous avez besoin de trouver un moyen de déconnecter un utilisateur sans invalider le contenu de son panier. Il vous est impossible de faire cela avec l’API 2.3 !

Voici une architecture envisageable pour cette application qui utilise les avantages des sessions en tant que cache local : si l’utilisateur ne s’est pas loggé, il est considéré comme un invité et la session stocke le contenu de son panier. Les éléments y sont conservés tout au long de la session, d’une durée que vous avez estimée suffisante pour un invité. Cependant, si l’utilisateur s’est loggé, le contenu du panier est sauvegardé de manière plus sûre et est passé à la base de données pour un stockage semi-permanent. La base de données sera régulièrement nettoyée pour supprimer les éléments ajoutés plus d’un mois auparavant. Pour une question de performances, la session utilisateur devrait être utilisée pour stocker le contenu du panier même si l’utilisateur est loggé, mais la session devrait être utilisée comme un cache local de la base de données—permettant à des requêtes ultérieures d’afficher le contenu du panier sans avoir à accéder à la base de données à chaque requête.

Les logins utilisateurs peuvent être suivis en positionnant manuellement un cookie avec une période d’expiration longue. Après la page de login, le cookie stocke une forme “hashée” du numéro d’identification de l’utilisateur ; le numéro “hashé” correspond à un enregistrement de la base de données. Lors d’une visite ultérieure, l’utilisateur peut-être reconnu automatiquement et le contenu de son panier chargé dans la session. Par mesure de sécurité, au moment du paiement, la logique serveur vérifie le mot de passe avant de poursuivre. Bien que le serveur connaisse l’identité du client, comme le login est automatique, le paiement doit être protégé. Le flag indiquant que le mot de passe est vérifié doit bien sûr être stocké dans la session, avec une période d’expiration de 30 minutes qui semble assez appropriée ! Un déconnexion sur demande de l’utilisateur nécessite seulement de supprimer le cookie. L’architecture complète est montrée en Figure 3-3.

Figure 3-3. Architecture du panier
Figure 3-3. Architecture du panier

Cet exemple vous propose une gestion personnalisée de la gestion du login. La page de login par défaut pourrait être utilisée—cependant, elle est définie pour n’autoriser qu’une seule session login pour limiter les accès au contenu sécurisé. Elle n’est pas définie pour autoriser de multiples sessions login pour identifier les utilisateurs de l’application.

Quand utiliser les sessions ?

Comme nous l’avons vu dans l’exemple de l’application précédente, les sessions sont utiles, mais ne sont pas la panacée. Les sessions sont bien adaptées aux cas suivant :

Stocker l’état du login
Une période d’expiration de session est utile, et les changements de navigateurs ou de machines nécessitent un nouveau login.

Stocker les informations utilisateur extraite de la base de données
Le cache local évite les requêtes sur la base de données.

Stocker des données utilisateur temporaires
On entend par données temporaires les résultats de recherche, le contenu des formes HTML ou le contenu du panier d’un invité qui n’a pas besoin d’être préservé sur une longue période.

N’utilisez pas SingleThreadModel

Maintenant, au tour d’une caractéristique des servlets qui n’est jamais correctement utilisée : SingleThreadModel. Voici mon conseil : ne l’utilisez pas. En aucun cas.

Cette interface devait rendre la vie des programmeurs plus simple en ce qui concerne la gestion des threads, mais le fait est que SingleThreadModel n’est d’aucune aide. C’est une erreur reconnue au niveau de l’API Servlet, et elle est à peu près aussi utile qu’un pétard mouillé un jour de Quatorze Juillet.

Voici comment l’interface fonctionne : toute servlet implémentant SingleThreadModel hérite d’un cycle de vie spécial au sein du serveur. Au lieu d’allouer une instance de servlet pour traiter les requêtes multiples (avec de multiples threads opérant sur la servlet simultanément), le serveur alloue un pool d’instances (avec au plus un thread opérant sur les servlets simultanément). Vue de loin, cela semble bien, mais il n’y a en fait aucun avantage.

Considérez une servlet qui a besoin d’accéder à une ressource unique, comme une connexion à une base de données transactionnelle. Cette servlet a besoin de se synchroniser avec la ressource quelque soit le modèle de son propre thread. Il n’y a pas de différence si deux threads sont sur la même instance de servlet ou sur différentes instances de servlet ; le problème est que deux threads essaient d’obtenir la connexion, et que cela ne peut être solutionné qu’avec une synchronisation soignée.

Considérez maintenant que plusieurs copies de la ressource sont disponibles, mais que l’accès à n’importe laquelle doit être synchronisé. Le scénario est le même. La meilleure approche est de ne pas utiliser SingleThreadModel, mais de gérer un pool de ressources partagé par toutes les servlets. Par exemple, avec les connexions aux base des données, il est classique d’avoir un pool de connexions. Vous pourriez utiliser SingleThreadModel et faire en sorte que chaque instance de servlet ait sa propre copie de la ressource, mais c’est une mauvaise façon de gérer les ressources. Un serveur ayant des centaines de serlvets pourrait nécessiter des milliers d’instances de ressource.

En tant qu’auteur de livre, j’ai du garder une esprit ouvert et trouver un usage convaincant pour SingleThreadModel. (Après tout, j’ai besoin d’écrire des exemples montrant au mieux comment utiliser cette caractéristique). L’utilisation la plus vraisemblable m’a été donnée par un chef de projet dont les programmeurs avaient l’habitude d’utiliser des variables globales comme en langage C. En implémentant SingleThreadModel ils pouvaient faire transiter des données entre méthodes de servlets en utilisant des variables d’instances plutôt que par paramètres. Bien que SingleThreadModel le permette, ce n’est pas une bonne façon de faire et pas recommendable à moins que vous ne travailliez avec des programmeurs débutant en Java. Si c’est le meilleur cas d’utilisation, vous savez maintenant qu’il n’y a pas de bon cas d’utilisation. En fin de compte : n’utilisez pas SingleThreadModel.

Dans l’article suivant, vous apprendrez à utiliser les servlets comme cache local.

Textes originaux en anglais sur O’Reilly : Servlet Best Practices par The O’Reilly Java Authors

opoppon Le langage JSP et les Servlets Java , ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire