Tutoriel sur l'annotation @SessionAttributes de Spring MVC

Image non disponible

Cet article présente des annotations de Spring MVC qui permettent, lors des développements d'applications web, de ne plus avoir besoin de manipuler l'objet HttpSession mis à disposition par le conteneur web.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum. 1 commentaire Donner une note à l'article (5).

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Le développement d'applications web requiert 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 deux annotations @Scope("session") et @SessionAttributes en font partie. Dans ce billet, je vous expliquerai le fonctionnement de l'annotation @SessionAttributes qu'il est essentiel de maîtriser avant utilisation. 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.

II. 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ée 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 bidirectionnel est mis en œuvre sur un modèle utilisé conjointement pour le rendu de la page et la soumission de données (exemple : formulaire d'édition).
Spring MVC représente le modèle comme un ensemble de clés-valeur (tableau associatif). La clé est une chaîne 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 :

 
Sélectionnez
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
        model.addAttribute(accountManager.findAccount(number));
}

Pour l'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 :

 
Sélectionnez
@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 deux exemples présentés ci-dessus sont équivalents. Concentrons-nous à présent sur le rôle de l'annotation @ModelAttribute.

III. Annotation @ModelAttribute sur les méthodes

Le comportement de l'annotation @ModelAttribute diffère en fonction de là où elle est apposée :

  1. Sur les méthodes des contrôleurs ;
  2. 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. À noter que plusieurs méthodes d'un même contrôleur peuvent être annotées 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 @RequestMapping("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 :

 
Sélectionnez
request.setAttribute(modelName, modelValue);

Lorsque le mode debug est activé, la trace suivante est générée dans les logs :

 
Sélectionnez
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 :

 
Sélectionnez
<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 rafraîchissement 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.

IV. 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'il voudrait 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 dernière se déclare au niveau de la classe de type @Controller. Ses deux 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èles à 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. Ces attributs seront alors à la fois disponibles en tant qu'attributs de la requête (HttpServletRequest) et de la session (HttpSession).
Pour conserver 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 :

Image non disponible

V. Libérer la mémoire

À présent que nous avons vu comment ajouter des données en session, apprenons à les retirer, et cela 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 :

Appel à la méthode setComplete()
Sélectionnez
@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 :

 
Sélectionnez
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.RequestMappingHandler

VI. 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 deux 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 :

Classe de test
Sélectionnez
@Controller
@SessionAttributes( value="myBean1", types={MyOtherBean.class} )
public class MyController {
    
    private static final Logger LOG = LoggerFactory.getLogger(MyController.class);
    @ModelAttribute("myBean1")
    public MyBean addMyBean1ToSessionScope() {
        LOG.info("Inside of addMyBean1ToSessionScope");
        return new MyBean("My Bean 1");
    }
    
    @ModelAttribute("myBean2")
    public MyBean addMyBean2ToRequestScope() {
        LOG.info("Inside of addMyBean2ToRequestScope");
        return new MyBean("My Bean 2");
    }
    
    @ModelAttribute("myOtherBeanA")
    public MyOtherBean addMyOtherBeanAToSessionScope() {
        LOG.info("Inside of addMyOtherBeanAToSessionScope");
        return new MyOtherBean("My Other Bean A");
    }
    
    @ModelAttribute("myOtherBeanB")
    public MyOtherBean addMyOtherBeanBToSessionScope() {
        LOG.info("Inside of addMyOtherBeanBToSessionScope");
        return new MyOtherBean("My Other Bean B");
    }        
    @RequestMapping("/dosomething")
    public String doSomethingtHandlingMethod(Model model, HttpServletRequest request, HttpSession session) {
        LOG.info("Inside of dosomething handler method");
        printModel(model);
        printRequest(request);
        printSession(session);
        return "sessionsattributepage";
    }
    
    @RequestMapping("/endsession")
    public String endSessionHandlingMethod(SessionStatus status, Model model, HttpServletRequest request, HttpSession session){
        status.setComplete();
        printModel(model);
        printRequest(request);
        printSession(session);        
        return "sessionsattributepage";
      }
}

Extrait de la classe OtherController :

Classe de test
Sélectionnez
@Controller
@SessionAttributes({"myBean1", "myBean3"})
public class OtherController {
    
    private static final Logger LOG = LoggerFactory.getLogger(MyController.class);
    
    @ModelAttribute("myBean3")
    public MyBean addMyBean3ToSessionScope() {
        LOG.info("Inside of addMyBean3ToSessionScope");
        return new MyBean("My Bean 3");
    }
    @RequestMapping("/other")
    public String otherHandlingMethod(Model model, HttpServletRequest request, HttpSession session, @ModelAttribute("myBean1") MyBean myBean) {
        LOG.info("Inside of other handler method");
        LOG.info(myBean.toString());
        printModel(model);
        printRequest(request);
        printSession(session);
        return "sessionsattributepage";
    }
}

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 + :

  1. git clone git://github.com/arey/spring-mvc-toolkit.git ;
  2. git checkout SessionAttributes ;
  3. mvn clean install ;
  4. cd spring-mvc-toolkit-demo ;
  5. mvn jetty:run-war ;
  6. 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.

VII. Appel à dosomething

Traces observées lors de l'appel à http://localhost:8080/dosomething :

Logs
Sélectionnez
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 :

Image non disponible

Analyse :

  • les quatre 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 quatre beans sont présents au niveau de la requête. Par contre, seuls les trois beans référencés par l'annotation @SessionAttributes(value="myBean1", types={MyOtherBean.class}) sont présents en session.

VIII. Premier appel à other

Traces observées lors du clic sur le lien « /other » :

Logs
Sélectionnez
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 :

Image non disponible

Analyse :

  • lors de l'appel à la méthode @RequestMapping :

    • les deux 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és 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 quatre beans.

IX. Appel à endsession

Trace observée lors du clic sur le lien « /endession » :

Logs
Sélectionnez
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 :

Image non disponible

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 quatre méthodes annotées avec @ModelAttribute est appelée : addMyBean2ToRequestScope. Les trois autres méthodes ne sont pas appelées, car les beans qu'elles créent sont déjà présents 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 ».

X. Second appel à other

Image non disponible

Injecté dans le handler, le bean myBean1 n'est plus disponible en session.

 
Sélectionnez
public String otherHandlingMethod(Model model, HttpServletRequest request, HttpSession session, @ModelAttribute("myBean1") MyBean myBean) {

XI. Tests unitaires

La mise au point de tests unitaires ou de tests d'intégration mettant en jeu un 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 deux appels de handler, Spring Test ne conserve pas les données sauvegardées en session. Lors du 2appel, il est donc 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 montre un exemple d'utilisation :

Classe de test
Sélectionnez
public class SessionAttributesTest {
 
    private MockMvc mockMvc;
 
    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new MyController(), new OtherController()).build();
    }
 
    @Test
    public void reusingSessionBean() throws Exception {
        MvcResult result = mockMvc.perform(get("/dosomething"))
               .andDo(print())
               .andExpect(model().attributeExists("myBean1"))
               .andReturn();
        MyBean myBean1 = (MyBean) result.getModelAndView().getModel().get("myBean1");
        mockMvc.perform(get("/other")
               .sessionAttr("myBean1", myBean1))            
               .andDo(print())
               .andExpect(model().attributeExists("myBean3"));
        
    }
}

XII. 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.

XIII. Références

XIV. Remerciements

Cet article a été publié avec l'aimable autorisation d'Antoine Rey.

Nous tenons à remercier Malick SECK pour sa relecture orthographique attentive de cet article et Régis Pouiller pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2014 Antoine Rey. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.