Рубрики
Без рубрики

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

Изучите различные способы хранения атрибутов в сеансе с Spring MVC.

Автор оригинала: Chris Oberle.

1. Обзор

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

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

В этом уроке мы сосредоточимся на простом примере и рассмотрим 2 различные стратегии работы с атрибутом сеанса :

  • Использование прокси-сервера с областью действия
  • Используя @ Атрибуты сеанса аннотация

2. Настройка Maven

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

Для нашей настройки требуется родительская декларация, веб-стартер и стартер thymeleaf.

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


    org.springframework.boot
    spring-boot-starter-parent
    2.4.0
    

 

    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
     
    
        org.springframework.boot
        spring-boot-starter-test
        test
    

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

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

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

Если мы создадим TodoItem с помощью формы, последующие обращения к форме будут предварительно заполнены значениями последнего добавленного TodoItem . Мы будем использовать t его функцию, чтобы продемонстрировать, как “запоминать” значения формы , которые хранятся в области сеанса.

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

public class TodoItem {

    private String description;
    private LocalDateTime createDate;

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

}

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

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

  • @GetMapping(“/форма”) – Этот метод будет отвечать за инициализацию формы и отображение представления формы. Метод заполнит форму последним добавленным TodoItem , если ToDoList не пуст.
  • @PostMapping(“/форма”) – Этот метод будет отвечать за добавление отправленного TodoItem в ToDoList и перенаправление на URL-адрес списка.
  • @GetMapping(“/todos.html”) – Этот метод просто добавит ToDoList в Модель для отображения и отображения списка.

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

4.1. Настройка

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

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

Для более подробного обсуждения областей применения бобов весной обратитесь к нашей статье на эту тему .

Во-первых, мы определяем наш компонент в классе @Configuration :

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

Затем мы объявляем компонент как зависимость для @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. Модульное тестирование

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

Сначала мы определяем Тестовую конфигурацию и Пользовательский конфигуратор :

@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. Обсуждение

Ключевой особенностью использования стратегии прокси-сервера с областью действия является то, что она не влияет на сигнатуры метода сопоставления запросов. Это обеспечивает читаемость на очень высоком уровне по сравнению со стратегией @sessionAttributes .

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

Именно по этой причине мы должны использовать прокси-сервер вместо того, чтобы просто вводить компонент, не являющийся прокси-сервером, с областью действия сеанса. Мы не можем ввести компонент с меньшей областью действия в компонент с большей областью действия.

Попытка сделать это в этом случае вызовет исключение с сообщением, содержащим: Область “сеанс” не активна для текущего потока .

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

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

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

5.1. Настройка

В этой настройке мы не определяем Список дел как управляемый пружиной @Bean . Вместо этого мы объявляем его как @ModelAttribute и указываем аннотацию @sessionAttributes , чтобы включить его в сеанс для контроллера .

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

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

Во-первых, мы объявляем наш компонент, предоставляя метод на контроллере, и мы аннотируем метод с помощью @ModelAttribute :

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

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

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

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

Используя addFlashAttribute , мы сообщаем платформе, что хотим, чтобы наш ToDoList выдержал перенаправление без необходимости кодировать его в 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 для хранения атрибута в сеансе является простым решением, которое не требует дополнительной конфигурации контекста или управляемых пружиной @Bean s .

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

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

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

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

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

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