Introduction aux servlets (1)
Dans le monde Java, les servlets constituent la pierre de touche des technologies composants web. En règle générale, les servlets sont utilisées pour traiter la logique applicative d'une application Web, alors que la logique de présentation est quant à elle plutôt dévolue aux JSP.
Les servlets sont des programmes s'exécutant sur le serveur Web, et retournant des pages Web dynamiques (à la volée), au même titre que les CGI et les langages de script côté serveur tels que PHP ou ASP.
Les servlets se chargent et s'exécutent dans un moteur de servlet (appelé également conteneur de servlet), à l'intérieur d'une machine virtuelle Java (JVM). Concrètement, c'est ce conteneur qui implémente toutes les interfaces et classes que l'on appelle traditionnellement l'API Servlet ou littéralement Servlet Application Programmer Interfaces. Ce mécanisme rend possible deux choses :
- les servlets sont portables, c'est-à-dire dans ce cas-ci indépendantes des plates-formes et des serveurs, conformément à la philosophie java. Avec les servlets, vous pouvez réellement "écrire une fois, servir partout".
- l'API Servlet représente tout ce que le programmeur Java a besoin de connaître pour développer des servlets, sans se soucier des détails de l'implémentation.
Les servlets Java, telles que définies par la division Java Software de Sun MicroSystems, constituent un paquetage optionnel à Java. Cela signifie qu'elles sont officiellement reconnues par Sun et qu'elles font bien partie du langage Java, mais qu'elles ne font pourtant pas partie de l'API noyau de Java : à la place, elles font partie intégrante de la plate-forme J2EE.
L'API Servlet est divisée en deux packages principaux : javax.servlet et javax.servlet.http.[1]
Ce package contient les interfaces et classes relatives aux servlets indépendantes de tout protocole.
Cette interface représente l'interface centrale de l'API Servlet. Toute servlet digne de ce nom est tenue de l'implémenter, directement ou indirectement. Elle possède cinq méthodes :
init() :
- Cette méthode est appelée par le conteneur de servlet afin d'indiquer à la servlet qu'elle doit s'initialiser et être prête pour le service. Le conteneur passe un objet de type ServletConfig comme paramètre.
service() :
- Cette méthode est appelée par le conteneur de servlet à chaque requête du client afin de permettre à la servlet de répondre à la requête.
La méthode service()traite les requêtes et crée les réponses. Le conteneur de servlets invoque cette méthode lorsqu'il reçoit une requête de la servlet concernée. La signature complète de la méthode est la suivante :
public void service (ServletRequest, ServletResponse)
throws ServletException, java.io.IOException;
destroy() :
Cette méthode est appelée par le conteneur de servlet après que la servlet a été retirée et que toutes les requêtes destinées à la servlet ont été traitées.
getServletConfig() :
Retourne un type d'information disponible concernant la servlet, comme celle d'un paramètre passé à la méthode init().
getServletInfo() :
Retourne l'information disponible concernant la servlet, comme l'auteur, la version, et l'information copyright.
La classe GenericServlet implémente l'interface Servlet. Il s'agit d'une classe abstraite puisqu'elle fournit une implémentation pour toutes les méthodes excepté pour la méthode service(). Elle ajoute également quelques méthodes pour le logging. On peut étendre cette classe et implémenter la méthode service() pour écrire n'importe quel genre de servlet.
L'interface javax.servlet.ServletRequest fournit une vue générique de la requête envoyée par le client. Elle définit des méthodes qui extraient l'information de la requête.
L'interface javax.servlet.ServletResponse fournit une moyen générique d'envoyer une réponse. Elle définit des méthodes qui permettent d'envoyer une réponse convenable au client.
Ce package fournit les fonctionnalités de bases requises par les servlets HTTP. Les différentes interfaces et classes de ce package héritent des interfaces et classes du package javax.servlet correspondantes, afin de constituer un support spécifique pour le protocole HTTP.
javax.servlet.http.HttpServlet est une classe abstraite qui hérite de GenericServlet. Parmi les méthodes les plus importantes qu'elle ajoute, signalons-en trois principales :
service() : avec la signature suivante [2] :
protected void service (HttpServletRequest, HttpServletResponse)
throws ServletException, java.io.Exception;
doGet() : avec la signature suivante :
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
L'implémentation par défaut de service() dans HttpServletdistribue les requêtes HTTP GET à cette méthode, et les servlets l'implémentent donc pour traiter les requêtes GET. L'implémentation par défaut retourne une erreur HTTP SC_BAD_REQUEST.
doPost() : avec la signature suivante :
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
L'implémentation par défaut de service() dans HttpServlet distribue les requêtes HTTP POST à cette méthode, et les servlets l'implémentent donc pour traiter les requêtes POST. L'implémentation par défaut retourne une erreur HTTP SC_BAD_REQUEST.
Voici le code du désormais classique HelloWorld. Listing HelloWorld.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorldServlet extends HttpServlet {
public void service(HttpServeltRequest requete, HttpServletResponse reponse) throws ServletException, IOException {
PrintWriter pw = reponse.getWriter();
pw.println("<html>");
pw.println("<head>");
pw.println("</head>");
pw.println("<body>");
pw.println("<h3>Hello World</h3>");
pw.println("<body>");
pw.println("</html>");
}
}
Le code est composé des étapes suivantes :
1°) On hérite de la classe abstraite HttpServlet en redéfinissant la méthode protégée service().
2°) La méthode getWriter() de ServletResponse retourne un objet PrintWriter qui peut être utilisée pour envoyer des données au client. Cet objet est très utilisé par les servlets pour générer des pages Web dynamiquement.
3°) On remarque que contrairement à un programme classique, mais comme une applet, une servlet ne dispose pas de méthode main(). A la place, chaque fois que le serveur achemine une requête à une servlet qui lui est destinée, il invoque sa méthode service().
Regardons à présent comment écrire une servlet qui compte et affiche le nombre de fois que l'on y a accédé. Listing CompteurServlet.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class CompteurServlet extends HttpServlet
{
int nb_visites = 0;
public void doGet(HttpServeltRequest requete, HttpServletResponse reponse) throws ServletException, IOException
{
PrintWriter pw = reponse.getWriter();
nb_visites++;
pw.println("<html>");
pw.println("<head>");
pw.println("</head>");
pw.println("<body>");
pw.println("<h3>Vous avez accédé à cette serlvet " + nb_visites + " fois.</h3>");
pw.println("<body>");
pw.println("</html>");
}
}
Le code est très simple : il permet simplement d'incrémenter la variable d'instance nb_visites à chaque fois qu'une requête est faite sur la servlet. Cela est rendu possible parce que lorsque le conteneur de servlet charge cette servlet, il en crée une seule instance pour traiter chaque requête faite sur la servlet. Ainsi, les mêmes variables d'instance existent entre les invocations successives. On appelle cela la persistance d'instance.
Ici, on a redéfini la méthode doGet() et non la méthode protégée service(), ce qui constitue une meilleure approche. En effet, comme il a été dit plus haut, l'implémentation par défaut de service() appelle la méthode doGet() ou doPost() suivant le type de la requête HTTP. En redéfinissant service() comme nous l'avons fait dans l'exemple HelloWorld, on perd ce comportement par défaut bien utile [3].
Introduction aux servlets (2) - ServletRequest et ServletResponse
Après avoir expliqué les bases des servlets nous allons nous focaliser ici plus particulièrement sur les objets ServletRequest et ServletResponse.
Comme nous l'avons déjà dit, l'API Servlet s'applique à n'importe quel protocole, mais, en pratique, force est de constater que la majorité des servlets sont écrites pour le protocole HTTP.
Comme vous le savez sûrement, le protocole HTTP consiste en échanges de requêtes/réponses, les requêtes étant effectuées du client vers le serveur, et les réponses étant envoyées en retour par le serveur au client. Un navigateur Web envoie une requête HTTP à un serveur Web lorqu'une des situations suivantes se produit :
- Un utilisateur clique sur un hyperlien au sein d'une page HTML.
- Un utilisateur remplit un formulaire dans une page HTML et le valide.
- Un utilisateur entre une URL dans la barre d'adresse du navigateur et presse le bouton Entrer. Bien sûr, on peut appeler des pages Web par beaucoup d'autres moyens, notamment via des scripts, mais ces situations reviennent aux trois principales énumérées ci-dessus.
Par défaut, le navigateur utilise la méthode HTTP GET dans l'ensemble de ces cas. Cependant, on peut configurer le comportement du navigateur pour qu'il utilise d'autres méthodes HTTP, par exemple via l'attribut method dans le cas d'un formulaire :
<FORM name='loginForm' method='POST' action='/loginServlet'>
<INPUT type='text' name='utilisateur'>
<INPUT type='password' name='mot_de_passe'>
<INPUT type='submit' name='loginBouton' value='Login'>
</FORM>
Le tableau suivant résume les principales différences entre les méthodes GET et POST.
Les données ne peuvent être mémorisées dans l'historique du navigateur. |
De ces caractéristiques, il découle que l'on utilisera GET plutôt :
- pour retrouver un fichier HTML, XML, un fichier image, etc. parce que seul le nom du fichier nécessite d'être envoyé.
Et que l'on utilisera plutôt POST dans les cas suivants :
- pour envoyer une grosse quantité de données.
- pour uploader un fichier, parce que la taille du fichier pourrait excéder 255 caractères ou bien qu'il pourrait s'agir d'un fichier binaire.
- pour saisir un nom d'utilisateur/mot de passe, pour des raisons évidentes de sécurité.
En résumé, on pourrait dire que la méthode GET est utilisée pour lire des informations [1] (document, graphique, ou le résultat d'une requête sur une base de données) tandis que la méthode POST est quant à elle conçue pour poster des informations.
Il existe d'autres types de méthodes dans le protocole HTTP, comme par exemple la méthode HEAD, utilisée principalement pour récupérer des méta-informations sur la ressource. Typiquement, on fait appel à cette méthode pour contrôler la date de la dernière modification d'une ressource sur le serveur, et s'épargner ainsi la tâche d'un téléchargement inutile.
En fait pour chaque méthode du protocole HTTP, il existe une méthode correspondante dans la classe HttpServlet de type :
public void doXXX(HttpServletRequest, HttpServletResponse) throws ServletException, IOException;
où doXXX() dépend de la méthode HTTP, comme le montre le tableau suivant [2] :
GET |
doGet() |
HEAD |
doHead() |
POST |
doPost() |
PUT |
doPut() |
DELETE |
doDelete() |
OPTIONS |
doOptions() |
TRACE |
doTrace() |
La classe HttpServlet fournit des implémentations vides pour chacune de ces méthodes. On doit donc les redéfinir pour les adapter à notre propre logique applicative [3].
Vous vous demandez peut-être qui appelle ces différentes méthodes doXXX(). En voilà ci-dessous le processus détaillé :
1 - Le conteneur de servlet commence par appeler la méthode service(ServletRequest, ServletResponse) de HttpServlet.
2 - La méthode service(ServletRequest, ServletResponse) de HttpServletappelle la méthode service(HttpServletRequest, HttpServletResponse) de la même classe. Comme il a été déjà remarqué, on observe en passant que la méthode service est surchargée (overloaded) dans la classe HttpServlet[4] (modificateur).
3 - La méthode service(HttpServletRequest, HttpServletResponse) de HttpServlet analyse la requête et détermine quelle méthode HTTP est utilisée; elle appelle alors la méthode doXXX() correspondante de la servlet. Par exemple, si la requête utilise la méthode POST, la méthode service protégée appelle la méthode doPost() de la servlet [5].
La classe ServletRequest et sa sous-classe HttpServletRequest [6] permettent toutes les deux d'analyser une requête. La première fournit des méthodes relatives à n'importe quel protocole, alors que la seconde qui en hérite ajoute des méthodes spécifiques à HTTP. On utilise presque tout le temps HttpServletRequest, mais il est tout de même utile de connaître quelles méthodes sont spécifiquement implémentées par HttpServletRequest, et lesquelles sont héritées de HttpServlet.
Le premier objectif de ServletRequest est de récupérer les paramètres envoyés par un client. Le tableau ci-dessous décrit les méthodes utilisées à cette fin :
String getParameter(String nomParam) |
|
String[] getParameterValues(String nomParam) |
|
Enumeration getParameterNames() |
La classe qui implémente l'interface HttpServletRequest implémente toutes les méthodes de ServletRequest, appliquées au protocole HTTP : cette classe permet donc d'analyser et d'interpréter des messages http en particulier de récupérer les paramètres nommés (posté par un formulaire, par exemple) et de fournir les informations appropriées à la servlet.
Voici un exemple illustrant la façon dont on peut utiliser ces différentes classes et méthodes. Un formulaire HTML permet à un utilisateur d'envoyer deux paramètres au serveur, son nom ainsi que les langages informatiques qu'il connaît :
Le code HTML correspondant au formulaire de cette page est le suivant :
<form action="/introduction02/servlet/ServletForm" method="POST">
<br>
<table>
<tr>
<td>Nom <input type="text" name="nom" value=""></td>
</tr>
<tr>
<td>Langages </td>
<td><select name="langages" size="5" multiple>
<option value="Java">Java</option>
<option value="C">C</option>
<option value="C++">C++</option>
<option value="VB">VB</option>
<option value="XML">XML</option>
</select>
</td>
</tr>
</table>
<br><br>
<input type="submit" value="Envoyer">
</form>
L'attribut action de FORM indique que la servlet ServletForm doit traiter la requête. D'autre part, puisque l'attribut method vaut POST, les paramètres renseignés vont être transmis au serveur par une requête HTTP POST.
On remarque également le chemin relatif à la racine pour indiquer l'adresse de la servlet - commençant par / -, et qui est ajouté au chemin http://localhost:8080 ou équivalent [8].
Le listing ci-dessous montre une implémentation possible de la méthode doPost() de ServletForm, récupérant les paramètres envoyés :
public void doPost(HttpServletRequest req,
HttpServletResponse rep)
{
PrintWriter pw = rep.getWriter();
String nom = req.getParameter("nom");
String[] langages = req.getParameterValues("langages");
if (nom!=null && langages!=null){
pw.println("Nom du candidat:" + nom);
pw.println("Langages connus:");
pw.println("<ul>");
for (int i=0; i<langages.length; i++)
{
pw.println("<li>" + langages[i] + "</li>");
}
pw.println("</ul>");
}
}
Dans l'exemple ci-dessus, on connaît les noms des paramètres, donc on peut utiliser les méthodes getParameter() et getParameterValues(). Dans le cas contraire, on aurait dû utiliser getParameterNames() pour retrouver ces noms, par exemple comme ceci :
Enumeration parametres = req.getParameterNames();
while (parametres.hasMoreElements())
{
String parametre = (String)parametres.nextElement();
String[] valeurs = req.getParameterValues(parametre);
pw.println("<b>" + parametre + "</b>: ") ;
pw.println("<ul>");
for (int i=0; i<valeurs.length; i++)
{
pw.println("<li>" + valeurs[i] + "</li>");
}
pw.println("</ul>");
}
On vérifie le fonctionnement attendu de la servlet après avoir renseigné les champs de saisie et cliqué sur le bouton Envoyer :
De même qu'il existe des méthodes afin de récupérer les paramètres des requêtes, il existe des méthodes pour récupérer les noms et valeurs des en-têtes HTTP. A la différence cependant que les en-têtes sont spécifiques au protocole HTTP, et donc que les méthodes qui les traitent appartiennent à l'interface HttpServletRequest et non à HttpServlet. Parmi ces méthodes, on peut mentionner les suivantes :
String getHeader(String nomHeader) |
|
Enumeration getHeaders(String nomHeader) |
Cette méthode retourne toutes les valeurs associées à l'en-tête nommé sous la forme d'un Enumeration d'objets String ou un Enumeration vide si l'en-tête n'a pas été spécifié. |
Enumeration getHeaderNames() |
Cette méthode, retourne le nom de tous les en-têtes sous forme d'un Enumeration d'objets String ou un Enumeration vide si l'en-tête n'a pas été spécifié. |
Voilà par exemple comment afficher tous les en-têtes présents dans la requête :
Enumeration noms = req.getHeaderNames();
while (noms.hasMoreElements())
{
String nom_tete = (String) noms.nextElement();
String valeur = req.getHeader(nom_tete);
pw.println("<b>" +nom_tete + "</b>: " + valeur+ "<br>");
}
En ajoutant cette partie de code à notre servlet, on obtient le résultat suivant :
Sans surprise, l'objet ServletResponse fournit des méthodes valables pour tout type de protocole, alors que HttpServletResponse, qui en hérite, propose des méthodes supplémentaires spécifiques à HTTP.
ServletResponse déclare plusieurs méthodes génériques afin d'envoyer une réponse, parmi lesquelles setContentType(), getWriteret getOutputStream().
Cette méthode définit le type du contenu de la réponse au type spécifié. Dans les servlets HTTP, elle définit l'en-tête HTTP Content-Type. Des exemples de valeur sont par exemple text/plain pour du plein-texte, image/jpeg pour une image de type JPEG ou plus fréquemment text/html ; il est également possible d'y définir l'encodage utilisé. Cette méthode doit être appelée avant d'invoquer un PrintWriter
Cette méthode retourne un objet de la classe java.io.PrintWriter pour écrire des données caractères. Cet objet code les caractères conformément au charset donné précédemment par le type de contenu via setContentType(). Nous avons vu précédemment comment utiliser cette méthode sur l'interface HttpServletResponse.
Cette méthode retourne un objet javax.servlet.ServletOutputStream nécessaire pour écrire des données binaires (octet par octet). Aucun encodage n'est effectué.
Attention, on ne peut invoquer à la fois getWriter() et getOutputStream() sur une même instance de ServletResponse sous peine de déclencher une IllegalStateException. On peut appeler la même méthode plusieurs fois cependant.
Par exemple, si l'on souhaite envoyer un fichier binaire, de type JAR, au client, nous devons invoquer la méthode getOutputStream() :
public class ZipServlet extends HttpServlet
{
public void doGet(HttpServletRequest req,
HttpServletResponse rep) throws IOException
{
rep.setContentType("application/jar");
File fichier = new File("test.jar");
byte[] bytearray = new byte[(int)fichier.length()];
FileInputStream is = new FileInputStream(fichier);
is.read(bytearray);
OutputStream os = rep.getOutputStream();
os.write(bytearray);
os.flush();
}
}
Si vous testez cette servlet, assurez-vous auparavant d'avoir mis le fichier test.jar dans le dossier bin de Tomcat.
HttpServletResponse declare des méthodes spécifiques à HTTP, comme setHeader(), setStatus(), sendRedirect() et sendError().
public abstract void setHeader(String Nom, String valeur)
La méthode setHeader() fixe la valeur de l'en-tête indiqué sous forme de String. Si l'en-tête a déjà été fixé, la nouvelle valeur écrase la précédente. Cette méthode a été introduite dans l'API Servlet 2.2 et est à mettre en parallèle avec les deux méthodes public abstract void setIntHeader(String name, int valeur) et public abstract void setDateHeader(String name, long date) de la même interface. Cette dernière méthode peut être utile pour indiquer une valeur sous forme de date au header LastModified, indiquant la date de dernière modification du document.
public abstract void sendRedirect(String Location) throws IOException, IllegalStateException
Après avoir analysé une requête, une servlet peut rediriger la réponse vers une adresse spécifiée, en définissant automatiquement l'en-tête Location.
Par exemple, le bout de code ci-dessous contrôle la valeur du paramètre et redirige le browser vers une nouvelle adresse si la valeur n'est pas celle attendue :
if ("Nietzsche".equals(request.getParameter("philosophe")))
{
//traitement particulier
}
else {
reponse.sendRedirect("http://www.philosophie.fr");
}
Deux choses sont à noter ici :
- la nouvelle URL doit être écrite sous forme absolue, en incluant le protocole http://. Cette méthode doit être appelée avant que la réponse ne soit validée, sinon elle lance une java.lang.IllegalStateException. Par exemple, le code suivant va générer un tel type d'erreur :
public void doPost(HttpServletRequest req, HttpServletResposne rep) throws IOException, ServletException
{
PrintWriter pw = rep.getWriter();
pw.println("<html><body>HelloWorld!</body></html>");
pw.flush();
pw.sendRedirect("http://www.philosophie.fr");
}
En effet, l'instruction pw.flush() force l'envoi du header au navigateur client : a ce point, la réponse est donc considérée comme validée et il n'y a plus aucun intérêt à ajouter quoi que ce soit au message.
Enfin, il peut arriver que quelque chose tourne mal, par exemple lorsqu'on demande à la servlet de retourner un fichier qui n'existe pas, ou qu'on lui demande des choses qu'elles ne sait pas faire.
L'interface fournit les méthodes sendError(int status_code) et sendError(int status_code, String message) pour gérer de telles situations et envoyer le code d'état approprié au client.
Par exemple, si la servlet ne trouve pas un fichier, on peut renseigner le client par l'instruction suivante :
reponse.sendError(HttpServletResponse.SC_NOT_FOUND);
Dans le cas d'une chose impossible à faire :
reponse.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
NOTES:
[1] les seules informations que la méthode GET est susceptible de fournir sont une séquence de caractères ajoutés à l'URL de la requête dans ce que l'on appelle une chaîne d'interrogation
[2] Les requêtes HTTP 1.0 ne peuvent être que d'un des trois types suivants : GET, POST ou HEAD. Les autres méthodes sont disponibles uniquement via HTTP 1.1.
[3] Plus exactement, il est nécessaire d'en redéfinir au moins une
[4] C'est du reste le fait qu'elle soit surchargée overloaded et non redéfinie overrided qui permet également à cette seconde méthode de même nom service d'avoir un modificateur d'accès différent : protected au lieu de public
[5] Si vous redéfinissez (override) la méthode service dans votre servlet, vous perdez le bénéfice de cette redirection automatique et il vous reviendra alors la tâche d'abord de déterminer la nature de la méthode HTTP, ensuite d'effectuer l'appel à la méthode doXXX() correcte.
[6] Il est nécessaire de clarifier les choses ici. Strictement parlant, javax.servlet.ServletRequest et javax.servlet.http.HttpServletRequest sont définies en tant qu'interfaces dans l'API Servlet. Ici, on parlera indistinctement d'interface ou de classe, sachant :
- premièrement qu'en toute rigueur, on devrait parler « d'un objet d'une classe qui implémente l'interface javax.servlet.ServletRequest ou l'interface javax.servlet.http.HttpServletRequest ».
- deuxièmement que c'est le conteneur de servlet qui fournit de telles classes, et que le nom de ces classes d'implémentation n'a aucune importance pour le développeur qui doit s'en tenir aux noms d'interfaces fournis par l'API Servlet.
[7] Si on appelle getParameter() sur un paramètre ayant plusieurs valeurs, la valeur retournée est la première valeur retournée par getParameterValues()
[8] Une autre façon de faire est d'appeler l'URL relative à la page HTML courante via <form action="../servlet/ServletForm2" method="POST">
Introduction aux servlets (3)
Cycle de vie – ServletConfig – ServletContext
Après la lecture des deux premières parties, il devrait être assez clair qu'une servlet reçoit une requête, la traite, et renvoie une réponse à l'aide d'une des méthodes doXXX().
Cependant, avant qu'une servlet puisse correctement répondre à la requête d'un client, le conteneur de servlet doit suivre certaines étapes afin que cette servlet se trouve dans un état propre à pouvoir répondre aux requêtes.
Regardons d'un peu plus l'ensemble de ces étapes, appelées cycle de vie de la servlet, et qui se décomposent ainsi :
- créer et initialiser la servlet.
- traiter les services demandés par les clients.
- détruire la servlet et la passer au ramasse-miettes.
Nous avons dit précédemment qu'une servlet persistait entre les requêtes sous forme d'instance d'objet. Autrement dit, lorsque le code de la servlet est chargé, le conteneur de servlet ne crée qu'une seule instance de classe. Ainsi, les coûts liés à la création d'un nouvel objet sont considérablement diminués, et la servlet dispose en permanence d'un certain nombre d'informations dont elle risque fort d'avoir besoin lors des prochaines requêtes [1]. Un exemple courant est une connexion à une base de données qui est ouverte une seule fois, et est ensuite utilisée à chaque nouvelle requête sur la base de données. Le processus d'instanciation est le suivant :
Lorsque l'on démarre le conteneur de servlet, il cherche un ensemble de fichiers de configuration appelés descripteurs de déploiement, décrivant toutes les applications Web.
Chaque application Web possède son propre fichier web.xml, incluant une entrée pour chaque servlet qu'il utilise. Une entrée spécifie le nom de la servlet et le nom de la classe de servlet [2]. Le conteneur de servlet crée une instance de la classe de la servlet concernée en utilisant la méthode Class.forName(nomClasse).newInstance(). Pour utiliser une telle méthode, la classe de servlet doit posséder un constructeur sans argument : typiquement, on ne définit pas de tel constructeur dans la servlet, et on laisse le compilateur ajouter le constructeur par défaut. A ce moment-là, la servlet est chargée.
Il est parfaitement envisageable d'initialiser une servlet avec un certain nombre de données lorsqu'elle est chargée. Comment rendre cela possible alors que l'on ne définit aucun constructeur ?
Réponse : lorsque le conteneur de servlet crée l'instance de servlet, il appelle la méthode init(ServletConfig) sur cette instance nouvellement créée. L'objet ServletConfig contient tous les paramètres d'initialisation qui ont été définis dans le descripteur de déploiement pour l'application web à laquelle appartient la servlet.
La servlet est initialisée une fois que la méthode init() a retourné son résultat. De plus, comme il ne sert strictement à rien d'initialiser un objet de façon répétitive, on est assuré que le conteneur de servlet n'appellera cette méthode init() qu'une seule fois sur l'instance de servlet.
Si vous regardez attentivement l'API Servlet et cherchez la classe GenericServlet, vous allez vous apercevoir que celle-ci possède une version surchargée de init(ServletConfig), la méthode init() définie sans arguments. Cette dernière peut être redéfinie (overriden) sans aucun problème dans le code de votre servlet. Autrement, si vous choisissez de redéfinir la méthode init() prenant en argument un servletConfig, vous devrez immanquablement y inclure comme première instruction un appel à super.init(config).
Comme exemple simple d'initialisation de servlet, avec utilisation d'un paramètre d'initialisation défini dans web.xml, on peut reprendre notre exemple de servlet compteur de visites, et « tricher » en faisant démarrer le compteur à 30 visites :
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class CompteurInitieServlet extends HttpServlet
{
int nb_visites = 0;
public void init() throws ServletException {
String initial = getInitParameter("initial");
try {
nb_visites = Integer.parseInt(initial);
}
catch (NumberFormatException e){
nb_visites = 0;
}
}
public void doGet(HttpServletRequest requete, HttpServletResponse reponse) throws ServletException, IOException
{
PrintWriter pw = reponse.getWriter();
nb_visites++;
pw.println("<html>");
pw.println("<head>");
pw.println("</head>");
pw.println("<body>");
pw.println("<h3>Vous avez accédé à cette servlet " + nb_visites + " fois.</h3>");
pw.println("<body>");
pw.println("</html>");
}
}
On verra plus en détail la méthode getInitParameter(String) de l'interface ServletConfigdans la suite de cet article. Disons simplement ici que cette méthode :
- permet de récupérer un paramètre par son nom lorsqu'il est défini dans le fichier web.xml de l'application web (voir ci-dessous).
- la signature de cette méthode impose un retour de valeur sous la forme d'un String, d'où l'utilisation d'une méthode de conversion en int.
La définition du paramètre d'initialisation dans le fichier web.xml est très simple, et se fait de la façon suivante :
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>CompteurInitieServlet</servlet-name>
<servlet-class>CompteurInitieServlet</servlet-class>
<init-param>
<param-name>
initial
</param-name>
<param-value>
30
</param-value>
</init-param>
<description>
Valeur initiale du compteur
</description>
</servlet>
</web-app>
On définit le nom du paramètre au sein de la balise <param-name> tandis que sa valeur est spécifiée dans <param-value>, ces deux balises sœurs étant englobées par la balise parent <init-param>.
Si vous avez correctement paramétré votre application, vous devriez alors obtenir l'écran suivant, en pointant pour la première fois sur la servlet :
La méthode d'instance public void destroy() est appelée par le moteur de servlets afin d'indiquer à une servlet qu'elle a été mise hors service. Après l'appel de cette méthode, la méthode de service de la servlet ne sera plus appelée par le conteneur de servlets.
Une servlet peut surcharger cette méthode afin de sauvegarder son état, libérer ses ressources (connexions à une bases de données, etc.).
Par exemple, la méthode destroy() pourrait être utilisée de la façon suivante pour fermer une connexion à une base de données, l'objet de type Connection dbConnexion ayant été précédemment défini :
public void destroy(){
try{
dbConnexion.close();
}
catch(Exception e)
{
System.out.println("Erreur lors de la fermeture de la connexion" + e.getMessage());
}
}
On a vu dans la section précédente que le conteneur de servlets passait un objet ServletConfig dans la méthode init(ServletConfig). Dans cette section, nous allons regarder un peu plus en détail cet objet.
L'interface ServletConfig est définie dans le package javax.servlet, et est plutôt simple à utiliser. Elle fournit quatre méthodes, ainsi que le montre le tableau ci-dessous :
Méthode |
Description |
String getInitParameter(String name) |
|
Enumeration getInitParameterNames() |
Cette méthode retourne le nom de tous les paramètres d'initialisation sous forme d'un Enumeration d'objets String ou un Enumeration vide s'il n'existe pas de paramètres. |
ServletContext getServletContext() |
Cette méthode retourne l'objet ServletContext() de la servlet, permettant une interaction avec le conteneur de servlets. |
String getServletName() |
On peut remarquer que ServletConfig fournit des méthodes seulement pour récupérer des paramètres. On ne peut pas ajouter ou même simplement définir des paramètres de ServletConfig via les méthodes de cet objet.
On a déjà utilisé plus haut une des méthodes de ServletConfig, i.e. getInitParameter. On peut se demander du reste pourquoi on avait pu l'appeler directement dans le code. En fait, la méthode init()telle que définie dans la section 1 de cet article est exactement équivalente à la définition suivante :
public void init() throws ServletException {
ServletConfig config = getServletConfig();//1
String initial = config.getInitParameter("initial");//2
try {
nb_visites = Integer.parseInt(initial);
}
catch (NumberFormatException e){
nb_visites = 0;
}
A la ligne 1, nous utilisons la méthode getServletConfig() mentionnée dans Introduction aux servlets (1) afin de récupérer l'objet ServletConfig sauvegardé par la méthode init(). A la ligne 2, nous appliquons alors la méthode getInitParameter() à cet objet afin de récupérer le paramètre d'initialisation.
Question : Pourquoi la version n°1 est-elle légitime ? Comment pouvons-nous nous passer de l'appel à getServletConfig() de l'interface Servlet ?
Réponse : Tout simplement parce que la classe GenericServlet implémente l'interface javax.servlet.ServletConfig en plus d'implémenter l'interface javax.servlet.Servlet. Par conséquent, un appel direct à getInitParameter() est de ce fait rendu possible.
Cela est vrai de n'importe quelle méthode de ServletConfig et l'on peut donc ajouter sans rique d'erreur, avant d'avoir récupéré l'objet ServletConfig lui-même, la première ligne suivante, qui se charge d'écrire un message dans la console avec le nom de la servlet concernée :
public void init() throws ServletException {
System.out.println(getServletName() + " : Initialisation...");
ServletConfig config = getServletConfig();
String initial = config.getInitParameter("initial");
try {
nb_visites = Integer.parseInt(initial);
}
catch (NumberFormatException e){
nb_visites = 0;
}
Deux remarques pour terminer cette section :
- il n'est pas nécessaire d'inclure le code de récupération des paramètres d'initialisation dans la méthode init() même si c'est fréquemment le cas.
- Les paramètres d'initialisation sont disponibles aux servlets qui ne sont pas des servlets HTTP, puisque cette fonctionnalité est celle de l'objet javax.servlet.GenericServlet. Une servlet générique peut donc être utilisée dans un serveur Web même s'il lui manque les fonctionnalités spécifiques à HTTP.
L'exemple de code ci-dessous montre comment une sous-classe de GenericServlet peut récupérer ses paramètres d'initialisation :
import java.io.*;
import java.util.*;
import javax.servlet.*;
public class InitServlet extends GenericServlet
{
public void service(ServletRequest requete, ServletResponse reponse) throws ServletException, IOException
{
reponse.setContentType("text/plain");
PrintWriter out = reponse.getWriter();
out.println("Paramètres d'initialisation de la servlet " + getServletName());
Enumeration enum = getInitParameterNames();
while(enum.hasMoreElements()){
String name = (String)enum.nextElement();
out.println(name + " : " + getInitParameter(name));
}
}
}
Cette servlet récupère le nom de la servlet ainsi que le nom et la valeur de chacun des paramètres d'initialisation, alors qu'elle n'hérite que de la classe GenericServlet. Si vous avez spécifié correctement de tels paramètres dans le fichier web.xml de l'application web, vous devriez obtenir un résultat de la forme suivante :
L'interface ServletContext définit un ensemble de méthodes que l'on peut utiliser pour communiquer avec le conteneur de servlets. Pour la servlet, c'est une sorte de fenêtre lui permettant d'avoir une vue sur son environnement [3], tel que les paramètres de l'application web dont elle fait partie, ou encore la version de son conteneur de servlet. Cela inclut aussi trouver l'information de chemin, accéder aux autres servlets s'exécutant sur le serveur et écrire dans le journal de serveur.
Chaque application web dispose d'un contexte de servlet différent.
L'interface ServletContext dispose d'un grand nombre de méthodes. Aussi allons-nous nous limiter dans cette section d'introduction seulement à deux d'entre elles, getResource() et getResourceAsStream(), toutes les deux utilisées pour retrouver une ressource associée à un chemin.
Signature : java.net.URL getResource(String path).
Cette méthode retourne une URL pour la ressource associée au chemin indiqué en paramètre. Le chemin doit commencer par / et il est interprété relativement à la racine du contexte. La méthode peut renvoyer null si aucune ressource n'est associée au chemin. Elle permet à un conteneur de servlets de rendre disponible aux servlets n'importe quelle ressource.
Attention, le contenu de la requête est renvoyé tel quel. La requête sur une page .jsp renvoie le code source de la page ; nous verrons dans un article ultérieur une autre technique pour inclure les résultats de l'exécution d'une page JSP [4].
Pour illustrer l'utilisation de cette méthode, on peut reprendre l'exemple de notre servlet ZipServlet (envoi d’un fichier binaire de type jar au client) de Introduction aux servlets(2), et réécrire le code pour rendre la ressource portable d'un conteneur de servlets à un autre :
import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ZipServlet extends HttpServlet
{
public void doGet(HttpServletRequest req,
HttpServletResponse rep) throws IOException
{
rep.setContentType("application/zip");
ServletContext context = getServletContext();
URL url = context.getResource("/zip/introduction03.zip");
//ouvre une connexion sur cet URL
//et renvoie un InputStream
InputStream is = url.openStream();
OutputStream os = rep.getOutputStream();
int byteslus = 0;
while( (byteslus = is.read(bytearray)) !=-1)
{
os.write(byteslus);
}
os.flush();
is.close();
}
}
Signature : java.io.InputStream getResource(String path).
Cette méthode retourne un InputStream pour lire le contenu de la ressource associée au chemin indiqué. La méthode peut renvoyer null si aucune ressource ne peut être associée au chemin.
Elle représente une méthode raccourci de getResource(path).openStream().
Par exemple, dans le code ci-dessus, la parie concernant la récupération d'un InputStream à partir de l'objet URL pourrait aussi s'écrire, directement à partir de l'objet ServletContext :
ServletContext context = getServletContext();
InputStream is = context.getResourceAsStream ("/zip/introduction03.zip");
Attention, quoique plus pratique que celui de la première méthode, l'emploi de getResourceAsStream() perd un certain nombre de méta-informations comme la taille du contenu et son type, disponibles avec getResource()
NOTES:
[1] On a illustré cela dans Introduction aux servlets(1) avec l'exemple du compteur de visites et l'utilisation de variables d'instance.
[2] Introduction aux servlets (1) pour un exemple.
[3] Un peu comme la monade chez Leibniz constitue une fenêtre sur le monde...
[4] Il s'agit de l'interface javax.servlet.RequestDispatcher.
La servlet qui suit lance un appel sur une base de données et renvoie les données au format HTML.
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class DBServlet extends HttpServlet {
private Connection con;
private PrintWriter out;
public void init(ServletConfig conf) throws ServletException {
super.init(conf);
try {
Class.forName("omnidex.jdbc.OdxJDBCDriver");
con =DriverManager.getConnection ("jdbc:omnidex:c:/datas/test.dsn", "login", "mdpasse");
} catch(Exception e) {
System.err.println(e);
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
res.setContentType("text/html");
try {
out = res.getWriter();
out.println("<html><head><title>");
out.println("JDBC Servlet");
out.println("</title></head><body>");
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT nom FROM clients WHERE id > 20");
out.println("<UL>");
while(rs.next()) {
out.println("<LI>" + rs.getString("nom"));
}
out.println("</UL>");
rs.close();
stmt.close();
} catch(SQLException e) {
out.println("Exception SQL");
} catch(IOException e) {
system.err.println("Exception I/O");
}
out.println("</body></html>");
out.close();
}
public void destroy() {
try {
con.close();
} catch(SQLException e) {
;
}
}
} // classe
La servlet classe (abstraite) HttpServlet, qui nous permet de gérer tout ce qui concerne le protocole HTTP requis par une application Web.
Trois méthodes sont ensuite définies: init(), doGet() et destroy(). Elles sont décrites dans ce qui suit, hors les gestionnaires d'exceptions.
C'est ici que se déroule l'ensemble des connexions à la base de données, ainsi que tous les processus ne nécessitant qu'un seul lancement.
Ici, deux processus sont lancés: l'enregistrement du pilote JDBC, et la connexion à la base de données.
C'est la méthode Class.forName qui permet de choisir le pilote JDBC que l'on utilise, ici le pilote Omnidex.
La méthode getConnection() ouvre une connexion vers l'adresse (l'URL) JDBC de la base de données. Les URLs JDBC offrent une manière unique d'identifier les bases de données, sous la forme « jdbc:nomdupilote:nomdelasource ».
Dans notre servlet, nous utilisons le pilote Omnidex, et la base de données se trouve sous la forme d'un fichier datasource nommé test.dsn, situé dans le répertoire C:\datas (DOS noté les ‘/’ dans l’URL).
Après l'initialisation, le servlet est en mesure de gérer de nombreuses requêtes par le biais d'une seule occurrence. Chaque requête en provenance du client déclenche l’appel de cette méthode.
La connexion (lancée par init()) réussie, le servlet est prêt à envoyer et recevoir des requêtes.
Pour construire la page HTML, il faut en premier lieu lui donner un type (text/html) à l'aide de setContentType(). Le flux de sortie est créé avec out=res.getWriter(). Une fois créé, il ne reste plus qu'à construire le HTML.
On créé un objet Statement, utilisé pour lancer la requête SQL, qui renverra un objet ResultSet. Cet objet sera utilisé pour afficher les données renvoyées par la requête SQL.
Une fois la requête terminée, les objets contenant le résultat de la requête et le Statement sont fermés.
Cet exemple permet d'observer une connexion persistante à une base de données, de telle sorte qu'une nouvelle requête du client ne nécessite pas de reconnections.
Une fois le servlet utilisé par le serveur, la méthode destroy() permet de fermer complètement la connexion.
Le driver permettant l’accès au SGBDR devra être accessible à votre servlet. Pour cela placez le fichier .jar du driver dans un répertoire lib sous le répertoire d’accueil de votre application (au besoin créez ce répertoire)