Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Implémenter le pattern Session Object avec Spring MVC

HTTP est un protocole déconnecté : un client se connecte à un serveur, lui transmet une requête, attend une réponse, et se déconnecte. Bonjour, au revoir, et on recommence à zéro.

Pour servir des documents sur un réseau, c'est parfait ; mais pour interagir avec une application web professionnelle, c'est un peu limité. L'utilisateur apprécie qu'on se rappelle de lui tout au long de sa session de travail, qui peut nécessiter de nombreux cycles requête/réponse.
Pour conserver ce contexte applicatif entre les requêtes, le serveur propose donc la notion de Session. Chaque utilisateur se voit ainsi attribuer un petit coffre-fort personnel, dans lequel les données qui le concernent sont stockées entre les requêtes.

Le problème

En Java, la session est représentée par la classe HttpSession, qui s'utilise à la manière d'une Map : chaque valeur est associée à une clé (souvent une String), qui permet de récupérer ladite valeur par la suite. HttpSession possède donc la simplicité d'utilisation d'une Map, mais également ses inconvénients.

Tout d'abord, il est nécessaire de se rappeler les clés utilisées lors de l'enregistrement des données. Afin de limiter les risques d'erreurs et de simplifier le refactoring, il est d'usage de les déclarer comme des constantes, mais rien n'interdit un accès direct, avec les risques de typos habituels.
Ensuite, comme il est possible de stocker des données de types hétérogènes, toutes les valeurs sont considérées comme de simples Object, et doivent être re-castées dans leur type original après récupération. Si le risque d'erreur est faible (et se repère rapidement lors des tests), cette gymnastique imposée est pénible à l'usage.

public void doGet(HttpServletRequest request, HttpServletResponse response) 
  throws ServletException, IOException  {
 
    // Une String, un cast, deux erreurs possibles !
    User user = (User) request.getSession().getAttribute("user"); 
 
 }

Le pattern Session Object

Le framework Apache Wicket propose une solution élégante à ce problème. Plutôt que de manipuler directement la session HTTP, Wicket permet de définir un "Session Object" (un simple POJO) qui, lui, sera placé en session. L'application interagit alors uniquement avec cet objet.

Les avantages sont nombreux. Tout d'abord, le code est découplé de l'API Servlet. Ensuite, les champs du "Session Object" sont clairement nommés et fortement typés. Enfin, rien n'empêche d'ajouter du code de validation dans les setters, ou des méthodes utilitaires !

public class SessionObject {
 
    // Donnée nommée et typée
    private User user;
 
    private User getUser() {
        return user;
    }
 
    private void setUser(User user) {
        this.user = user;
    }
 
    // Méthode utilitaire
    public boolean isConnected() {
        return user != null; 
    }
 
}

Implémentation en Spring MVC

Avec un peu d'astuce, il est possible d'implémenter le pattern "Session Object" avec Spring MVC.

Les bases

Tout d'abord, modélisons la structure de la session utilisateur sous la forme d'une classe SessionBean ; un simple champ "message" de type String suffira. Et pour prouver que chaque utilisateur possède sa propre instance de SessionBean, ajoutons un champ "id" immuable, généré aléatoirement à l'instanciation.

@Component
public class SessionBean {
 
    private final String id = UUID.randomUUID().toString();
    private String message;
 
    // +getters/setters
 
}

Ce bean peut ensuite être injecté dans un contrôleur.

@Controller
public class MyController {
 
    @Autowired
    private SessionBean sessionBean;
 
	@RequestMapping(value="/")
	public String myController() {
 
	    // Manipulation du SessionBean
        sessionBean.setMessage("Time : " + now());
 
        return "myView";
	}
 
	private String now() {
	    return new SimpleDateFormat("HH:mm:ss").format(new Date());
	}
 
}

Le problème des scopes

Si vous faisiez fonctionner le code présenté plus haut, vous vous apercevriez que tous les utilisateurs partagent le même SessionBean, ce qui n'est évidemment pas le comportement attendu.

Par défaut, Spring applique le scope "singleton" aux composants qu'il gère, c'est-à-dire que ceux-ci ne sont instanciés qu'une seule fois (lors de leur première utilisation). Pour la plupart des composants, c'est un réglage tout à fait raisonnable ; mais pour notre SessionBean, c'est problématique.

Heuseusement, Spring propose un scope spécialisé, "session", qui prend soin d'associer une instance différente du composant à chaque session HTTP. Modifions donc notre SessionBean pour en tirer parti.

@Component
@Scope(value="session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionBean {
 
    private final String id = UUID.randomUUID().toString();
    private String message;
 
    // +getters/setters
 
}

Pour faire collaborer des composants aux cycles de vie différents (un contrôleur scopé "singleton", et un SessionBean scopé "session"), Spring est contraint à quelques contorsions techniques.
Tout d'abord, il génère un proxy (proxy Java ou CGLIB) chargé de maintenir la table de correspondance session HTTP / instance du bean, et d'en instancier de nouveaux si besoin. Puis il injecte ce proxy à la place de l'objet original dans les composants y faisant référence (ici, le contrôleur).

Grâce à cette astuce, chaque client possède bien son propre SessionBean - il est facile de le vérifier en affichant son id dans le contrôleur. La gestion des données de session est désormais simplissime : il suffit d'agir sur le bean injecté !

