Attributs de session dans Spring MVC

1. Vue d'ensemble

Lors du développement d'applications Web, nous avons souvent besoin de faire référence aux mêmes attributs dans plusieurs vues. Par exemple, nous pouvons avoir des contenus de panier qui doivent être affichés sur plusieurs pages.

Un bon emplacement pour stocker ces attributs est dans la session de l'utilisateur.

Dans ce didacticiel, nous allons nous concentrer sur un exemple simple et examiner 2 stratégies différentes pour travailler avec un attribut de session :

  • Utilisation d'un proxy étendu
  • Utilisation de l' annotation @ SessionAttributes

2. Configuration de Maven

Nous utiliserons les démarreurs Spring Boot pour amorcer notre projet et introduire toutes les dépendances nécessaires.

Notre configuration nécessite une déclaration parent, un démarreur Web et un démarreur à feuilles de thym.

Nous inclurons également le démarreur de test de ressort pour fournir une utilité supplémentaire dans nos tests unitaires:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE     org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-thymeleaf   org.springframework.boot spring-boot-starter-test test  

Les versions les plus récentes de ces dépendances se trouvent sur Maven Central.

3. Exemple de cas d'utilisation

Notre exemple implémentera une application «TODO» simple. Nous aurons un formulaire pour créer des instances de TodoItem et une vue de liste qui affiche tous les TodoItem .

Si nous créons un TodoItem à l'aide du formulaire, les accès ultérieurs au formulaire seront préremplis avec les valeurs du TodoItem le plus récemment ajouté . Nous allons utiliser t sa fonction pour montrer comment les valeurs de forme « rappelez - vous » qui sont stockés dans la portée de la session.

Nos 2 classes de modèles sont implémentées sous forme de POJO simples:

public class TodoItem { private String description; private LocalDateTime createDate; // getters and setters }
public class TodoList extends ArrayDeque{ }

Notre classe TodoList étend ArrayDeque pour nous donner un accès pratique à l'élément le plus récemment ajouté via la méthode peekLast .

Nous aurons besoin de 2 classes de contrôleurs: 1 pour chacune des stratégies que nous examinerons. Ils auront des différences subtiles, mais la fonctionnalité de base sera représentée dans les deux. Chacun aura 3 @RequestMapping s:

