Session Attributes in Spring MVC – Spring MVC中的会话属性

最后修改: 2018年 3月 16日

中文/混合/英文(键盘快捷键:t)

 

1. Overview

1.概述

When developing web applications, we often need to refer to the same attributes in several views. For example, we may have shopping cart contents that need to be displayed on multiple pages.

在开发Web应用程序时,我们经常需要在几个视图中引用相同的属性。例如,我们可能有需要在多个页面显示的购物车内容。

A good location to store those attributes is in the user’s session.

存储这些属性的一个好位置是在用户的会话中。

In this tutorial, we’ll focus on a simple example and examine 2 different strategies for working with a session attribute:

在本教程中,我们将专注于一个简单的例子,并检查使用会话属性的2种不同策略

  • Using a scoped proxy
  • Using the @SessionAttributes annotation

2. Maven Setup

2.Maven的设置

We’ll use Spring Boot starters to bootstrap our project and bring in all necessary dependencies.

我们将使用Spring Boot启动器来引导我们的项目并引入所有必要的依赖。

Our setup requires a parent declaration, web starter, and thymeleaf starter.

我们的设置需要一个父本声明、网络启动器和百里香叶启动器。

We’ll also include the spring test starter to provide some additional utility in our unit tests:

我们还将包括spring test starter,以在我们的单元测试中提供一些额外的效用。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
    <relativePath/>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
     </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

The most recent versions of these dependencies can be found on Maven Central.

这些依赖的最新版本可以在Maven中心找到

3. Example Use Case

3.用例

Our example will implement a simple “TODO” application. We’ll have a form for creating instances of TodoItem and a list view that displays all TodoItems.

我们的例子将实现一个简单的 “TODO “应用程序。我们将有一个用于创建TodoItem实例的表单和一个显示所有TodoItem的列表视图。

If we create a TodoItem using the form, subsequent accesses of the form will be prepopulated with the values of the most recently added TodoItem. We’ll use this feature to demonstrate how to “remember” form values that are stored in session scope.

如果我们使用表单创建了一个TodoItem,那么随后对该表单的访问就会预先填充最近添加的TodoItem的值。我们将使用这一功能来演示如何 “记住 “存储在会话范围中的表单值。

Our 2 model classes are implemented as simple POJOs:

我们的两个模型类被实现为简单的POJO。

public class TodoItem {

    private String description;
    private LocalDateTime createDate;

    // getters and setters
}
public class TodoList extends ArrayDeque<TodoItem>{

}

Our TodoList class extends ArrayDeque to give us convenient access to the most recently added item via the peekLast method.

我们的TodoList类扩展了ArrayDeque,通过peekLast方法让我们方便地访问最近添加的项目。

We’ll need 2 controller classes: 1 for each of the strategies we’ll look at. They’ll have subtle differences but the core functionality will be represented in both. Each will have 3 @RequestMappings:

我们将需要2个控制器类。我们需要2个控制器类:每个控制器类用于我们要研究的策略。它们会有细微的差别,但核心功能都会体现在两个类中。每个类将有3个@RequestMappings。

  • @GetMapping(“/form”) – This method will be responsible for initializing the form and rendering the form view. The method will prepopulate the form with the most recently added TodoItem if the TodoList is not empty.
  • @PostMapping(“/form”) – This method will be responsible for adding the submitted TodoItem to the TodoList and redirecting to the list URL.
  • @GetMapping(“/todos.html”) – This method will simply add the TodoList to the Model for display and render the list view.

4. Using a Scoped Proxy

4.使用一个范围内的代理

4.1. Setup

4.1.设置

In this setup, our TodoList is configured as a session-scoped @Bean that is backed by a proxy. The fact that the @Bean is a proxy means that we are able to inject it into our singleton-scoped @Controller.

在这个设置中,我们的TodoList被配置为一个会话域的@Bean,并由一个代理支持。@Bean是一个代理,这意味着我们可以将其注入到我们的单子域的@Controller中。

Since there is no session when the context initializes, Spring will create a proxy of TodoList to inject as a dependency. The target instance of TodoList will be instantiated as needed when required by requests.

由于上下文初始化时没有会话,Spring将创建一个TodoList的代理,作为依赖注入。TodoList的目标实例将在请求需要时被实例化。

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

有关 Spring 中 Bean 作用域的更深入讨论,请参考我们关于该主题的文章

First, we define our bean within a @Configuration class:

首先,我们在一个@Configuration类中定义我们的Bean。

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

Next, we declare the bean as a dependency for the @Controller and inject it just as we would any other dependency:

接下来,我们将Bean声明为@Controller的依赖项,并像其他依赖项一样将其注入。

@Controller
@RequestMapping("/scopedproxy")
public class TodoControllerWithScopedProxy {

    private TodoList todos;

    // constructor and request mappings
}

Finally, using the bean in a request simply involves calling its methods:

最后,在请求中使用Bean,只需要调用它的方法。

@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

4.2.单元测试

In order to test our implementation using the scoped proxy, we first configure a SimpleThreadScope. This will ensure that our unit tests accurately simulate runtime conditions of the code we are testing.

