Attributs de session dans Spring MVC

Attributs de session dans Spring MVC

1. Vue d'ensemble

Lors du développement d'applications Web, il est souvent nécessaire 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 lesexamine 2 different strategies for working with a session attribute:

  • Utilisation d'un proxy étendu

  • Utilisation de l'annotation @SessionAttributes

2. Maven Setup

Nous utiliserons les démarreurs Spring Boot pour démarrer notre projet et intégrer toutes les dépendances nécessaires.

Notre configuration nécessite une déclaration parent, un démarreur Web et un démarreur thymeleaf.

Nous allons également inclure 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.0.0.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 peuvent être trouvéeson Maven Central.

3. Exemple de cas d'utilisation

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

Si nous créons unTodoItem en utilisant le formulaire, les accès suivants du formulaire seront préremplis avec les valeurs desTodoItem les plus récemment ajoutés. We’ll use this feature to demonstrate how to “remember” form values qui sont stockés dans l'étendue de la session.

Nos 2 classes de modèles sont implémentées en tant que POJO simples:

public class TodoItem {

    private String description;
    private LocalDateTime createDate;

    // getters and setters
}
public class TodoList extends ArrayDeque{

}

Notre classeTodoList étendArrayDeque pour nous donner un accès pratique à l'élément le plus récemment ajouté via la méthodepeekLast.

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

  • @GetMapping(“/form”) - Cette méthode sera responsable de l'initialisation du formulaire et du rendu de la vue du formulaire. La méthode préremplira le formulaire avec lesTodoItem les plus récemment ajoutés si leTodoList n'est pas vide.

  • @PostMapping(“/form”) - Cette méthode sera responsable de l'ajout desTodoItem soumis auxTodoList et de la redirection vers l'URL de la liste.

  • @GetMapping(“/todos.html”) – Cette méthode ajoutera simplement lesTodoList auxModel pour l'affichage et le rendu de la vue de liste.

4. Utilisation d'un proxy étendu

4.1. Installer

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

Puisqu'il n'y a pas de session lorsque le contexte s'initialise, Spring créera un proxy deTodoList à injecter en tant que dépendance. L'instance cible deTodoList 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 à nosarticle on the topic.

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 les@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 haricot 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. Tests unitaires

Afin de tester notre implémentation en utilisant le proxy de portée,we first configure a 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 unTestConfig et unCustomScopeConfigurer:

@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 unTodoItem: 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 lesTodoItem nouvellement ajoutés:

@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 de portée est queit has no impact on request mapping method signatures. Ceci 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 la portéesingleton par défaut.

C’est la raison pour laquelle nous devons utiliser un proxy au lieu d’injecter simplement un haricot de session non soumis à proxy. We can’t inject a bean with a lesser scope into a bean with greater scope.

Tenter de le faire, dans ce cas, déclencherait une exception avec un message contenant:Scope ‘session' is not active for the current thread.

Si nous voulons définir notre contrôleur avec une portée de session, nous pourrions éviter de spécifier unproxyMode. Cela peut avoir des inconvénients, en particulier si le contrôleur est coûteux à créer car il faudrait créer une instance de contrôleur pour chaque session utilisateur.

Notez queTodoList est disponible pour d'autres composants pour l'injection. Cela peut constituer un avantage ou un inconvénient selon le cas d'utilisation. Si rendre le bean disponible pour l'ensemble de l'application pose problème, l'instance peut être étendue au contrôleur à la place en utilisant@SessionAttributes comme nous le verrons dans l'exemple suivant.

5. Utilisation de l'annotation@SessionAttributes

5.1. Installer

Dans cette configuration, nous ne définissons pasTodoList comme un@Bean géré par Spring. Au lieu de cela, nousdeclare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller.

Lors du premier accès à notre contrôleur, Spring instanciera une instance et la placera dans lesModel. Puisque nous déclarons également le bean en@SessionAttributes, Spring stockera l'instance.

Pour une discussion plus approfondie de@ModelAttribute au printemps, reportez-vous à nosarticle on the topic.

Tout d'abord, nous déclarons notre bean en fournissant une méthode sur le contrôleur et nous annotons la méthode avec@ModelAttribute:

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

Ensuite, nous informons le contrôleur de traiter nosTodoList comme étant à portée de session en utilisant@SessionAttributes:

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

Enfin, pour utiliser le bean dans une requête, nous lui fournissons une référence dans la signature de méthode d'un@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";
}

Dans la méthode@PostMapping, nous injectonsRedirectAttributes et appelonsaddFlashAttribute avant de renvoyer nosRedirectView. C'est une différence importante dans la mise en œuvre par rapport à notre premier exemple:

@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 utilise une implémentationRedirectAttributes spécialisée deModel pour les scénarios de redirection afin de prendre en charge le codage des paramètres d'URL. Lors d'une redirection, tous les attributs stockés sur lesModel ne seraient normalement disponibles pour le framework que s'ils étaient inclus dans l'URL.

By using addFlashAttribute we are telling the framework that we want our TodoList to survive the redirect sans avoir besoin de l'encoder dans l'URL.

5.2. Tests unitaires

Le test unitaire de la méthode du contrôleur de vue de formulaire est identique au test que nous avons examiné dans notre premier exemple. Le test des@PostMapping, cependant, est un peu différent car nous devons accéder aux attributs flash afin de vérifier le comportement:

@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

La stratégie@ModelAttribute et@SessionAttributes pour stocker un attribut dans la session est une solution simple querequires no additional context configuration or Spring-managed @Beans.

Contrairement à notre premier exemple, il est nécessaire d'injecterTodoList dans les méthodes@RequestMapping.

De plus, nous devons utiliser les attributs flash pour les scénarios de redirection.

6. Conclusion

Dans cet article, nous avons examiné l'utilisation de proxys é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 survivent que pendant toute la durée de la session.

S'il était nécessaire de conserver les attributs entre les redémarrages du serveur ou les délais d'attente de session, nous pourrions envisager d'utiliser Spring Session pour gérer de manière transparente l'enregistrement des informations. Jetez un œil àour article sur Spring Session pour plus d'informations.

Comme toujours, tout le code utilisé dans lesover on GitHub disponibles de cet article.