Sitzungsattribute in Spring MVC

1. Übersicht

Bei der Entwicklung von Webanwendungen müssen wir häufig in mehreren Ansichten auf dieselben Attribute verweisen. Beispielsweise haben wir möglicherweise Warenkorbinhalte, die auf mehreren Seiten angezeigt werden müssen.

Ein guter Speicherort für diese Attribute ist die Sitzung des Benutzers.

In diesem Tutorial konzentrieren wir uns auf ein einfaches Beispiel und untersuchen zwei verschiedene Strategien für die Arbeit mit einem Sitzungsattribut :

  • Verwenden eines Proxys mit Gültigkeitsbereich
  • Verwenden der Annotation @ SessionAttributes

2. Maven Setup

Wir werden Spring Boot-Starter verwenden, um unser Projekt zu booten und alle erforderlichen Abhängigkeiten einzubeziehen.

Unser Setup erfordert eine übergeordnete Deklaration, einen Webstarter und einen Thymeleaf-Starter.

Wir werden auch den Federteststarter einbeziehen, um unseren Unit-Tests zusätzlichen Nutzen zu bieten:

 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  

Die neuesten Versionen dieser Abhängigkeiten finden Sie in Maven Central.

3. Anwendungsbeispiel

In unserem Beispiel wird eine einfache TODO-Anwendung implementiert. Wir haben ein Formular zum Erstellen von Instanzen von TodoItem und eine Listenansicht , in der alle TodoItems angezeigt werden .

Wenn wir mit dem Formular ein TodoItem erstellen , werden nachfolgende Zugriffe auf das Formular mit den Werten des zuletzt hinzugefügten TodoItem vorab ausgefüllt . Wir verwenden t seine Funktion zu demonstrieren , wie zu „erinnern“ Formularwerte , die in Sitzungsbereich gespeichert werden.

Unsere 2 Modellklassen sind als einfache POJOs implementiert:

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

Unsere TodoList- Klasse erweitert ArrayDeque , um uns über die peekLast- Methode einen bequemen Zugriff auf das zuletzt hinzugefügte Element zu ermöglichen .

Wir benötigen 2 Controller-Klassen: 1 für jede der Strategien, die wir betrachten werden. Sie werden subtile Unterschiede aufweisen, aber die Kernfunktionalität wird in beiden dargestellt. Jeder hat 3 @RequestMapping s:

  • @GetMapping ("/ form") - Diese Methode ist für die Initialisierung des Formulars und das Rendern der Formularansicht verantwortlich. Die Methode füllt das Formular mit dem zuletzt hinzugefügten TodoItem vor, wenn die TodoList nicht leer ist.
  • @PostMapping ("/ form") - Diese Methode ist dafür verantwortlich, das übermittelte TodoItem zur TodoList hinzuzufügen und zur Listen-URL umzuleiten.
  • @GetMapping ("/ todos.html") - Diese Methode fügtdem Modell einfach die TodoList zurAnzeige hinzu und rendert die Listenansicht .

4. Verwenden eines Proxys mit Gültigkeitsbereich

4.1. Konfiguration

In diesem Setup ist unsere TodoList als sitzungsbezogene @Bean konfiguriert , die von einem Proxy unterstützt wird. Die Tatsache, dass die @Bean ein Proxy ist, bedeutet, dass wir sie in unseren @Controller mit Singleton-Gültigkeitsbereich einfügen können .

Da beim Initialisieren des Kontexts keine Sitzung stattfindet, erstellt Spring einen Proxy von TodoList , der als Abhängigkeit eingefügt werden soll . Die Zielinstanz von TodoList wird nach Bedarf instanziiert, wenn dies für Anforderungen erforderlich ist.

Weitere Informationen zu Bean Scopes im Frühjahr finden Sie in unserem Artikel zum Thema.

Zuerst definieren wir unsere Bean innerhalb einer @ Configuration- Klasse:

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

Als Nächstes deklarieren wir die Bean als Abhängigkeit für den @Controller und injizieren sie wie jede andere Abhängigkeit:

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

Um die Bean in einer Anfrage zu verwenden, müssen lediglich ihre Methoden aufgerufen werden:

@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. Unit Testing

Um unsere Implementierung mithilfe des Proxys mit Gültigkeitsbereich zu testen, konfigurieren wir zunächst ein SimpleThreadScope . Dadurch wird sichergestellt, dass unsere Komponententests die Laufzeitbedingungen des zu testenden Codes genau simulieren.

Zunächst definieren wir eine TestConfig und einen CustomScopeConfigurer :

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

Jetzt können wir testen, ob eine erste Anforderung des Formulars ein nicht initialisiertes TodoItem enthält :

@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())); } } 

Wir können auch bestätigen, dass unsere Übermittlung eine Weiterleitung ausgibt und dass eine nachfolgende Formularanforderung mit dem neu hinzugefügten TodoItem vorab ausgefüllt wird :

@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. Diskussion

Ein wesentliches Merkmal der Verwendung der Proxy-Strategie mit Gültigkeitsbereich ist, dass sie keine Auswirkungen auf die Signaturen der Anforderungszuordnungsmethode hat. Dadurch bleibt die Lesbarkeit im Vergleich zur @ SessionAttributes- Strategie auf einem sehr hohen Niveau .

Es kann hilfreich sein, sich daran zu erinnern, dass Controller standardmäßig einen Singleton- Bereich haben.

Dies ist der Grund, warum wir einen Proxy verwenden müssen, anstatt einfach eine Bean ohne Sitzungsbereich ohne Proxy zu injizieren. Wir können eine Bohne mit geringerem Umfang nicht in eine Bohne mit größerem Umfang injizieren.

Wenn Sie dies versuchen, wird in diesem Fall eine Ausnahme mit der folgenden Meldung ausgelöst: Bereich 'Sitzung' ist für den aktuellen Thread nicht aktiv .

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

In diesem Artikel haben wir uns die Verwendung von Proxys mit Gültigkeitsbereich und @SessionAttributes als zwei Strategien für die Arbeit mit Sitzungsattributen in Spring MVC angesehen. Beachten Sie, dass in diesem einfachen Beispiel alle in der Sitzung gespeicherten Attribute nur für die Dauer der Sitzung überleben.

Wenn wir Attribute zwischen Serverneustarts oder Sitzungszeitlimits beibehalten müssen, können Sie Spring Session verwenden, um das Speichern der Informationen transparent zu gestalten. Weitere Informationen finden Sie in unserem Artikel zur Frühjahrssitzung.

Wie immer ist der gesamte in diesem Artikel verwendete Code auf GitHub verfügbar.