Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

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

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

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

  • Использование прокси с заданной областью
  • Использование аннотации @ SessionAttributes

2. Настройка Мавена

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

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

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

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</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>

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

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

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

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

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

public class TodoItem {

private String description;
private LocalDateTime createDate;

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

}

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

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

  • @GetMapping("/form") — этот метод будет отвечать за инициализацию формы и рендеринг представления формы. Метод предварительно заполнит форму последним добавленным элементом TodoItem , если TodoList не пуст.
  • @PostMapping («/ form») — этот метод будет отвечать за добавление отправленного TodoItem в TodoList и перенаправление на URL-адрес списка.
  • @GetMapping("/todos.html") — этот метод просто добавит TodoList в модель для отображения и рендеринга представления списка.

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

4.1. Настраивать

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

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

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

Во- первых, мы определяем наш компонент в классе @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
}

Наконец, использование 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. Модульное тестирование

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

Во- первых, мы определяем TestConfig и CustomScopeConfigurer :

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

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

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

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

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

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

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

5.1. Настраивать

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

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

Более подробное обсуждение @ModelAttribute в Spring можно найти в нашей статье на эту тему .

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

Используя addFlashAttribute , мы сообщаем платформе, что хотим, чтобы наш TodoList пережил перенаправление без необходимости кодировать его в URL-адресе.

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

Модульное тестирование метода контроллера представления формы идентично тесту, который мы рассмотрели в нашем первом примере. Однако тест @PostMapping немного отличается, потому что нам нужно получить доступ к атрибутам флэш-памяти, чтобы проверить поведение:

@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 для хранения атрибута в сеансе — это простое решение, не требующее дополнительной настройки контекста или управляемых Spring @Bean s .

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

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

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

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

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

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