IV. Fonctionnement interne▲
Vous devriez maintenant disposer des connaissances suffisantes pour pouvoir débuter le développement de servlets et les faire fonctionner grâce aux outils de travail que vous avez pu mettre en place précédemment. Je vais maintenant tenter de vous décrire de manière complète le fonctionnement interne du moteur de servlets qui permettra de faire fonctionner vos futures servlets de manière efficace et cohérente. Grâce aux connaissances que vous allez acquérir dans ce chapitre, j'espère que vous comprendrez réellement le fonctionnement des servlets. Cela devrait vous permettre d'être relativement indépendant par rapport à la suite de votre apprentissage, car vous pourrez comprendre n'importe quel document au sujet des servlets et vous pourrez aussi déduire des connaissances par vous même au prix d'un effort de réflexion logique. Il est important de noter que, même si les notions évoquées dans ce chapitre paraissent trop techniques et relever du détail, elles vous seront vraiment utiles dans votre développement de tous les jours. En effet, les mécanismes de fonctionnement de la plate-forme Java influeront directement sur le code que vous écrirez. Comme le dit si bien le proverbe, je vais tenter de vous apprendre à pêcher plutôt que de vous offrir le poisson à chaque fois que le besoin s'en fera sentir.
Les moteurs de Servlets ont besoin d'une machine virtuelle Java pour fonctionner. Cela signifie qu'en rentrant dans les détails de fonctionnement des Servlets, j'évoquerai à de nombreuses reprises des notions ayant trait au fonctionnement de toute machine virtuelle Java et à des notions encore plus universelles comme les threads, les processus, le chargement des classes au démarrage d'une application, le ramasse-miettes ou encore les références. Je considère tout ceci comme acquis et je ne donnerai pas d'explications sur ces notions à part si elles possèdent quelques spécificités liées à leur utilisation par les Servlets. Il est donc temps, si vous ne maîtrisez pas toutes ces notions, de vous documenter. Une bonne source pour se documenter sur de tels sujets est le bouquin « The Java Virtual Machine » disponible à l'URL http://java.sun.com/docs/books/vmspec/ qui vous apportera plus de connaissances qu'il n'en faut.
Il existe de nombreux fournisseurs de machines virtuelles Java différents. Chaque fournisseur se conforme à la norme définie par Sun et les bibliothèques de classes ainsi que les machines virtuelles fournies par ces organismes possèdent les mêmes « interfaces », mais pas forcément la même implémentation de ces interfaces. Un chargeur de classes peut très bien utiliser une méthode différente pour la JVM de Microsoft que pour la JVM de Sun. Ce n'est pas une chose rare. Les moteurs de Servlets eux aussi proviennent de nombreux éditeurs différents et sont donc programmés différemment par des développeurs différents. Ils se conforment aux aussi à une norme définie par Sun qui est représentée par les spécifications de l'API des Servlets qui ne décrit que les comportements que doivent suivre les moteurs de Servlets de façon générale. L'implémentation est libre et toujours différente entre deux moteurs de Servlets différents. Or nous avons vu que les moteurs de Servlets ne peuvent pas fonctionner sans une machine virtuelle Java, ce qui m'amène à cet avertissement : les comportements que je vais décrire dans le présent chapitre sont des comportements recommandés et rencontrés le plus souvent possible, mais peuvent être différents selon la solution adoptée. Je vous conseille donc de garder un œil critique sur ces descriptions, surtout celles qui évoquent les détails d'implémentation. Les comportements généraux sont quant à eux, en général, identiques pour chaque solution.
Je tenterai d'effectuer une comparaison avec les autres solutions une fois toutes les connaissances acquises à propos du fonctionnement interne.
IV-A. Le modèle de fonctionnement▲
Nous savons que pour faire fonctionner des Servlets, il est nécessaire de disposer d'une machine virtuelle Java, d'un moteur de Servlets implémentant l'API des Servlets et éventuellement d'un serveur Web. Ces différents composants prenant part au système d'exécution des Servlets coopèrent d'une façon bien déterminée. J'ai rapidement présenté ce fonctionnement lors de la description des différentes solutions (voir choix_solution) pour la mise en place d'une plate-forme exécutant des Servlets, mais il est nécessaire d'apporter des précisions.
La machine virtuelle Java peut être reliée de plusieurs façons au serveur Web qui gère les requêtes venant de l'utilisateur, ceci dépendant du type de solution choisi :
La machine virtuelle fait partie intégrante serveur, le serveur étant écrit en Java : c'est le cas du moteur de Servlets indépendant (comme Java Web Server ou Tomcat dans certains cas). Le moteur de Servlets est alors un thread de la JVM au même titre que les Servlets qu'il exécute, et que le serveur Web qui accepte les requêtes HTTP.
La machine virtuelle Java constitue un des Threads du serveur Web : c'est le cas du moteur de Servlets embarqué dans un serveur Web monoprocessus et multithreads. Dans ce cas précis, l'intégration des Servlets dans le serveur Web est importante et il est facile, par exemple, de récupérer des informations concernant la requête provenant du client via les méthodes fournies par l'API des Servlets.
La machine virtuelle Java est un processus externe au serveur Web, qui est lui multiprocessus. C'est le cas de Tomcat lorsqu'on l'utilise comme moteur de Servlets externe couplé au serveur Web Apache. Les processus lancés par le serveur Web et qui représente autant de clients effectuant des requêtes HTTP peuvent alors ce partager le processus correspondant à la JVM externe.
Suivant le choix de la solution, c'est aussi le coût en ressource processeur et mémoire qui va changer : plus le moteur de Servlets est externalisé, plus les changements de contexte sont lourds. En effet, les informations spécifiques à celle-ci pourront être partagées directement dans le cas des moteurs de Servlets indépendants (changement de contexte léger) ou devront être préalablement copiées en mémoire et nécessiter donc un traitement (changement de contexte lourd) dans le cas des moteurs de Servlets externes.
Cependant, quelle que soit la solution adoptée, chaque Servlet s'exécute dans un thread de la machine virtuelle Java à laquelle elle est associée. Cela implique que les Servlets peuvent communiquer entre elles facilement (changement de contexte léger), car elles fonctionnent dans le même espace d'adressage et les données sont donc accessibles en lecture et en écriture simplement. Le fait qu'elles constituent un thread exécuté dans la même machine virtuelle que le moteur de Servlets qui les exécute présente aussi quelques particularités gênantes. En effet, le moteur de Servlets et les Servlets partagent le même processus et une Servlet peut très bien entreprendre une action affectant le processus complet (et donc le moteur de Servlets). Un des exemples critiques est l'emploi de l'instruction System.exit() dans le code d'une de vos Servlets qui cause l'arrêt du moteur de Servlets jusqu'à ce que l'administrateur le redémarre. Bien entendu, il existe des moyens tout à fait classiques d'éviter ce type de problème (voir le chapitre traitant de la sécurité et plus particulièrement le Security Manager). Ce type de comportement illustre très bien un des aspects du fonctionnement des moteurs de Servlets.
La nécessité de disposer du traitement de plusieurs requêtes de manière simultanée provient de l'utilisation même des applications Web : plusieurs personnes peuvent demander le même service en même temps. Que se passe-t-il si plusieurs personnes demandent à accéder à la même Servlet en même temps ?
Il existe deux solutions possibles (comme il est précisé dans la normalisation des Servlets) :
- Une seule instance de chaque servlet est chargée en mémoire et chaque requête venant des clients Web sont autant de threads qui peuvent manipuler cette instance ;
- À chaque requête HTTP correspond une instance différente pour chacune des servlets. Plus il y a de requêtes demandant l'accès à une servlet, plus il y a d'instances de cette servlet chargées en mémoire.
La première solution constitue le comportement par défaut, ainsi aucune particularité dans la syntaxe de votre code ne doit être ajoutée. Lorsqu'un client accède à une Servlet, la méthode principale (doGet(), doPost() ou service() selon le choix de la méthode HTTP et de l'interface implémentée) est appelée et un nouveau thread est créé. C'est dans ce nouveau thread que le traitement correspondant au code de votre Servlet va être exécuté. Cependant, c'est toujours la même instance de la Servlet qui prend en charge la requête : les attributs de classes d'une même Servlet modifiés par un client sont donc modifiés pour tous. Cela implique l'utilisation des sémaphores via le mot clef synchronized afin de faire face aux éventuels problèmes de synchronisation. Il faut donc prendre garde à bien synchroniser la manipulation des attributs d'instance, mais aussi à ne pas tout encadrer dans un bloc synchronisé : le caractère simultané des requêtes serait alors perdu. Par exemple, vous ne devez pas marquer la méthod e principale (doGet(), doPost() ou service() en général) avec le mot clef synchronized, l'instruction accédant en lecture ou en écriture à l'attribut en question doit elle seule être encadrée. Voici un exemple de code d'une mauvaise synchronisation, puis d'une bonne synchronisation :
import
javax.servlet.*;
import
javax.servlet.http.*;
import
java.io.*;
public
class
MauvaisThread extends
HttpServlet {
private
int
compteur =
0
;
public
synchronized
void
doGet
(
HttpServletRequest req, HttpServletResponse res)
throws
ServletException, IOException {
PrintWriter out =
res.getWriter
(
);
compteur++
;
out.println
(
"Cette Servlet a été accédée "
+
compteur +
" fois."
);
}
}
import
javax.servlet.*;
import
javax.servlet.http.*;
import
java.io.*;
public
class
BonThread extends
HttpServlet {
private
int
compteur =
0
;
public
void
doGet
(
HttpServletRequest req, HttpServletResponse res)
throws
ServletException, IOException {
synchronized
(
this
) {
int
tmpCompteur =
++
compteur;
}
PrintWriter out =
res.getWriter
(
);
out.println
(
"Cette Servlet a été accédée "
+
tmpCompteur +
" fois."
);
}
}
La nécessité d'apporter une attention particulière à la synchronisation est une des principales charges de travail qu'impose l'attribution d'un thread séparé pour chaque requête vers la même Servlet. Par contre, l'amélioration au niveau de l'occupation mémoire et de la vitesse d'exécution est perceptible : un seul ensemble de classes est chargé en mémoire (les classes utilisées par les Servlets qui ont été demandées au moins une fois) et lors d'une nouvelle requête, tout l'environnement de la Servlet n'est pas dupliqué, c'est un thread qui est créé : cela prend moins de temps. C'est un modèle vraiment différent des applications utilisant la norme CGI, car, dans ce cas-là, à chaque requête correspond un processus, ce qui pose d'évidents problèmes d'occupation en mémoire et de vitesse d'exécution (surtout pour le lancement du script). Les autres modes de développement comme PHP (lorsqu'il est utilisé en tant que module d'un serveur Web) ou ISAPI par exemple sont assez similaires au niveau du modèle d'exécution (une seule instance du programme, chaque requête est un thread différent). Bien entendu, la synchronisation n'est pas nécessaire si vous manipulez des variables locales seulement, qui sont propres à chaque thread, et donc à chaque requête. Ce modèle d'exécution permet aussi la persistance des Servlets : une Servlet peut très bien conserver en mémoire des informations acquises au fur et à mesure des nombreuses requêtes, cela introduit de nombreuses facilités de programmation pour traiter des cas faisant appel à des renseignements provenant des requêtes antérieures, comme le compteur précédent.
Une autre conséquence qui montre bien le mode de fonctionnement d'une Servlet utilisant un mode d'exécution classique est la persistance des threads nouvellement créés à l'intérieur du code d'une Servlet. En effet, lorsqu'un thread est créé et lancé lors du traitement d'une requête, celui-ci ne se termine pas lorsque la requête est terminée (à moins qu'on précise le contraire), mais lorsque l'instance de la Servlet est détruite (c'est-à-dire rarement dans des conditions normales d'utilisation : lors de l'arrêt du moteur de Servlets). Cela permet d'effectuer des traitements de type batch très intéressant tout en manipulant les données de l'instance de la Servlet. On peut imaginer une Servlet lançant toutes les nuits la production d'un rapport en arrière-plan
L'autre modèle d'exécution est le modèle monothread. Ici, à chaque requête correspond une instance de Servlet. La totalité des instances créées pour une Servlet se nomme le pool de Servlets. Lors d'une nouvelle requête, le thread correspondant à cette requête puise dans ce pool de Servlets et communique avec l'une d'entre elles. Il est impossible, lors de l'utilisation de ce modèle d'exécution, que deux requêtes concurrentes soient liées à la même instance de Servlet. C'est important, car c'est cette impossibilité qui rend ce modèle d'exécution « Thread Safe », c'est-à-dire sûr, par rapport aux threads, sans modification dans le code, et sans devoir prêter attention à quoi que ce soit de particulier (comme la synchronisation).
La ou les Servlets désirant bénéficier de cette particularité doivent implémenter l'interface SingleThreadModel. Cela permet de bénéficier de ce comportement pour certaines Servlets de votre choix, et de conserver un comportement multithreadé pour les autres. C'est la seule contrainte au niveau de l'écriture du code source d'une Servlet utilisant un tel modèle d'exécution.
Bien entendu, les possibilités et avantages évoqués lors de la description du modèle d'exécution traditionnel ont disparu, mais d'autres sont apparus. D'une part vous êtes vraiment sûr que plusieurs requêtes effectuées en même temps ne produiront pas de résultats erronés (ce que vous ne pouvez garantir à moins d'être un expert en programmation multithread). Ensuite cela vous permet d'exploiter plus efficacement le caractère multithread des applications utilisées par vos Servlets. C'est un peu paradoxal, mais prenons l'exemple d'une Servlet liée à un SGBD : dans le cas d'une seule instance de Servlet, il n'est pas efficace de gérer les multiples connexions avec des comptes utilisateurs différents (correspondant aux utilisateurs effectuant la requête côté client) tout en synchronisant les connexions et les requêtes. C'est un peu le même problème que de réinventer la roue : le travail effectué en temps normal par le SGBD pour synchroniser les connexions multiples serait alors effectué par la Servlet. De plus, le traitement ne serait pas le plus efficace dans le cas du modèle d'exécution traditionnel, alors qu'il le serait dans le cas d'un modèle monothread, tout en débarrassant le programmeur de tâches fastidieuses de synchronisation. En effet chaque instance de la Servlet utilise son environnement et les requêtes envoyées à la base n'ont pas besoin d'être synchronisées.
Nous avons vu que, dans tous les cas, chaque Servlet était matérialisée par un thread lancé par une JVM, et qu'il existe deux modèles d'exécution : le modèle multithread et le modèle monothread. Le premier correspond très bien à une gestion centralisée des ressources (comme pour un serveur de discussion en temps réel par exemple). En effet, la servlet agit alors comme un répartiteur de messages entre les différentes personnes connectées, les informations doivent donc être centralisées. Le deuxième s'accorde très bien avec l'utilisation de systèmes gérant déjà le multithreading et nécessitant des traitements ponctuels efficaces qui ne se basent pas sur les requêtes précédentes. Ainsi l'absence de gestion de synchronisation permet de servir chaque requête au plus vite, alors que la concurrence des requêtes gérées par le SGBD est assurée par ce dernier.
La description du fonctionnement interne de la gestion des Servlets au sein d'un moteur de Servlets étant terminée, je vais maintenant décrire les différentes étapes du cycle de vie d'une Servlet.
IV-B. Le cycle de vie▲
De la compilation du code à l'arrêt du moteur de Servlets en passant par le traitement des requêtes provenant du client, les Servlets entament un chemin bien précis qui peut se décomposer (de manière très générale) en trois étapes :
- Le chargement ;
- Le traitement des requêtes, que l'on peut appeler exécution ;
- La destruction.
IV-B-1. Le chargement▲
Afin qu'une Servlet puisse accueillir les requêtes venant des clients, nous avons vu dans la section précédente qu'une instance de cette Servlet doit être présente. Le processus de chargement de la Servlet est effectué par le moteur de Servlet. Ce chargement peut être effectué au démarrage du moteur de Servlets ou bien juste au moment où le moteur détermine qu'il a besoin de la Servlet en question.
Tout d'abord, le moteur de Servlets recherche une classe du type de la Servlet à charger à l'endroit où se trouvent les classes des Servlets pour chaque contexte (voir contextes). Si la Servlet n'est pas déjà chargée, le moteur de Servlets charge la Servlet en utilisant un chargeur de classe normal à partir du système de fichier local, ou de toute autre ressource distante (cela dépend des contextes, pour plus de renseignements à propos des contextes, voir contextes). Une fois que la Servlet est chargée, elle est instanciée et un objet du type de la Servlet chargée est donc présent en mémoire. Une Servlet peut être chargée une fois ou plus par le moteur de Servlets, cela dépend du modèle d'exécution choisi : si la Servlet implémente l'interface SingleThreadModel plusieurs instances peuvent être créées en mesure du nombre de requêtes faites par l'utilisateur, si rien n'est précisé par le programmeur (la classe de la servlet n'implémente pas SingleThreadModel) une seule instance de la Servlet peut être présente. Plusieurs instances d'une même Servlet peuvent être créées également si plusieurs Servlets identiques possèdent des paramètres d'initialisation différents.
L'initialisation est la dernière étape du chargement d'une Servlet. Une Servlet doit être initialisée avant de pouvoir répondre aux requêtes provenant du client. En effet, il est possible que certaines variables d'instance (ou toutes) de la classe correspondant à la Servlet à charger requièrent des valeurs de départ, dépendant de paramètres connus seulement au moment où elles sont chargées ou enregistrées lors de leur dernière destruction.Les valeurs de ces paramètres d'initialisation peuvent être précisées par un outil de configuration pour certains moteurs de servlets (comme Java Web Server), par une balise spéciale lorsque l'on utilise SSI (voir section ssi) ou par programme. Le moteur de Servlet effectue cette initialisation en appelant la méthode init(ServletConfig config) de l'interface Servlet. un paramètre de type ServletConfig est passé lors de l'appel de cette méthode. Ainsi, il est possible d'accéder dans le code source de la Servlet aux paramètres d'initialisation employée pour cette dernière. L'objet de type ServletConfig passé en paramètre à cette méthode permet également d'accéder à un objet de type ServletContext (car il implémente l'interface ServletContext) qui décrit l'environnement d'exécution dans lequel tourne la Servlet. Il faut noter que si vous désirez personnaliser l'initialisation de votre Servlet en redéfinissant la méthode init de celle-ci, vous devez impérativement utiliser l'instruction
super
.init
(
config);
en admettant que config soit l'identifiant de l'objet de type ServletConfig passé en paramètre à la méthode init que vous redéfinissez.
Je vous ai décrit le fonctionnement du chargement d'une Servlet dans le cas où tout se déroule comme prévu. Mais qu'en est-il lorsqu'une erreur survient, ou que le chargement de la Servlet n'est pas désiré ? La méthode init peut générer une exception de type ServletException ou UnavailableException. Une fois l'exception générée, le processus de chargement est abandonné et l'instance de la Servlet est immédiatement détruite. La méthode destroy que nous allons découvrir d'ici peu n'est pas appelée dans ce cas. Une fonctionnalité intéressante est fournie par le constructeur de l'exception de type UnavailableException. En effet, il est possible, via un paramètre passé au constructeur de l'exception, d'imposer au moteur de Servlets de respecter un délai avant une nouvelle tentative de chargement de la Servlet dont le chargement vient d'échouer. L'instruction suivante :
throw
new
UnavailableException
(
"Charge du serveur trop élevée"
, 120
);
permet de respecter un délai de deux minutes avant la prochaine tentative de chargement à cause d'un taux d'occupation de la machine serveur trop élevé.
Le rechargement d'une servlet
Il existe une fonctionnalité mise à disposition des développeurs de servlets lorsqu'ils se trouvent dans une phase de développement soutenue. En effet, plutôt que d'effectuer des manipulations impliquant une interruption de service (le redémarrage du serveur) pour recharger une servlet afin de prendre en compte les modifications apportées au code, c'est le moteur de servlets qui recharge la servlet de façon automatique s'il pense que cela est nécessaire. Ainsi, il suffit de recompiler le code source d'une servlet pour qu'une nouvelle instance de celle-ci remplace l'ancienne instance. On peut également utiliser la commande touch sous Unix en lui passant comme paramètre le fichier de la classe à recharger pour effectuer la même chose (sans le coût d'une recompilation).
Pour déterminer s'il est nécessaire de recharger une classe, le moteur de servlets détermine si le fichier de classe a changé (en déterminant si la date de dernière modification du fichier de classe est bien égale à la date de modification du fichier de classe au moment de son dernier chargement par exemple). S'il a changé, le moteur de servlets arrête le chargeur de classe utilisé pour charger la classe précédente et effectue le chargement de la nouvelle classe avec un nouveau chargeur de classe. Il est nécessaire de passer par cette astuce, car il est impossible de charger deux fois la même classe avec le même ClassLoader.
Cette fonctionnalité peut être fort intéressante pour accélérer la mise à jour du code et ne pas interrompre le service pour cette manœuvre. Cependant, l'utilisation d'un ClassLoader différent de celui utilisé par les autres classes de la même application Web peut poser des problèmes liés au contexte et à la récupération d'éléments partagés, car les références utilisées par les autres classes de l'application peuvent ne plus être valides. Si la notion de contexte est importante pour votre application, je vous conseille de désactiver la possibilité du rechargement de classe à la volée ou de ne pas l'utiliser.
IV-B-2. L'exécution▲
L'exécution consiste, pour une Servlet, à accueillir les connexions initiées par les clients et à traiter leur demande.
En ce qui concerne les Servlets, il est possible de programmer des applications destinées à servir exclusivement les requêtes utilisant le protocole HTTP, ou bien tous types de requêtes. Dans le premier cas, la Servlet doit hériter de la classe HttpServlet, dans l'autre, elle hérite de la classe GenericServlet, toutes deux appartenant au package javax.servlet .
Pour répondre à une requête générique, le moteur de Servlets appelle la méthode service préalablement redéfinie dans le code de votre Servlet. Il est possible d'effectuer tous les traitements que vous désirez à partir de l'appel de cette méthode et de renvoyer la réponse. À cette fin, un objet de type ServletRequest possédant toutes les informations sur la requête et un objet de type ServletResponse permettant de transmettre les données produites au cours des traitements effectués par la Servlet au client sont passés en paramètres à cette méthode service.
Cependant, dans la plupart des cas vous utiliserez une classe héritant de la classe HttpServlet pour programmer vos Servlets.
Lorsqu'une requête est reçue par le moteur de Servlet, celui-ci examine d'abord de quel type de requête (on parle de méthode) il s'agit . Il en existe peu en ce qui concerne le protocole HTTP. Ce sont :
- GET : afin de demander au serveur de nous transmettre un flux d'information. Cela peut être un fichier statique ou des informations créées dynamiquement (c'est le cas des servlets ou toute autre solution de programmation web côté serveur) ;
- POST : même utilité que GET, mais les paramètres de la requête sont passés directement au serveur au lieu d'être concaténés à l'URL qui identifie la ressource à laquelle on désire accéder ;
- HEAD : afin de s'informer sur l'entête renvoyé par le serveur lors d'une requête donnée ;
- OPTIONS : permet de connaître les services disponibles sur un serveur ;
- PUT : permet de déposer un fichier dans l'arborescence du système de fichier localisé sur la machine faisant tourner le serveur Web ;
- DELETE : permet de supprimer un fichier de l'arborescence du système de fichier localisé sur la machine faisant tourner le serveur Web ;
- TRACE : permet de « pister » une requête afin de voir ce que voit le serveur.
Une fois que la méthode de la requête est identifiée, le moteur de Servlet appelle la méthode adéquate pour la Servlet à laquelle la requête tente d'accéder. Pour cela il appelle d'abord la méthode service de la classe qui se charge alors d'appeler la méthode adéquate. Voici la table de correspondance entre la méthode HTTP et la méthode appelée par le moteur de Servlets :
Méthode HTTP |
Méthode appelée par le moteur de Servlets |
---|---|
GET |
doGet() |
POST |
doPost() |
HEAD |
doHead() |
OPTIONS |
doOptions() |
PUT |
doPut() |
DELETE |
doDelete() |
TRACE |
doTrace() |
Deux objets sont passés à ces méthodes lorsqu'elles sont appelées :
- Un objet de type HttpServletRequest : il possède tous les renseignements sur les paramètres passés à la requête ;
- Un objet de type HttpServletResponse : il permet d'obtenir un flux de sortie pour communiquer la réponse au client.
Vous pouvez définir plusieurs de ces méthodes dans une servlet. Par exemple dans le cas où une servlet soit appelée par un formulaire qui peut utiliser soit la méthode POST soit la méthode GET pour transmettre sa requête, vous définirez le code de la méthode doGet et de la méthode doPost. Les méthodes présentes dans le tableau constituent l'équivalent de la méthode main pour une application classique ou la méthode start pour une applet. En fait la méthode main est la méthode service, mais elle appelle automatiquement la méthode de votre classe correspondant à la méthode HTTP utilisée par le client qui effectue la requête si vous ne la surchargez pas, ce qui est le cas le plus courant. On peut donc dire que du point de vue du programmeur, la méthode service est négligeable (dans le cas de servlets HTTP).
On remarque également que chacune de ces méthodes peut générer une exception, ce qui suppose que des erreurs peuvent survenir. Que se passe-t-il à ce moment-là ? Tout dépend de l'exception qui est générée. Si c'est une exception du type ServletException, c'est le signe d'une erreur lors du traitement de la requête effectué par la méthode en question (doXXX ou service) et le moteur de servlets doit prendre les mesures appropriées à l'annulation de la requête (terminer le thread créé pour l'occasion, etc.). Si l'erreur produite nécessite une suspension du service fourni par la servlet, c'est une exception du type UnavailableException qu'il faut générer. Si l'on déclare que cette suspension est permanente, grâce au paramètre entier du constructeur de l'exception, le moteur de servlets supprime la servlet des servlets actives, appelle sa méthode destroy et libère l'espace mémoire alloué à l'instance de la servlet. Si le délai passé en paramètre au constructeur de l'exception de type UnavailableException est positif, alors le moteur de servlets peut choisir de ne plus router les requêtes dont la servlet est la cible durant la période passée en argument par le programmeur. Durant la période de l'indisponibilité, le moteur de servlets établit une réponse contenant le code HTTP 503 (service non disponible) pour toute requête dont la cible est la servlet qui a généré l'exception. Il est utile d'adopter ce type de comportement lorsque, par exemple, le SGBD utilisé par la servlet pour enregistrer les données et qui est situé sur une autre machine n'est plus disponible.
Lors de l'apparition d'une erreur, il peut être utile d'informer le serveur web et l'administrateur de celui-ci qu'une erreur s'est produite. Il est également pratique, en vue d'un débogage, de disposer de la trace précise des erreurs qui sont survenues. Les serveurs disposent en général d'un système de fichiers journaux pour cela, et les moteurs de servlets ne font pas exception à la règle. Pour utiliser ces fichiers journaux, il est recommandé d'utiliser la méthode log(String message) ou log(Exception e, String message) de l'interface ServletContext. La première écrit le message dans le journal du serveur, la deuxième ajoute à ce message la trace de la pile (le résultat de e.printStackTrace()).
IV-B-3. La destruction▲
Pour certaines raisons, le moteur de servlets peut décider s'il doit conserver ou non l'instance (ou les instances) de la Servlet au sein de la machine virtuelle. L'instance d'une Servlet peut être détruite quelques millisecondes après sa création ou après l'arrêt du moteur de Servlets. Rien ne peut contraindre le moteur de Servlets à ne pas détruire l'instance d'une Servlet.
Une fois que le moteur de Servlets a déterminé qu'il n'est plus nécessaire de conserver une instance de la Servlet (par exemple quand le serveur doit libérer de l'espace mémoire, ou lorsque l'exécution du moteur de Servlets est arrêtée), il doit permettre à la Servlet de libérer les ressources qu'elle a acquises au cours de son exécution (que ce soient les buffers utilisés pour l'écriture en sortie, les fichiers lus et écrits ou encore les objets instanciés) et de sauvegarder ce qui doit être persistant. C'est pour cela que le moteur de Servlets appelle la méthode destroy de l'interface Servlet, implémentée par toutes vos Servlets (voir ecrire_premiere_servlet). Avant cela, tous les threads qui ont été créés au sein de la méthode service de la Servlet doivent soit être terminés, soit dépasser un temps défini par le moteur de Servlets.
Une fois la méthode destroy() appelée, le moteur de Servlets ne doit plus router les requêtes provenant des utilisateurs vers la Servlet concernée. Dans le cas où le moteur de servlets pense que l'instance de la servlet puisse être utile à nouveau, il doit créer une nouvelle instance de celle-ci et poursuivre la destruction de la précédente. Une fois que la fin du corps de la méthode destroy() est atteinte, le moteur de servlets doit détruire l'instance de la servlet et rendre l'objet éligible par le ramasse-miettes de la machine virtuelle.
Vous disposez maintenant d'une vue d'ensemble suffisante pour comprendre les différentes étapes que devront emprunter les servlets que vous programmerez. Je vais maintenant aborder un autre concept important qui influe sur le fonctionnement des servlets que vous programmerez au sein du moteur de servlets que vous utilisez : les contextes.
IV-B-4. Les contextes▲
Lorsque vous développez des Servlets, vous développez des applications différentes qui répondent à des exigences différentes et qui ont donc des besoins différents. Par exemple, vous pouvez mettre à disposition des visiteurs de votre site un forum de discussions, une application de configuration du serveur à distance (lorsque vous êtes en congé et que votre patron vous appelle en urgence pour redémarrer le serveur Web) et votre site Web principal. Toutes ces applications sont indépendantes l'une de l'autre, car elles n'utilisent pas les mêmes ressources. Chaque classe correspondant à chaque composant de l'application doit rester visible seulement pour les autres classes de la même application. Par contre, au sein d'une même application, il doit être possible de partager des données facilement.Par exemple, dans l'application de commerce en ligne, une servlet permet aux utilisateurs de s'identifier au moment d'entrer sur le site. Lorsque l'un d'eux effectue un achat, c'est une autre servlet de l'application de commerce en ligne qui sera executée, et elle pourra puiser dans la liste des utilisateurs identifiés pour vérifier que l'acheteur s'est bien identifié. Pour toutes ces raisons, le concept de contexte a été créé.
IV-B-4-a. Définition d'un contexte▲
Un contexte constitue pour chaque servlet d'une même application une vue sur le fonctionnement de cette application. Une application web peut être composée de :
- Servlets ;
- JSP ;
- Classes utilitaires ;
- Documents statiques (pages html, images, sons, etc.) ;
- Beans, applications clients ;
- Métainformations décrivant la structure de l'application.
Dans le code source d'une servlet, un contexte est représenté par un objet de type ServletContext qui peut être obtenu par l'intermédiaire d'un objet de type ServletConfig, par exemple de la façon suivante :
public
void
init
(
ServletConfig config) {
ServletContext contexte =
config.getServletContext
(
);
/* suite du code */
}
Grâce à ce contexte, il est possible d'accéder à chacune des ressources de l'application web correspondant au contexte. Il faut noter qu'à une application correspond un et un seul contexte, et qu'à un contexte correspond une et une seule application. On peut donc en déduire que chaque contexte est propre à une application et qu'il n'est pas possible de partager des ressources entre applications différentes. Par exemple la servlet ServletLogin de l'application de commerce en ligne ne peut pas accéder aux instances de servlets de l'application de configuration du serveur web. Les ressources qu'il est possible de partager sont :
- Des documents statiques. Vous pouvez accéder à n'importe quel document statique d'un contexte grâce à la méthode getRessource ou à getRessourceAsStream.Le chemin passé en paramètre est interprété de façon relative à la racine de l'application correspondant au contexte en cours. Cela rejoint ce que j'ai dit précédemment : une servlet ne peut accéder qu'aux ressources de l'application à laquelle elle appartient quand elle utilise un contexte ;
- Des instances de servlets : en utilisant la méthode getServlet ;
- Des paramètres d'initialisation pour toute l'application. En utilisant la méthode getInitParameter, il est possible de connaître la valeur d'un paramètre d'initialisation si on possède son nom. Pour connaître tous les paramètres d'initialisations définis, il faut utiliser la méthode getInitParameterNames ;
- Des attributs d'instance de servlets. Il est ainsi possible de partager des données au sein d'une même application. Pour cela, utilisez les méthodes setAttribute et getAttribute, en fournissant à chacune de ces méthodes le nom que devra avoir l'attribut au sein de l'application. Pour obtenir le nom de tous les attributs partagés, vous devez utiliser la méthode getAttributeNames et pour retirer un attribut, il suffit d'utiliser la méthode removeAttribute.
Vous devriez maintenant comprendre ce qu'est un contexte. C'est une abstraction supplémentaire qui permet de matérialiser les relations privilégiées que connaissent les modules d'une même application et le fait que chaque application est différente. Je vais maintenant décrire comment mettre en place plusieurs contextes différents avec le moteur de servlets Tomcat.
IV-B-4-b. Configuration avec Tomcat▲
À chaque contexte correspond une arborescence dans le système de fichiers qui contient les ressources accédées lors des requêtes vers le moteur de servlets. Cette arborescence est identique pour chaque contexte. Voici comment se décompose la structure des répertoires :
- La racine : elle fait office de répertoire racine pour les ressources qui font partie du contexte. Par exemple l'URL http://www.monserveur.fr/commerce/index.html fait référence au fichier index.html de ce répertoire racine. Vous pouvez créer les répertoires que vous désirez ou déposer les fichiers que vous voulez dans ce répertoire, par exemple un répertoire images/ qui sera accessible via l'URL http://www.monserveur.fr/commerce/images/ ;
- Le répertoire WEB-INF : situé à la racine, il contient un fichier web.xml qui est le descripteur de déploiement du contexte. Il contient tous les paramètres de configuration utilisés par le contexte ;
- Le répertoire WEB-INF/classes/ : c'est le répertoire dans lequel vous déposez les classes de vos servlets et des classes personnalisées utilisées par celles-ci. Le chargeur de classe de l'application vient chercher les classes à charger dans ce répertoire ;
- Le répertoire WEB-INF/lib/ : il permet de déposer les archives au format jar (Java ARchive Files) qui contiennent des servlets, des beans ou des classes utilitaires utilisées par l'application. Le chargeur de classe de l'application recherche aussi dans ces archives pour trouver les classes dont il a besoin.
Toute cette arborescence peut être regroupée dans une archive (compressée ou non) de la même manière qu'une application Java classique, en utilisant l'utilitaire jar (pour Java ARchive tool). De cette façon il n'y a plus qu'un seul fichier à manipuler et votre application peut-être signée, afin de la rendre digne de confiance auprès des gens qui utiliseront votre application web. En général, avant de procéder à l'archivage de l'arborescence de tout un contexte, il faut créer à sa racine un répertoire META-INF et y placer un fichier MANIFEST.MF contenant les informations nécessaires à l'outil jar, afin qu'il puisse trouver tous les composants de l'application au moment voulu (lors d'une requête par exemple). Il est dit dans les spécifications des servlets que ce répertoire ne doit pas être mis à disposition du client par le serveur Web qui transmet les requêtes au moteur de servlets (ou par le moteur de servlets lui-même s'il est utilisé comme serveur web en même temps). Le contenu de ce répertoire ne sera donc pas visible de l'extérieur à moins que vous en décidiez autrement. Pour archiver une application, il suffit d'entrer la commande :
jar cfvm application.war META-
INF/
MANIFEST.MF -
C /
usr/
local/
tomcat/
webapps/
appli/
ce qui produira le fichier application.war archivant l'application dont la racine est située dans le répertoire /usr/local/tomcat/webapps/appli/ et les informations sur l'archive contenues dans le fichier MANIFEST.MF situé dans le répertoire META-INF de la racine de cette application.
Vous savez maintenant que le fichier web.xml présent dans $APP/WEB-INF/ (où $APP correspond au répertoire de votre application web, par exemple /usr/local/tomcat/commerce) permet de spécifier les directives de configuration de l'application web. Voyons les concepts de base de la configuration d'une application web (et donc d'un contexte).
IV-B-4-b-i. La configuration d'un contexte▲
Le fichier web.xml est écrit en XML (vous vous en doutiez) et sa structure obéit aux règles d'une DTD définie spécifiquement par Sun. La DTD définit simplement quelles sont les balises qui sont utilisables à tel ou tel endroit du fichier de configuration. Vous devez préciser que vous utilisez cette DTD en haut du fichier web.xml de chaque application en ajoutant :
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application
2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd"
>
Les informations données par le fichier de configuration sont :
- Les paramètres d'initialisation du contexte ;
- La configuration de la session ;
- Les définitions des servlets et des JSP ;
- Les correspondances entre servlets et entre JSP ;
- Les correspondances entre types MIME ;
- La liste des fichiers de bienvenue ;
- Les pages d'erreur ;
- La sécurité.
Je ne vais pas aborder tous ces aspects de la configuration, mais sachez qu'il est possible de régler tous ces paramètres au sein d'un contexte. Je vous présente maintenant la configuration d'un contexte de base correspondant à notre application de configuration du serveur Web à distance.
<webapp>
<!-- nom de l'application -->
<display-name>
Application de configuration d'un serveur Web à distance
</display-name>
<!-- paramètres disponibles dans le contexte de l'application -->
<context-param>
<param-name>
Webmaster</param-name>
<param-value>
darktigrou@bigfoot.com</param-value>
</context-param>
<!-- définition de la servlet de login -->
<servlet>
<servlet-name>
login</servlet-name>
<servlet-class>
ServletLogin.class</servlet-class>
<!-- paramètres d'initialisation de la servlet -->
<init-param>
<pram-name>
maxlogin</param-name>
<param-value>
1</param-value>
</init-param>
</servlet>
<!-- fin de la définition de la servlet -->
<!-- correspondance entre URL et servlets appelées -->
<servlet-mapping>
<!-- toute requête effectuée vers une URL comprenant "/catalog/"
est redirigée vers la servlet login -->
<servlet-name>
login</servlet-name>
<url-pattern>
/login/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30</session-timeout>
</session-config>
<mime-mapping>
<extension>
pdf</extension>
<mime-type>
application/pdf</mime-type>
</mime-mapping>
<!-- définition des fichiers utilisés par défaut lors d'une requête concernant
une URL ne spécifiant pas de fichier -->
<welcome-file-list>
<welcome-file>
index.jsp</welcome-file>
<welcome-file>
index.html</welcome-file>
<welcome-file>
index.htm</welcome-file>
</welcome-file-list>
<!-- définition des pages d'erreur -->
<error-page>
<!-- pour une erreur de code HTTP 404, c'est le fichier 404.html présent à
la racine du contexte qui est transmis au client -->
<error-code>
404</error-code>
<location>
/404.html</location>
</error-page>
<!-- fin de la configuration du contexte -->
</web-app>
IV-B-4-c. Les différentes choses à savoir à propos des contextes▲
Les contextes sont très intéressants pour classer et organiser correctement vos applications. Cependant, l'implémentation des contextes dans la majorité des moteurs de servlets ne permet pas d'être certain qu'ils adopteront le comportement que l'on attend d'eux dans certaines circonstances.
C'est le cas lorsqu'on utilise le rechargement des servlets, mais j'ai déjà évoqué le problème précédemment (voir rechargement).
C'est aussi le cas lorsque vous travaillez dans un environnement distribué qui utilise plusieurs machines virtuelles pour des besoins de répartition de charge ou de tolérance de panne. Dans ce cas-là même si deux JVM exécutent la même application, deux contextes différents sont utilisés : il existe un seul contexte par application et par JVM.
Vous pouvez également utiliser la possibilité de « virtual hosting » de votre serveur Web. Ce procédé consiste à assigner plusieurs hôtes logiques à la même machine (par exemple http://www.linux.com/ et http://www.linux.net/ peuvent être deux sites différents hébergés par le même serveur web). Lorsqu'un tel procédé est utilisé, un contexte différent est créé pour chaque hôte virtuel ou chaque application dans le même hôte virtuel (ou hôte logique) : un contexte ne peut pas être partagé entre deux ou plusieurs hôtes virtuels.