Sitzungsattribute in Spring MVC

Sitzungsattribute in Spring MVC

1. Überblick

Bei der Entwicklung von Webanwendungen müssen wir häufig in mehreren Ansichten auf dieselben Attribute verweisen. Zum Beispiel 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 undexamine 2 different strategies for working with a session attribute:

  • 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 Web-Starter 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.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
    

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

3. Beispiel für einen Anwendungsfall

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

Wenn wir mit dem Formular einTodoItem erstellen, werden nachfolgende Zugriffe auf das Formular mit den Werten der zuletzt hinzugefügtenTodoItem vorab ausgefüllt. We’ll use this feature to demonstrate how to “remember” form values, die im Sitzungsbereich gespeichert sind.

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{

}

UnsereTodoList-Klasse erweitertArrayDeque, um uns einen bequemen Zugriff auf das zuletzt hinzugefügte Element über diepeekLast-Methode 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@RequestMappings:

  • @GetMapping(“/form”) - Diese Methode ist für die Initialisierung des Formulars und das Rendern der Formularansicht verantwortlich. Die Methode füllt das Formular mit den zuletzt hinzugefügtenTodoItem vor, wennTodoList nicht leer ist.

  • @PostMapping(“/form”) - Diese Methode ist dafür verantwortlich, die übermitteltenTodoItem zuTodoList hinzuzufügen und zur Listen-URL umzuleiten.

  • @GetMapping(“/todos.html”) – Bei dieser Methode werden einfachTodoList zuModel hinzugefügt, um sie anzuzeigen und die Listenansicht zu rendern.

4. Verwenden eines Proxys mit Gültigkeitsbereich

4.1. Konfiguration

In diesem Setup ist unserTodoList als@Bean mit Sitzungsbereich konfiguriert, das von einem Proxy unterstützt wird. Die Tatsache, dass@Bean ein Proxy ist, bedeutet, dass wir es in unsere@Controller mit Singleton-Gültigkeitsbereich injizieren können.

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

Weitere Informationen zu Bean Scopes im Frühjahr finden Sie in unserenarticle on the topic.

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 die@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 nur die 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, stellen Sie sicher, dass unsere Komponententests die Laufzeitbedingungen des zu testenden Codes genau simulieren, umwe first configure a SimpleThreadScope. zu testen.

Zuerst definieren wir einTestConfig und einCustomScopeConfigurer:

@Configuration
public class TestConfig {

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

Jetzt können wir zunächst testen, ob eine erste Anforderung des Formulars nicht initialisierteTodoItem: 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 den neu hinzugefügtenTodoItem 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, dassit has no impact on request mapping method signatures. die Lesbarkeit im Vergleich zur Strategie von@SessionAttributesauf einem sehr hohen Niveau hält.

Es kann hilfreich sein, sich daran zu erinnern, dass Controller standardmäßig den Gültigkeitsbereich vonsingletonhaben.

Dies ist der Grund, warum wir einen Proxy verwenden müssen, anstatt einfach eine nicht-Proxy-Bean mit Sitzungsumfang zu injizieren. We can’t inject a bean with a lesser scope into a bean with greater scope.

Der Versuch, dies zu tun, würde in diesem Fall eine Ausnahme mit einer Nachricht auslösen, die Folgendes enthält:Scope ‘session' is not active for the current thread.

Wenn wir bereit sind, unseren Controller mit Sitzungsumfang zu definieren, können wir die Angabe vonproxyMode vermeiden. Dies kann Nachteile haben, insbesondere wenn die Erstellung des Controllers teuer ist, da für jede Benutzersitzung eine Controller-Instanz erstellt werden müsste.

Beachten Sie, dassTodoList anderen Komponenten zur Injektion zur Verfügung steht. Dies kann je nach Anwendungsfall ein Vorteil oder ein Nachteil sein. Wenn es problematisch ist, die Bean für die gesamte Anwendung verfügbar zu machen, kann die Instanz stattdessen mit@SessionAttributes auf den Controller übertragen werden, wie im nächsten Beispiel gezeigt wird.

5. Verwenden der Annotation@SessionAttributes

5.1. Konfiguration

In diesem Setup definieren wirTodoList nicht als Spring-verwaltete@Bean. Stattdessen haben wirdeclare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller.

Beim ersten Zugriff auf unseren Controller instanziiert Spring eine Instanz und platziert sie inModel. Da wir die Bean auch in@SessionAttributes deklarieren, speichert Spring die Instanz.

Eine ausführlichere Beschreibung von@ModelAttribute im Frühjahr finden Sie in unserenarticle on the topic.

Zuerst deklarieren wir unsere Bean, indem wir eine Methode auf dem Controller bereitstellen, und kommentieren die Methode mit@ModelAttribute:

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

Als nächstes weisen wir den Controller an, unsereTodoList mit@SessionAttributes als sitzungsbezogen zu behandeln:

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

Um die Bean innerhalb einer Anfrage zu verwenden, geben wir schließlich einen Verweis darauf in der Methodensignatur von@RequestMapping an:

@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";
}

Bei der@PostMapping-Methode injizieren wirRedirectAttributes und rufenaddFlashAttribute auf, bevor wir unsereRedirectView zurückgeben. Dies ist ein wichtiger Unterschied in der Implementierung im Vergleich zu unserem ersten Beispiel:

@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 verwendet eine spezielleRedirectAttributes-Implementierung vonModel für Umleitungsszenarien, um die Codierung von URL-Parametern zu unterstützen. Während einer Umleitung sind alle aufModel gespeicherten Attribute normalerweise nur für das Framework verfügbar, wenn sie in der URL enthalten sind.

By using addFlashAttribute we are telling the framework that we want our TodoList to survive the redirect, ohne dass es in der URL codiert werden muss.

5.2. Unit Testing

Der Komponententest der Form View Controller-Methode ist identisch mit dem Test, den wir in unserem ersten Beispiel angesehen haben. Der Test der@PostMapping ist jedoch etwas anders, da wir auf die Flash-Attribute zugreifen müssen, um das Verhalten zu überprüfen:

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

Die Strategie@ModelAttribute und@SessionAttributes zum Speichern eines Attributs in der Sitzung ist eine einfache Lösung fürrequires no additional context configuration or Spring-managed @Beans.

Im Gegensatz zu unserem ersten Beispiel ist es erforderlich,TodoList in die@RequestMapping-Methode. zu injizieren

Darüber hinaus müssen Flash-Attribute für Umleitungsszenarien verwendet werden.

6. Fazit

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 gültig sind.

Wenn Attribute zwischen Serverneustarts oder Sitzungszeitlimits beibehalten werden müssen, können Sie Spring Session verwenden, um das Speichern der Informationen transparent zu handhaben. Weitere Informationen finden Sie unterour article in der Frühjahrssitzung.

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