Le développement d’applications web requière une vigilance toute particulière quant à l’utilisation de la session web. Spring MVC offre les mécanismes permettant aux développeurs de ne plus manipuler directement l’objet HttpSession mis à disposition par le conteneur web. Les 2 annotations @Scope(« session ») et @SessionAttributes en font parties. Dans ce billet, je vous expliquerai le fonctionnement de l’annotation @SessionAttributes qu’il est essentiel de maitriser avant d’utiliser. Nous verrons qu’elle fonctionne de pair avec l’annotation @ModelAttribute et qu’elle permet de simuler une portée conversation. Nous commencerons cet article par rappeler ce qu’est un modèle et nous le terminerons en testant unitairement du code qui utilise @SessionAttributes.
Le modèle de Spring MVC
Comme son nom l’indique, Spring MVC est un framework de présentation basé sur le pattern Model View Controller. Un modèle est mis à disposition de la vue par le contrôleur, par exemple pour alimenter les listes déroulantes lors du rendu de la page HTML. Un modèle peut également être soumis au contrôleur par la vue (post de formulaire) ; on parle alors de « command object ». La conversion de données (ou binding) entre des chaînes de caractères du protocole HTTP et la représentation Java du modèle est assuré par les Converter et les Formatter configurés au démarrage du contexte Spring ou via l’annotation @InitBinder pour du sur-mesure. Un binding bi-directionnel est mis en œuvre sur un modèle utilisé conjointement pour le rendu de la page et la soumission de données (ex : formulaire d’édition).
Spring MVC représente le modèle comme un ensemble de clé-valeur (tableau associatif). La clé est une chaine de caractère. La valeur peut-être de n’importe quel type. La classe ModelMap implémente cette représentation. Elle étend la classe java.util.LinkedHashMap.
Dans les contrôleurs Spring MVC, il est possible de manipuler l’interface Model pour ajouter manuellement des données au modèle soit directement, soit par l’utilisation de la classe ModelAndView. Voici un exemple tiré du manuel de référence de Spring Framework :
@ModelAttribute public void populateModel(@RequestParam String number, Model model) { model.addAttribute(accountManager.findAccount(number)); }
Pour implémentation de l’interface Model, Spring MVC utilise la classe BindingAwareModelMap qui étend indirectement ModelMap.
Dans cet exemple, l’instance renvoyée par l’appel à la méthode findAccount est de type Account. La clé est calculée par convention de nommage via la classe org.springframework.core.Conventions. Il est bien entendu possible d’utiliser la méthode model.addAttribute(« account », accountManager.findAccount(number)); pour spécifier une clé.
L’enrichissement du modèle peut également être réalisé sans manipulation de l’interface Model :
@ModelAttribute public Account addAccount(@RequestParam String number) { return accountManager.findAccount(number); }
Sur mes applications, je privilégie cette seconde syntaxe qui est moins verbeuse et permet de découper le code en autant de méthodes que d’objets à ajouter dans le modèle.
D’un point de vue technique, les 2 exemples présentés ci-dessus sont équivalents. Concentrons-nous à présent sur le rôle de L’annotation @ModelAttribute.
Annotation @ModelAttribute sur les méthodes
Le comportement de l’annotation @ModelAttribute diffère en fonction de là où elle est apposée :
- sur les méthodes des contrôleurs
- sur les paramètres des méthodes des contrôleurs.
Dans les exemples précédents, l’annotation @ModelAttribute annote une méthode d’un contrôleur. Elle indique à Spring MVC que la méthode est responsable de préparer le modèle. A noter que plusieurs méthodes d’un même contrôleur peuvent être annotés avec @ModelAttribute. Spring MVC appelle toutes les méthodes @ModelAttribute avant d’appeler la méthode @RequestMapping (également appelé handler) chargée de traiter la requête HTTP en appelant les services métiers.
Les données ajoutées au modèle dans les méthodes @ModelAttributes sont ensuite accessibles à la méthode @RequestMapping.
Dans le second exemple, la méthode addAccount renvoie un Account sans manipuler l’interface Model. Spring MVC sait implicitement que l’objet retourné par une méthode @ModelAttribute doit être ajouté au modèle. Pour la clé, il utilise les mêmes conventions de nommage que la méthode addAttribute(Object attributeValue) . Il est possible de spécifier la clé en utilisant la syntaxe @ModelAttribute(« account ») .
Une fois l’appel à la méthode @RequestMapping réalisé, et avant le rendu de la vue, Spring MVC doit mettre à disposition de la vue le modèle.
Par défaut, Spring MVC utilise les attributs de la requête. Tout se joue dans la méthode exposeModelAsRequestAttributes de la classe AbstractView. Les objets du modèle sont ajoutés aux attributs de la requête comme on pourrait le faire en manipulant l’API Servlet :
request.setAttribute(modelName, modelValue);
Lorsque le mode debug est activée, la trace suivante est généré dans les logs :
18:42:41.702 [qtp20079748-21] DEBUG o.s.web.servlet.view.JstlView - Added model object 'account' of type [com.javaetmoi.core.mvc.demo.model.Account] to request in view with name 'accountdetail'
Ici, la vue est une JSP utilisant les tags JSTL.
Dans le corps de la page JSP, il est alors possible d’utiliser une Expression Language (EL) évaluant les propriétés du modèle :
<c:out value= »${account.number} » />
Attention aux performances
L’annotation @ModelAttribute peut causer des problèmes de performance si l’on ne maîtrise pas son cycle d’appel dans les contrôleurs de Spring MVC.
En effet, l’appel systématique aux méthodes @ModelAttribute à chaque rafraichissement de page peut détériorer les performances d’une application lorsqu’un appel à un ou plusieurs web services et/ou DAO est nécessaire pour construire le modèle.
L’utilisation de l’annotation @SessionAttributes ou d’un cache applicatif permet d’enrayer ce type de déconvenue.
L’annotation @SessionAttributes
Les handlers des contrôleurs Spring MVC (annotés avec @RequestMapping) acceptent en paramètre de nombreux types de paramètres ; les interfaces HttpSession et HttpServletRequest en font partie. Un développeur peut donc directement manipuler HttpSession pour ajouter en session des données du modèle qu’ils voudraient voir conserver sur plusieurs requêtes HTTP.
Afin de simplifier le code d’accès à la session web, et toujours dans l’idée d’éviter de manipuler directement la session, Spring MVC propose l’annotation @SessionAttributes. Cette annotation se déclare au niveau de la classe de type @Controller. Ses 2 propriétés value et type permettent de lister respectivement le nom des modèles (le nom des clés) et/ou le type de modèle à sauvegarder de manière transparente dans la session HTTP.
Avant le rendu de la vue, Spring MVC copie par référence les attributs du modèle référencés par @SessionAttributes dans la session. Les attributs du modèle seront alors à la fois disponible en tant qu’attribut de la requête (HttpServletRequest) et de la session (HttpSession).
Pour persister les données du modèle en session, Spring MVC utilise l’abstraction SessionAttributeStore. L’implémentation par défaut repose sur la session HTTP. Mais on pourrait très bien imaginer une implémentation utilisant un cache de données distribué (type Redis ou GemFire) ou une base NoSQL. Gains escomptés de cette approche :
- Affinité de session plus nécessaire
- Tolérance aux pannes renforcées
- Livraisons sans interruption de service
Cette ouverture sera peut-être prochainement exploitée par le nouveau projet spring-session.
Une autre facilité apportée par l’annotation @SessionAttributes est d’éviter au développeur de tester si un objet existe déjà en session avant de l’instancier/ou de le récupérer puis de l’ajouter à la session.
En effet, avant d’invoquer la méthode @RequestMapping cible, Spring MVC commence par initialiser le modèle du contrôleur (méthode RequestMappingHandlerAdapter#invokeHandleMethod). Dans un premier temps, il restaure les attributs du modèle qui sont en session (méthode ModelFactory# initModel). Dans un second temps, il itère sur les méthodes annotées par @ModelAttributes (méthode ModelFactory#invokeModelAttributeMethods). Avant d’appeler chaque méthode @ModelAttributes, il vérifie si l’attribut retourné n’existe pas déjà dans le modèle (et donc préalablement en session).
Le diagramme d’activités ci-dessous illustre le mécanisme complet :
Libérer la mémoire
A présent que nous avons vu comment ajouter des données en session, apprenons à les retirer, et ceci toujours sans manipuler l’interface HttpSession. Pour se faire, Spring MVC met à disposition l’interface SessionStatus.
La méthode setComplete() permet de supprimer de la session tous les attributs référencés par l’annotation @ModelAttributes du contrôleur où elle est appelée.
Comme le montre l’exemple de code tiré du projet spring-mvc-toolkit, Spring MVC sait passer au handler une instance de SessionStatus :
@RequestMapping("/endsession") public String endSessionHandlingMethod(SessionStatus status){ status.setComplete(); return "sessionsattributepage"; }
Lorsqu’un attribut a été retiré de la session (« myBean1 » dans l’exemple ci-dessous) et que l’on cherche à initier le modèle à partir des données en session @SessionAttributes(« myBean1 »), Spring MVC lève une HttpSessionRequiredException :
org.springframework.web.HttpSessionRequiredException: Expected session attribute 'myBean1' at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:103) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:726)
Démonstration
Les explications données dans ce blog s’appuient sur des tests réalisés dans la branche SessionAttributes du projet spring-mvc-toolkit. Reprenant l’idée présentée dans le billet Understanding Spring MVC Model and SessionAttributes, les 2 contrôleurs MyController et OtherController tracent l’appel de méthodes et affichent le contenu du modèle, de la requête et de la session. La page sessionsattributepage.jsp affiche quant à elle le contenu de la requête et de la session.
Extrait de la classe MyController:
Extrait de la classe MyController.java
Extrait de la classe OtherController:
Extrait de la classe OtherController.java
Voici les étapes à suivre pour exécuter l’application démo. Les prérequis sont d’avoir installé sur son post Git, Java 6 ou + et maven 3 ou + :
- git clone git://github.com/arey/spring-mvc-toolkit.git
- git checkout SessionAttributes
- mvn clean install
- cd spring-mvc-toolkit-demo
- mvn jetty:run-war
- Naviguer sur http://localhost:8080/dosomething
Nous allons décrire à présent les traces affichées et le contenu des pages observé lors de la navigation sur les liens.
Appel à dosomething
Traces observées lors de l’appel à http://localhost:8080/dosomething :
Inside of addMyBean1ToSessionScope Inside of addMyBean2ToRequestScope Inside of addMyOtherBeanAToSessionScope Inside of addMyOtherBeanBToSessionScope Inside of dosomething handler method --- Model data --- myBean1 -- MyBean [name=My Bean 1] myBean2 -- MyBean [name=My Bean 2] myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] === Request data === *** Session data ***
Page affichée dans le navigateur :
- Les 4 méthodes annotées par @ModelAttribute sont appelées avant la méthode @RequestMapping.
- Les beans créés par chacune de ces méthodes sont disponibles dans le modèle dès l’appel à la méthode @RequestMapping.
- Lors de l’appel à la méthode @RequestMapping, la requête et la session HTTP ne contiennent encore aucun attribut.
- Lors du rendu de la page, les 4 beans sont présents au niveau de la requête. Par contre, seul les 3 beans référencés par l’annotation @SessionAttributes( value= »myBean1″, types={MyOtherBean.class} ) sont présents en session.
Premier appel à other
Traces observées lors du clic sur le lien « /other » :
Inside of addMyBean3ToSessionScope Inside of other handler method MyBean [name=My Bean 1] --- Model data --- myBean3 -- MyBean [name=My Bean 3] myBean1 -- MyBean [name=My Bean 1] === Request data === *** Session data *** myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1]
Page affichée dans le navigateur :
- Lors de l’appel à la méthode @RequestMapping :
- les 2 beans référencés par l’annotation @SessionAttributes({« myBean1 », « myBean3 »}) sont disponibles dans le modèle,
- les beans myOtherBeanA et myOtherBeanB sont présents en session mais pas recopiées dans le modèle
- Lors du rendu de la page JSP : Le bean myBean3 créé par le contrôleur est ajouté à la session qui compte désormais 4 beans
Appel à endsession
Trace observée lors du clic sur le lien « /endession » :
Inside of addMyBean2ToRequestScope --- Model data --- myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1] myBean2 -- MyBean [name=My Bean 2] === Request data === *** Session data *** myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1] myBean3 -- MyBean [name=My Bean 3]
Page affichée dans le navigateur :
Analyse :
- L’URL /endession est mappée sur le contrôleur MyController déjà utilisé lors du 1er accès à l’URL /dosomething
- Seule l’une des 4 méthodes annotées avec @ModelAttribute est appelée : addMyBean2ToRequestScope. Les 3 autres méthodes ne sont pas appelées car les beans qu’elles créent sont déjà présent en session.
- L’appel à la méthode setComplete(); ne retire pas instantanément les beans de la session mais joue le rôle de marqueur.
- Les beans référencés par MyController sont supprimés de la session avant le rendu de la page JSP. Bien que supprimés de la session, ils sont disponibles dans le scope request.
Second appel à other
Injecté dans le le handler, le bean myBean1 n’est plus disponible en session.
public String otherHandlingMethod(Model model, HttpServletRequest request, HttpSession session, @ModelAttribute(« myBean1 ») MyBean myBean) {
Tests unitaires
La mise au point de tests unitaires ou de tests d’intégration mettant en jeu ou plusieurs contrôleurs annotés avec @SessionAttributes nécessite un travail supplémentaire.
En effet, lorsque le handler d’un contrôleur s’appuie sur une donnée qui devrait être présente en session, il est nécessaire d’utiliser la méthode sessionAttr pour passer au contrôleur la donnée attendue.
Par ailleurs, entre 2 appels de handler, Spring Test ne conserve pas les données sauvegardées en session. Lors du 2ième appel, il est donc sorte nécessaire de réinjecter la donnée créée lors du premier appel. La classe MvcResult permet d’accéder au résultat du 1er appel.
Le test unitaire SessionAttributesTest monte un exemple d’utilisation :
Extrait de la classe SessionAttributesTest.java
Conclusion
Introduite depuis Spring 2.5, l’annotation @SessionAttributes n’a pas d’équivalent dans d’autres frameworks MVC. Je pense par exemple à Struts. Son utilisation demande de comprendre son fonctionnement et la « magie » qu’on peut lui prêter. J’espère que cet article vous aura permis de démystifier ces mécanismes. La prochaine fois que vous l’utiliserez, je vous invite à vous référer au diagramme présenté au milieu de ce billet. N’hésitez pas non plus à cloner le projet spring-mvc-toolkit et à jouer avec la branche SessionAttributes.
Références :
- Manuel de référence du framework Spring MVC
- Understanding Spring MVC Model and SessionAttributes
- Projet Spring Session
- Power of Spring’s @ModelAttribute and @SessionAttributes
- Spring MVC – Session Attributes handling
Hi,
Look like it´s a good post, but I dont speak french and the translation is too bad. I´m very interested on reading this, so, if possible, translate it to english. Thanks in advance!
Excellent billet! Merci beaucoup!
Très pédagogique ! merci pour cet excellent article.
Excellent article, technique et bien expliqué.
N’y a-t-il pas une légère erreur dans la partie expliquant l’annotation @ModelAttribute ?
« Il est possible de spécifier la clé en utilisant la syntaxe @RequestMapping(« account »). »
=> @ModelAttribute(« account »)
Merci Florian pour avoir relevé cette erreur. Je viens de la corriger.