TP 3 : Design patterns côté serveur en Java
Ce TP fait suite au TP2. Dans cette partie, vous allez refactorer l'application réalisée et tirer un meilleur parti des possibilités du protocole HTTP pour améliorer les performances de votre application.
Objectifs pédagogiques
- Utiliser les APIs Java pour la programmation côté serveur (servlets/JSP/filtres)
- Concevoir une application Web complète et fonctionnelle à l'aide de méthodes de génie logiciel
- Commencer l'optimisation d'une application côté serveur
Date de rendu
Ce TP et le précédent sont à réaliser et à pusher dans la forge pour le dimanche 3 novembre 2019 à 23h59.
Outils
- Serveur Tomcat installé sur votre machine et sur votre VM
- Navigateurs Web
- IDEs NetBeans / Eclipse / IntelliJ
Refactoring de votre application
Une fois que l'utilisateur est connecté, la totalité de la logique applicative est gérée par billet.jsp, ce qui n'est pas très satisfaisant.
Pattern contexte
Le pattern contexte permet aux éléments d'une application de communiquer et d'échanger des informations. L'API Servlet fournit, avec le conteneur de servlets, un objet nommé ServletContext. Vous allez l'utiliser pour partager des données du modèle entre les différents éléments de votre application.
Par exemple, vous aurez besoin de la notion de groupe dans plusieurs CU : gestion des groupes, mais aussi gestion des billets. Le plus simple est de mettre le modèle à disposition de l'application dès l'appel de la servlet Init. Pour cela :
- Commencez par créer une classe Groupe : Un groupe contiendra un nom, une description textuelle, un propriétaire, une liste de participants et une instance de GestionBillets. Générez les constructeurs (vide et avec tous les paramètres), ainsi que les getters et setters avec votre IDE.
- Dans la méthode init(ServletConfig) de la servlet Init, récupérez le contexte applicatif, et ajoutez-y une instance de HashMap<String, Groupe> ; vous êtes libres de choisir le nom de l'attribut, mais "groupes" semble un bon candidat.
- Dans les JSP qui utilisent les groupes, vous pouvez y accéder avec une "action JSP" <jsp:usebean>, en utilisant l'attribut type (et non class) et le type de son interface : java.util.Map.
Testez.
Pattern MVC
Refactorez votre application pour appliquer un pattern MVC. Il existe 2 façons de le mettre en place, que vous allez utiliser dans 2 cas d'utilisation différents :
MVC pull-based
- Le code du contrôleur se trouve dans une servlet. Le contrôleur se contente d'aiguiller les requêtes vers les vues JSP.
- Le code de la vue se trouve dans des JSP. Chaque JSP est responsable d'interroger les objets du modèle pour se mettre à jour.
- Le code du modèle se trouve dans des JavaBeans. Les scopes de ces beans sont définis en fonction de la durée de vie des informations qu'ils contiennent.
Mettez en place cette version du MVC pour les cas d'utilisation de gestion des groupes d'utilisateurs :
- Créez une servlet contrôleur qui répondra aux requêtes en GET ou en POST. Dans les deux cas, elle se contentera de transférer la requête à la vue.
- Pour le modèle, vous utiliserez la Map<String, Groupe> de la question précédente.
- Pour la vue, créez une nouvelle page JSP (sur le même modèle que billets.jsp) qui permettra à l'utilisateur de sélectionner / créer un groupe d'utilisateurs. Pour l'y aider, affichez toutes les informations dont vous disposez sur chaque groupe dans la JSP. La vue accèdera au modèle en tant que JavaBean à l'aide de l'action <jsp:useBean>. À vous de déterminer le scope de ce bean.
Pour permettre aux utilisateurs d'accéder à ce code :
- Revenez à la version initiale du formulaire de login.
- Après la phase de login (servlet Init), redirigez le client vers la gestion des groupes.
- Rajoutez également un lien vers cette partie dans le menu de l'application.
- À la fin de la JSP de gestion des groupes, placez un formulaire qui renvoie vers une servlet User (qui ne sera pas approfondie ici) qui :
- rajoute le nom du groupe à la session de l'utilisateur,
- rajoute le pseudo de l'utilisateur au groupe et
- redirige (en interne) vers le CU de gestion des billets ci-dessous.
Si vous avez modifié la servlet Init à la question 1.4 du TP précédent, revenez à la version précédente (doGet redirige vers la page d'accueil).
Testez.
MVC Push-based
Dans cette solution :
- Le code du contrôleur se trouve dans une servlet. Le contrôleur est responsable de déclencher les changements d'états du modèle, puis de récupérer les données et d'aiguiller les requêtes vers les JSP vues.
- Le code de la vue se trouve dans des JSP. Chaque JSP recevra en attribut de requête les données du modèle transmises par le contrôleur.
- Le code du modèle se trouve dans des classes Java connues du contrôleur.
Mettez en place cette version du MVC pour les cas d'utilisation de consultation et d'ajout de billets. Pour cela :
- Mettez en place une servlet qui servira de contrôleur pour ces cas d'utilisation. Elle aura pour responsabilité de répondre aux méthodes HTTP GET et POST en interrogeant le modèle (l'instance de GestionBillets correspondant au groupe), en récupérant l'objet contenant les données nécessaires au traitement de la requête (i.e. le billet demandé) et en le transférant à la vue sous forme d'attribut de requête.
- Modifiez le fonctionnement de la vue (billet.jsp) pour qu'elle n'ait connaissance que de ce billet.
- Pour le modèle, vous utiliserez la Map placée précédemment dans le contexte.
Testez.
Contrôle de l'accès aux vues
Maintenant que toutes vos JSP sont derrière un contrôleur, vous allez faire en sorte qu'on ne puisse plus y accéder directement. C'est une étape de la sécurisation de votre application.
- Déplacez vos JSP dans un sous-dossier (nommé par exemple "jsp") du dossier WEB-INF qui n'est pas exposé aux clients,
- Utilisez des redirections internes depuis vos contrôleurs vers le chemin local : WEB-INF/jsp/xxx.jsp
Pattern chaîne de responsabilité
Authentification
Créez un Filter qui vérifie, en fonction de l'URL de la page appelée (voir plus bas), que l'utilisateur est connecté (qu'il possède un login), avant de lui permettre d'accéder à l'application. Si ce n'est pas le cas, il sera redirigé vers le formulaire de connexion. Pour cela :
- Créez une classe qui implémente l'interface javax.servlet.Filter
- Reprenez (et adaptez) le contenu de la méthode de service de la servlet Init dans celle de votre filtre :
- Si l'utilisateur possède un pseudo : poursuivez le traitement à l'aide de la méthode chain.doFilter()
- Sinon : si la requête provient du formulaire de connexion, traitez-la comme le faisait la servlet Init
- Sinon : renvoyez un code de redirection dans la réponse pour que l'utilisateur aboutisse au formulaire de connexion
- Déclarez ce filtre dans le fichier de configuration de l'application
Attention : le filtre ne doit pas bloquer l'accès au formulaire de login. Vous configurerez votre application pour qu'il soit appliqué à toutes les URL sauf celle de ce formulaire.
Autorisation
Créez un second filtre qui vérifie, quand un utilisateur souhaite accéder à / poster un billet dans un groupe, qu'il est bien membre de ce groupe.
Remarque : l'ordre d'exécution des filtres par le conteneur de servlet est celui d'apparition dans le fichier web.xml. Il ne suffit donc pas d'annoter les filtres dans le code.
À ce stade, vous avez refactoré votre application en utilisant certains patterns vus en Génie Logiciel. Vous avez rendu le code plus lisible, en utilisant les différents éléments de l'API Java EE Web à bon escient.
Gestion du cache
L'état actuel de votre application est le suivant : toutes les 5 secondes, chaque client interroge le serveur, qui parcourt alors toute la liste de billets et re-génère dynamiquement un nouvel affichage contenant la totalité des billets enregistrés, qui sera renvoyé à chaque client via le réseau. Si le nombre d'utilisateurs est grand et/ou s'il y a de nombreux billets enregistrés dans la base, ce fonctionnement peut vite représenter une charge de travail importante pour le serveur, ainsi qu'une consommation inutile de bande passante réseau.
L'objectif de cette partie est d'améliorer le processus de récupération et d'affichage des billets échangés par les différents utilisateurs. Pour cela, vous allez permettre à votre serveur de déterminer s'il' doit recalculer l'affichage des billets pour un client, ou s'il n'y a pas lieu de le faire. Deux possibilités sont à votre disposition.
Utilisation des en-têtes HTTP
La plupart des clients renvoient au serveur un en-tête HTTP If-Modified-Since, indiquant la date de leur dernier chargement d'une page. Si cette date est postérieure à celle du dernier POST reçu, aucun nouveau billet n'est disponible. Plutôt que de renvoyer une réponse HTTP contenant une page complète, il suffit de renvoyer juste un en-tête HTTP, avec le code de statut 304 indiquant au client que la page n'a pas été modifiée.
Pour cela :
- Vérifiez que votre serveur envoie bien au client un en-tête Last-Modified à la génération de la réponse (sur certaines versions de Tomcat, le header est positionné par défaut sur les pages HTML et pas sur les JSP) ; sinon, rajoutez-le dans billet.jsp.
- Vérifiez que votre client vous renvoie bien cette valeur dans un en-tête If-Modified-Since lors de la requête d'actualisation de la page billet.jsp
- Lors de l'appel en GET de billet.jsp, comparez la valeur de l'en-tête If-Modified-Since reçu avec celle de l'attribut du contexte applicatif et réagissez en conséquence (génération de la page ou code de statut 304). Vous pouvez vous aider de ce tutoriel.
Il est possible que votre navigateur ne prenne plus en compte le header Refresh après réception d'un code de retour 304. Ne passez pas trop de temps à essayer de régler ce problème, qui ne se posera plus dans les versions ultérieures.
Utilisation des cookies
Si le numéro du dernier billet mémorisé (i.e. le nombre total de billets) est égal à celui du dernier billet envoyé à un client (i.e. le nombre total de billets au moment où le serveur lui a servi la réponse), cela signifie qu'aucun billet n'est arrivé depuis la dernière requête du client. Il est donc inutile de lui renvoyer la page.
- rajoutez / updatez un cookie chez votre client indiquant le numéro du dernier billet obtenu lors de l'envoi d'une réponse par Affichage.jsp
- lors de la réception d'une requête GET par billet.jsp, testez l'existence de ce cookie et comparez sa valeur au nombre total de billets mémorisés
- réagissez en conséquence (voir plus haut).
Indication :
Utilisez le getter de GestionBillets permettant de récupérer le nombre total de billets.
Vous avez maintenant commencé l'optimisation de votre application en termes de charge serveur et de bande passante réseau. Ce travail sera poursuivi lors des TPs REST et AJAX.
Déploiement
Déployez votre application sur votre VM avec le contexte v1. Elle doit donc être accessible aux URLs suivantes :
- http://192.168.75.xx:8080/v1/
- https://192.168.75.xx/api/v1/
Aide :
- La manager app de Tomcat est conçue pour être à la racine du serveur et les liens et actions de formulaires renvoient donc un 404 quand elle est installée ailleurs (ce que nous voulons justement éviter concernant votre application...). Le plus simple pour arriver à l'utiliser est de rajouter une directive de proxy dans nginx qui redirige /manager/ (en HTTPS) sur /manager/ (en HTTP sur le port 8080).
- Si vous utilisez des cookies (même pour la session), ils sont associés à un path spécifique sur le serveur. Comme celui-ci n'est pas le même sur le serveur nginx vu par le client et sur le serveur Tomcat qui positionne les cookies, il faut demander à votre proxy de réécrire également ce path dans les cookies. Pour cela, rajoutez dans votre config de proxy nginx la ligne
proxy_cookie_path ~*^/.* /;
(voir ici pour plus d'explications).
Testez.
Si le comportement n'est pas le même à partir des 2 URLs, c'est que vous avez mis des URLs absolues quelque part.
Remarque : les URLs dans la configuration des servlets et des filtres (dans les annotations : url-patterns ou le fichier web.xml : servlet-mappping) ne sont pas absolues, même si elles commencent par un slash ; elles définissent un chemin à partir du contexte de déploiement de l'application.