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 :
@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 :
@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 :
- 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. À 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 :
request.setAttribute
(
modelName, modelValue);
Lorsque le mode debug est activé, la trace suivante est générée 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 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 :
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 :
@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.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 :
@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 :
@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 + :
- 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.
VII. 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 :
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 » :
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 :
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 » :
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 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▲
Injecté dans le handler, le bean myBean1 n'est plus disponible en session.
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 2e appel, 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 :
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.