Interaction avec la session HTTP

Le problème de l'instanciation étant résolu, il ne reste plus qu'à gérer le stockage du SessionBean en session HTTP pour finaliser la mise en place du pattern "Session Object".

La solution naturelle consisterait à placer manuellement le bean dans la session HTTP en utilisant les API classiques.

@RequestMapping(value="/")
public String myController(HttpSession session) {
 
    sessionBean.setMessage("Time : " + now());
 
    // Mise en session HTTP
    session.setAttribute("sessionBean", sessionBean);
 
    return "myView";
}

En réalité, ce n'est pas nécessaire !
En effet, pour être capable de retrouver le bean associé à une session HTTP particulière, Spring place tout simplement le bean... dans la session HTTP. Simple et efficace !

La clé sous laquelle l'objet est stocké est construite selon le pattern "scopedTarget.<bean_id>"[1]. Dans notre exemple, cette clé serait donc "scopedTarget.sessionBean".

Utilisation dans les JSP

Passons maintenant côté vue, pour afficher le message véhiculé par le SessionBean.

Un peu de JSP-EL, et le tour est joué :

<!DOCTYPE HTML>
<html>
<body>
    Session bean message : ${sessionScope['scopedTarget.sessionBean'].message}
    Session bean UUID :    ${sessionScope['scopedTarget.sessionBean'].id}
</body>
</html>

La clé "scopedTarget.sessionBean" contenant un point, il est impossible d'utiliser la notation habituelle ${scopedTarget.sessionBean.message}, ni même ${sessionScope.scopedTarget.sessionBean.message} ; il est nécessaire de recourir à la notation avec des crochets.

L'expression finale est insatisfaisante[2] car elle est complexe, et qu'elle expose les mécanismes internes de Spring (préfixe "scopedTarget."). Peut-on la simplifier un peu ?

Le problème vient évidemment du préfixe. On ne peut malheureusement pas le modifier (c'est une constante de Spring) ni le configurer. Mais on peut contourner le problème, en enregistrant le même bean sous une clé de session plus simple !

Le code ci-dessous présente un listener de session qui scrute l'activité de la session HTTP, et intercepte toutes les opérations (création/modification/suppression) sur les attributs associés à des clés préfixées par "scopedTarget." pour les rejouer sur des clés dépréfixées.

public class SpringSessionScopedBeanDeprefixer implements HttpSessionAttributeListener {
 
    private static final String PREFIX = "scopedTarget.";
 
    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        String name = event.getName();
        if (name.startsWith(PREFIX)) {
            HttpSession session = event.getSession();
            Object value = event.getValue();
 
            // Aliasing : scopedTarget.foo -> foo
            session.setAttribute(name.substring(PREFIX.length()), value);
        }
    }
 
    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        String name = event.getName();
        if (name.startsWith(PREFIX)) {
            HttpSession session = event.getSession();
            session.removeAttribute(name.substring(PREFIX.length()));
        }
    }
 
    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        String name = event.getName();
        if (name.startsWith(PREFIX)) {
            HttpSession session = event.getSession();
            Object value = session.getAttribute(name);
            session.setAttribute(name.substring(PREFIX.length()), value);
        }
    }
 
}

Ce listener se déclare dans le descripteur de déploiement web.xml.

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
 
    <listener>
        <listener-class>net.thecodersbreakfast.sessionbean.presentation.SpringSessionScopedBeanDeprefixer</listener-class>
    </listener>
 
    (...)
 
</web-app>

Grâce à cette astuce, le SessionBean est désormais accessible sous la clé "sessionBean", nettement plus agréable à utiliser dans les JSP.

<!DOCTYPE HTML>
<html>
<body>
    Session bean message : ${sessionBean.message} 
    Session bean UUID :    ${sessionBean.id}
</body>
</html>

Conclusion

La pattern "Session Object" présente des avantages en termes de simplicité, de maintenabilité et de documentation. Les opérations ne sont plus réalisées sur la session HTTP, mais sur un objet intermédiaire permettant de nommer et typer précisément les données manipulées.

Spring MVC autorise cette approche au niveau des contrôleurs, avec une sytaxe simple et une configuration minimale ; et il est possible d'étendre cette facilité d'utilisation jusqu'aux JSP... en faisant preuve d'astuce !

Qu'en pensez-vous de cette architecture ?
L'avez-vous déjà mise en place ou aperçue sur un projet ?

Notes

[1] voir la classe org.springframework.aop.scope.ScopedProxyUtils pour plus de détails

[2] Pour rester poli.


Commentaires

1. Le lundi 5 janvier 2015, 19:15 par Charles Morin

Très bon tutoriel. J'ai appliqué la solution dans mon application et ça fonctionne à merveille. La seule différence de mon côté est que j'utilise la configuration par annotations, donc je n'ai pas définit mon listener dans web.xml, mais bien dans une classe AppInitializer implémentant l'interface WebApplicationInitializer. De cette façon, plus besoin de XML. Il faut avoir un conteneur avec servlet 3.0+. J'utilise JBoss 7.2.

..
AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext();
servletContext.addListener(new SpringSessionScopedBeanDeprefixer());
..

Merci!

2. Le lundi 5 janvier 2015, 20:16 par Olivier Croisier

Merci pour ce retour d'expérience, et pour la configuration alternative !

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.