  • @GetMapping («/ form») - Cette méthode sera responsable de l'initialisation du formulaire et du rendu de la vue du formulaire. La méthode préremplir la forme avec le plus récemment ajouté TodoItem si le todolist n'est pas vide.
  • @PostMapping («/ form») - Cette méthode sera responsable de l'ajout du TodoItem soumisà la TodoList et de la redirection vers l'URL de la liste.
  • @GetMapping ("/ todos.html") - Cette méthode ajoutera simplement la TodoList au modèle pour l'affichage et restituera la vue de liste.

4. Utilisation d'un proxy de portée

4.1. Installer

Dans cette configuration, notre TodoList est configuré comme un @Bean à portée de session qui est sauvegardé par un proxy. Le fait que @Bean soit un proxy signifie que nous pouvons l'injecter dans notre @Controller à portée singleton .

Puisqu'il n'y a pas de session lorsque le contexte s'initialise, Spring créera un proxy de TodoList à injecter en tant que dépendance. L'instance cible de TodoList sera instanciée selon les besoins lorsque requis par les requêtes.

Pour une discussion plus approfondie des portées bean au printemps, reportez-vous à notre article sur le sujet.

Tout d'abord, nous définissons notre bean dans une classe @Configuration :

@Bean @Scope( value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) public TodoList todos() { return new TodoList(); }

Ensuite, nous déclarons le bean comme une dépendance pour le @Controller et l'injectons comme nous le ferions pour toute autre dépendance:

@Controller @RequestMapping("/scopedproxy") public class TodoControllerWithScopedProxy { private TodoList todos; // constructor and request mappings } 

Enfin, utiliser le bean dans une requête implique simplement d'appeler ses méthodes:

@GetMapping("/form") public String showForm(Model model) { if (!todos.isEmpty()) { model.addAttribute("todo", todos.peekLast()); } else { model.addAttribute("todo", new TodoItem()); } return "scopedproxyform"; }

4.2. Test unitaire

Afin de tester notre implémentation à l'aide du proxy scoped, nous configurons d'abord un SimpleThreadScope . Cela garantira que nos tests unitaires simulent avec précision les conditions d'exécution du code que nous testons.

Tout d'abord, nous définissons un TestConfig et un CustomScopeConfigurer :

@Configuration public class TestConfig { @Bean public CustomScopeConfigurer customScopeConfigurer() { CustomScopeConfigurer configurer = new CustomScopeConfigurer(); configurer.addScope("session", new SimpleThreadScope()); return configurer; } }

Nous pouvons maintenant commencer par tester qu'une requête initiale du formulaire contient un TodoItem non initialisé:

@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @Import(TestConfig.class) public class TodoControllerWithScopedProxyIntegrationTest { // ... @Test public void whenFirstRequest_thenContainsUnintializedTodo() throws Exception { MvcResult result = mockMvc.perform(get("/scopedproxy/form")) .andExpect(status().isOk()) .andExpect(model().attributeExists("todo")) .andReturn(); TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo"); assertTrue(StringUtils.isEmpty(item.getDescription())); } } 

Nous pouvons également confirmer que notre envoi émet une redirection et qu'une demande de formulaire ultérieure est pré-remplie avec le TodoItem nouvellement ajouté :

@Test public void whenSubmit_thenSubsequentFormRequestContainsMostRecentTodo() throws Exception { mockMvc.perform(post("/scopedproxy/form") .param("description", "newtodo")) .andExpect(status().is3xxRedirection()) .andReturn(); MvcResult result = mockMvc.perform(get("/scopedproxy/form")) .andExpect(status().isOk()) .andExpect(model().attributeExists("todo")) .andReturn(); TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo"); assertEquals("newtodo", item.getDescription()); }

4.3. Discussion

Une caractéristique clé de l'utilisation de la stratégie de proxy à portée est qu'elle n'a aucun impact sur les signatures de méthode de mappage de demande. Cela maintient la lisibilité à un niveau très élevé par rapport à la stratégie @SessionAttributes .

Il peut être utile de rappeler que les contrôleurs ont une portée singleton par défaut.

C'est la raison pour laquelle nous devons utiliser un proxy au lieu de simplement injecter un bean à portée de session non proxy. Nous ne pouvons pas injecter un bean avec une portée moindre dans un bean avec une plus grande portée.

Tenter de le faire, dans ce cas, déclencherait une exception avec un message contenant: L' étendue 'session' n'est pas active pour le thread actuel .

If we're willing to define our controller with session scope, we could avoid specifying a proxyMode. This can have disadvantages, especially if the controller is expensive to create because a controller instance would have to be created for each user session.

Note that TodoList is available to other components for injection. This may be a benefit or a disadvantage depending on the use case. If making the bean available to the entire application is problematic, the instance can be scoped to the controller instead using @SessionAttributes as we'll see in the next example.

5. Using the @SessionAttributes Annotation

5.1. Setup

In this setup, we don't define TodoList as a Spring-managed @Bean. Instead, we declare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller.

The first time our controller is accessed, Spring will instantiate an instance and place it in the Model. Since we also declare the bean in @SessionAttributes, Spring will store the instance.

For a more in-depth discussion of @ModelAttribute in Spring, refer to our article on the topic.

First, we declare our bean by providing a method on the controller and we annotate the method with @ModelAttribute:

@ModelAttribute("todos") public TodoList todos() { return new TodoList(); } 

Next, we inform the controller to treat our TodoList as session-scoped by using @SessionAttributes:

@Controller @RequestMapping("/sessionattributes") @SessionAttributes("todos") public class TodoControllerWithSessionAttributes { // ... other methods }

Finally, to use the bean within a request, we provide a reference to it in the method signature of a @RequestMapping:

@GetMapping("/form") public String showForm( Model model, @ModelAttribute("todos") TodoList todos) { if (!todos.isEmpty()) { model.addAttribute("todo", todos.peekLast()); } else { model.addAttribute("todo", new TodoItem()); } return "sessionattributesform"; } 

In the @PostMapping method, we inject RedirectAttributes and call addFlashAttribute before returning our RedirectView. This is an important difference in implementation compared to our first example:

@PostMapping("/form") public RedirectView create( @ModelAttribute TodoItem todo, @ModelAttribute("todos") TodoList todos, RedirectAttributes attributes) { todo.setCreateDate(LocalDateTime.now()); todos.add(todo); attributes.addFlashAttribute("todos", todos); return new RedirectView("/sessionattributes/todos.html"); }

Spring uses a specialized RedirectAttributes implementation of Model for redirect scenarios to support the encoding of URL parameters. During a redirect, any attributes stored on the Model would normally only be available to the framework if they were included in the URL.

By using addFlashAttribute we are telling the framework that we want our TodoList to survive the redirect without needing to encode it in the URL.

5.2. Unit Testing

The unit testing of the form view controller method is identical to the test we looked at in our first example. The test of the @PostMapping, however, is a little different because we need to access the flash attributes in order to verify the behavior:

@Test public void whenTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo() throws Exception { FlashMap flashMap = mockMvc.perform(post("/sessionattributes/form") .param("description", "newtodo")) .andExpect(status().is3xxRedirection()) .andReturn().getFlashMap(); MvcResult result = mockMvc.perform(get("/sessionattributes/form") .sessionAttrs(flashMap)) .andExpect(status().isOk()) .andExpect(model().attributeExists("todo")) .andReturn(); TodoItem item = (TodoItem) result.getModelAndView().getModel().get("todo"); assertEquals("newtodo", item.getDescription()); }

5.3. Discussion

The @ModelAttribute and @SessionAttributes strategy for storing an attribute in the session is a straightforward solution that requires no additional context configuration or Spring-managed @Beans.

Unlike our first example, it's necessary to inject TodoList in the @RequestMapping methods.

In addition, we must make use of flash attributes for redirect scenarios.

6. Conclusion

Dans cet article, nous avons examiné l'utilisation de proxies étendus et de @SessionAttributes comme 2 stratégies pour travailler avec les attributs de session dans Spring MVC. Notez que dans cet exemple simple, tous les attributs stockés dans la session ne survivront que pendant la durée de vie de la session.

Si nous devions conserver les attributs entre les redémarrages du serveur ou les délais d'expiration de session, nous pourrions envisager d'utiliser Spring Session pour gérer de manière transparente l'enregistrement des informations. Consultez notre article sur la session de printemps pour plus d'informations.

Comme toujours, tout le code utilisé dans cet article est disponible sur GitHub.