为了测试我们使用范围代理的实现,我们首先配置了一个SimpleThreadScope这将确保我们的单元测试能够准确地模拟我们正在测试的代码的运行时间条件。

First, we define a TestConfig and a CustomScopeConfigurer:

首先,我们定义一个TestConfig和一个CustomScopeConfigurer

@Configuration
public class TestConfig {

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

Now we can start by testing that an initial request of the form contains an uninitialized TodoItem:

现在我们可以开始测试表单的初始请求是否包含一个未初始化的TodoItem:

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

We can also confirm that our submit issues a redirect and that a subsequent form request is prepopulated with the newly added TodoItem:

我们还可以确认,我们的提交发出了一个重定向,并且随后的表单请求被预先填上了新添加的TodoItem

@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

4.3.讨论

A key feature of using the scoped proxy strategy is that it has no impact on request mapping method signatures. This keeps readability on a very high level compared to the @SessionAttributes strategy.

使用范围代理策略的一个关键特征是,它对请求映射方法签名没有影响。@SessionAttributes策略相比,这使可读性保持在一个非常高的水平。

It can be helpful to recall that controllers have singleton scope by default.

回顾一下控制器默认有singleton范围是有帮助的。

This is the reason why we must use a proxy instead of simply injecting a non-proxied session-scoped bean. We can’t inject a bean with a lesser scope into a bean with greater scope.

这就是为什么我们必须使用代理,而不是简单地注入一个非代理的会话范围的Bean的原因。我们不能将范围较小的Bean注入范围较大的Bean中。

Attempting to do so, in this case, would trigger an exception with a message containing: Scope ‘session’ is not active for the current thread.

在这种情况下,试图这样做会触发一个异常,其中包含一个消息。Scope ‘session’对当前线程不活跃

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.

如果我们愿意在会话范围内定义我们的控制器,我们可以避免指定一个proxyMode。这可能有弊端,特别是如果控制器的创建成本很高,因为必须为每个用户会话创建一个控制器实例。

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.

请注意,TodoList可以被其他组件注入。这可能是一个好处,也可能是一个坏处,这取决于用例。如果让Bean对整个应用程序可用是有问题的,那么可以使用@SessionAttributes将实例范围扩大到控制器,我们将在下一个例子中看到。

5. Using the @SessionAttributes Annotation

5.使用@SessionAttributes注释

5.1. Setup

5.1.设置

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.

在这个设置中,我们没有将TodoList定义为Spring管理的@Bean。相反,我们将其声明为@ModelAttribute,并指定@SessionAttributes注解,将其范围扩大到控制器的会话

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.

第一次访问我们的控制器时,Spring将实例化并将其放在Model中。由于我们也在@SessionAttributes中声明了bean,Spring将存储该实例。

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

有关Spring中@ModelAttribute的更深入讨论,请参阅我们的专题文章

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

首先,我们通过在控制器上提供一个方法来声明我们的Bean,我们用@ModelAttribute来注释这个方法。

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

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

接下来,我们通过使用@SessionAttributes通知控制器将我们的TodoList视为会话范围。

@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:

最后,为了在请求中使用Bean,我们在@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方法中,我们注入RedirectAttributes并在返回RedirectView之前调用addFlashAttribute。与我们的第一个例子相比,这是实现上的一个重要区别。

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

Spring为重定向场景使用了专门的RedirectAttributes实现Model,以支持URL参数的编码。在重定向过程中,存储在Model上的任何属性通常只有在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.

通过使用addFlashAttribute,我们告诉框架,我们希望我们的TodoList能够在重定向中存活,而不需要在URL中进行编码。

5.2. Unit Testing

5.2.单元测试

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:

表单视图控制器方法的单元测试与我们在第一个例子中看到的测试相同。然而,对@PostMapping的测试有些不同,因为我们需要访问flash属性,以验证行为。

@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

5.3.讨论

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.

在会话中存储属性的@ModelAttribute@SessionAttributes策略是一种直接的解决方案,不需要额外的上下文配置或Spring管理的@Beans

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

与我们的第一个例子不同,有必要在@RequestMapping methods中注入TodoList

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

此外,我们必须利用flash属性来实现重定向的场景。

6. Conclusion

6.结论

In this article, we looked at using scoped proxies and @SessionAttributes as 2 strategies for working with session attributes in Spring MVC. Note that in this simple example, any attributes stored in session will only survive for the life of the session.

在这篇文章中,我们研究了使用范围代理和@SessionAttributes作为Spring MVC中处理会话属性的两种策略。请注意,在这个简单的例子中,存储在会话中的任何属性都只在会话的有效期内存在。

If we needed to persist attributes between server restarts or session timeouts, we could consider using Spring Session to transparently handle saving the information. Have a look at our article on Spring Session for more information.

如果我们需要在服务器重启或会话超时之间持续保存属性,我们可以考虑使用Spring Session来透明地处理保存信息。请看我们关于Spring Session的文章以了解更多信息。

As always, all code used in this article’s available over on GitHub.

一如既往,本文中使用的所有代码都可以在GitHub上找到