Атрибуты сеанса в Spring MVC

Атрибуты сеанса в Spring MVC

1. обзор

При разработке веб-приложений нам часто приходится ссылаться на одни и те же атрибуты в нескольких представлениях. Например, у нас может быть содержимое корзины покупок, которое должно отображаться на нескольких страницах.

Хорошее место для хранения этих атрибутов - сеанс пользователя.

В этом руководстве мы сосредоточимся на простом примере иexamine 2 different strategies for working with a session attribute:

  • Использование прокси с ограниченной областью видимости

  • Использование аннотации @SessionAttributes

2. Maven Setup

Мы будем использовать стартеры Spring Boot для начальной загрузки нашего проекта и внесения всех необходимых зависимостей.

Наша установка требует родительского объявления, веб-стартера и тимелист-стартера.

Мы также включим стартер весенних тестов, чтобы обеспечить дополнительную полезность в наших модульных тестах:


    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
    

Самые последние версии этих зависимостей можно найти вon Maven Central.

3. Пример использования

В нашем примере будет реализовано простое приложение «TODO». У нас будет форма для создания экземпляровTodoItem и представление списка, в котором отображаются всеTodoItems.

Если мы создадимTodoItem с помощью формы, последующие обращения к форме будут предварительно заполнены значениями последнего добавленногоTodoItem. We’ll use this feature to demonstrate how to “remember” form values, которые хранятся в области сеанса.

Наши 2 модельных класса реализованы как простые POJO:

public class TodoItem {

    private String description;
    private LocalDateTime createDate;

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

}

Наш классTodoList расширяетArrayDeque, чтобы предоставить нам удобный доступ к последнему добавленному элементу с помощью методаpeekLast.

Нам понадобится 2 класса контроллеров: по одному для каждой стратегии, которую мы рассмотрим. У них будут небольшие различия, но основные функции будут представлены в обоих. У каждого будет 3@RequestMappingсс:

  • @GetMapping(“/form”) - этот метод будет отвечать за инициализацию формы и визуализацию представления формы. Метод предварительно заполнит форму самым последним добавленнымTodoItem, еслиTodoList не пуст.

  • @PostMapping(“/form”) - этот метод будет отвечать за добавление отправленногоTodoItem вTodoList и перенаправление на URL-адрес списка.

  • @GetMapping(“/todos.html”) – Этот метод просто добавитTodoList кModel для отображения и визуализации представления списка.

4. Использование прокси с заданной областью

4.1. Настроить

В этой настройке нашTodoList настроен как@Bean в области сеанса, поддерживаемый прокси. Тот факт, что@Bean является прокси-сервером, означает, что мы можем внедрить его в нашу единичную область видимости@Controller.

Поскольку при инициализации контекста сеанса нет, Spring создаст проксиTodoList для внедрения в качестве зависимости. Целевой экземплярTodoList будет создан по мере необходимости, когда этого требуют запросы.

Для более подробного обсуждения областей действия bean-компонентов в Spring обратитесь к нашемуarticle on the topic.

Сначала мы определяем наш bean-компонент в классе@Configuration:

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

Затем мы объявляем bean-компонент как зависимость для@Controller и вводим его так же, как и любую другую зависимость:

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

    private TodoList todos;

    // constructor and request mappings
}

Наконец, использование компонента в запросе просто включает вызов его методов:

@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. Модульное тестирование

Чтобы протестировать нашу реализацию с использованием прокси с ограниченной областью действия,we first configure a SimpleThreadScope.. Это гарантирует, что наши модульные тесты точно имитируют условия выполнения кода, который мы тестируем.

Сначала мы определяемTestConfig и aCustomScopeConfigurer:

@Configuration
public class TestConfig {

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

Теперь мы можем начать с проверки того, что первоначальный запрос формы содержит неинициализированный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()));
    }
}

Мы также можем подтвердить, что наша отправка вызывает перенаправление и что последующий запрос формы предварительно заполняется только что добавленным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. обсуждение

Ключевой особенностью использования стратегии прокси с ограниченной областью видимости является то, чтоit has no impact on request mapping method signatures. сохраняет удобочитаемость на очень высоком уровне по сравнению со стратегией@SessionAttributes.

Может быть полезно вспомнить, что контроллеры по умолчанию имеют область действияsingleton.

Это причина, по которой мы должны использовать прокси, а не просто вводить незаксируемый сессионный компонент. We can’t inject a bean with a lesser scope into a bean with greater scope.с

Попытка сделать это в этом случае вызовет исключение с сообщением, содержащим:Scope ‘session' is not active for the current thread.

Если мы хотим определить наш контроллер с областью сеанса, мы могли бы избежать указанияproxyMode. Это может иметь недостатки, особенно если создание контроллера дорого, поскольку для каждого сеанса пользователя необходимо создавать экземпляр контроллера.

Обратите внимание, чтоTodoList доступен для инъекций другим компонентам. Это может быть преимуществом или недостатком в зависимости от варианта использования. Если сделать bean-компонент доступным для всего приложения проблематично, вместо этого экземпляр можно ограничить контроллером с помощью@SessionAttributes, как мы увидим в следующем примере.

5. Использование аннотации@SessionAttributes

5.1. Настроить

В этой настройке мы не определяемTodoList как@Bean, управляемый Spring. Вместо этого мыdeclare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller.

При первом обращении к нашему контроллеру Spring создаст экземпляр и поместит его вModel. Поскольку мы также объявляем компонент в@SessionAttributes, Spring сохранит экземпляр.

Для более подробного обсуждения@ModelAttribute в Spring обратитесь к нашемуarticle on the topic.

Сначала мы объявляем наш bean-компонент, предоставляя метод в контроллере, и аннотируем метод@ModelAttribute:

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

Затем мы сообщаем контроллеру, что он должен рассматривать нашTodoList как сессионный, используя@SessionAttributes:

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

Наконец, чтобы использовать 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";
}

В методе@PostMapping мы вводимRedirectAttributes и вызываемaddFlashAttribute перед тем, как вернуть нашRedirectView. Это важное отличие в реализации по сравнению с нашим первым примером:

@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 использует специализированную реализациюRedirectAttributesModel для сценариев перенаправления для поддержки кодирования параметров URL. Во время перенаправления любые атрибуты, хранящиеся вModel, обычно будут доступны для инфраструктуры, только если они были включены в URL-адрес.

By using addFlashAttribute we are telling the framework that we want our TodoList to survive the redirect без необходимости кодировать его в URL.

5.2. Модульное тестирование

Модульное тестирование метода контроллера вида формы идентично тесту, который мы рассмотрели в нашем первом примере. Однако проверка@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. обсуждение

Стратегия@ModelAttribute и@SessionAttributes для сохранения атрибута в сеансе является простым решением, чемrequires no additional context configuration or Spring-managed @Beans.

В отличие от нашего первого примера, необходимо ввестиTodoList в методы@RequestMapping.

Кроме того, мы должны использовать атрибуты flash для сценариев перенаправления.

6. Заключение

В этой статье мы рассмотрели использование прокси с областью видимости и@SessionAttributes как 2 стратегии для работы с атрибутами сеанса в Spring MVC. Обратите внимание, что в этом простом примере любые атрибуты, сохраненные в сеансе, будут существовать только в течение всего сеанса.

Если нам нужно было сохранить атрибуты между перезапусками сервера или тайм-аутом сеанса, мы могли бы рассмотреть возможность использования Spring Session для прозрачной обработки сохранения информации. Взгляните наour article в Spring Session для получения дополнительной информации.

Как всегда, весь код, используемый в этой статье, доступенover on GitHub.