Atributos da sessão no Spring MVC
1. Visão geral
Ao desenvolver aplicativos da Web, geralmente precisamos nos referir aos mesmos atributos em várias visualizações. Por exemplo, podemos ter o conteúdo do carrinho de compras que precisa ser exibido em várias páginas.
Um bom local para armazenar esses atributos é na sessão do usuário.
Neste tutorial, vamos nos concentrar em um exemplo simples eexamine 2 different strategies for working with a session attribute:
-
Usando um proxy com escopo
-
Usando a anotação @SessionAttributes
2. Configuração do Maven
Usaremos iniciadores Spring Boot para inicializar nosso projeto e trazer todas as dependências necessárias.
Nossa configuração requer uma declaração pai, um iniciador da Web e um iniciador do timeleaf.
Também incluiremos o iniciador de teste de primavera para fornecer alguma utilidade adicional em nossos testes de unidade:
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
As versões mais recentes dessas dependências podem ser encontradason Maven Central.
3. Exemplo de caso de uso
Nosso exemplo implementará um aplicativo "TODO" simples. Teremos um formulário para a criação de instâncias deTodoIteme uma exibição de lista que exibe todos osTodoItems.
Se criarmos umTodoItem usando o formulário, os acessos subsequentes do formulário serão pré-preenchidos com os valores dosTodoItem adicionados mais recentemente. We’ll use this feature to demonstrate how to “remember” form values que estão armazenados no escopo da sessão.
Nossas 2 classes de modelo são implementadas como POJOs simples:
public class TodoItem {
private String description;
private LocalDateTime createDate;
// getters and setters
}
public class TodoList extends ArrayDeque{
}
Nossa classeTodoList estendeArrayDeque para nos dar acesso conveniente ao item adicionado mais recentemente por meio do métodopeekLast.
Precisaremos de 2 classes de controlador: 1 para cada uma das estratégias que examinaremos. Eles terão diferenças sutis, mas a funcionalidade principal será representada em ambos. Cada um terá 3@RequestMappings:
-
@GetMapping(“/form”) - Este método será responsável por inicializar o formulário e renderizar a visualização do formulário. O método irá pré-preencher o formulário com osTodoItem adicionados mais recentemente seTodoList não estiver vazio.
-
@PostMapping(“/form”) - Este método será responsável por adicionar oTodoItem enviado aoTodoListe redirecionar para a URL da lista.
-
@GetMapping(“/todos.html”) – Este método simplesmente adicionaráTodoList aModel para exibir e renderizar a exibição de lista.
4. Usando um proxy com escopo
4.1. Configuração
Nesta configuração, nossoTodoList é configurado como um@Bean com escopo de sessão que é apoiado por um proxy. O fato de@Bean ser um proxy significa que podemos injetá-lo em nosso@Controller com escopo único.
Como não há sessão quando o contexto é inicializado, o Spring criará um proxy deTodoList para injetar como uma dependência. A instância de destino deTodoList será instanciada conforme necessário quando exigido pelas solicitações.
Para uma discussão mais aprofundada sobre escopos de bean no Spring, consulte nossoarticle on the topic.
Primeiro, definimos nosso bean dentro de uma classe@Configuration:
@Bean
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public TodoList todos() {
return new TodoList();
}
Em seguida, declaramos o bean como uma dependência para@Controllere o injetamos como faríamos com qualquer outra dependência:
@Controller
@RequestMapping("/scopedproxy")
public class TodoControllerWithScopedProxy {
private TodoList todos;
// constructor and request mappings
}
Por fim, usar o bean em uma solicitação simplesmente envolve chamar seus métodos:
@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. Teste de Unidade
Para testar nossa implementação usando o proxy com escopo definido,we first configure a SimpleThreadScope. Isso garantirá que nossos testes de unidade simulem com precisão as condições de tempo de execução do código que estamos testando.
Primeiro, definimos umTestConfig e umCustomScopeConfigurer:
@Configuration
public class TestConfig {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("session", new SimpleThreadScope());
return configurer;
}
}
Agora podemos começar testando se uma solicitação inicial do formulário contém umTodoItem: não inicializado
@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()));
}
}
Também podemos confirmar que nosso envio emite um redirecionamento e que uma solicitação de formulário subsequente é pré-preenchida com oTodoItem recém-adicionado:
@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. Discussão
Um recurso importante do uso da estratégia de proxy com escopo definido é queit has no impact on request mapping method signatures. mantém a legibilidade em um nível muito alto em comparação com a estratégia@SessionAttributes.
Pode ser útil lembrar que os controladores têm escoposingleton por padrão.
Essa é a razão pela qual devemos usar um proxy em vez de simplesmente injetar um bean com escopo de sessão sem proxy. We can’t inject a bean with a lesser scope into a bean with greater scope.
A tentativa de fazer isso, neste caso, acionaria uma exceção com uma mensagem contendo:Scope ‘session' is not active for the current thread.
Se estivermos dispostos a definir nosso controlador com escopo de sessão, podemos evitar a especificação deproxyMode. Isso pode ter desvantagens, especialmente se o controlador for caro para criar porque uma instância do controlador precisaria ser criada para cada sessão do usuário.
Observe queTodoList está disponível para outros componentes para injeção. Isso pode ser um benefício ou uma desvantagem, dependendo do caso de uso. Se tornar o bean disponível para todo o aplicativo é problemático, a instância pode ser definida para o controlador em vez de usar@SessionAttributes, como veremos no próximo exemplo.
5. Usando a anotação@SessionAttributes
5.1. Configuração
Nesta configuração, não definimosTodoList como um@Bean gerenciado por Spring. Em vez disso, nósdeclare it as a @ModelAttribute and specify the @SessionAttributes annotation to scope it to the session for the controller.
Na primeira vez que nosso controlador é acessado, o Spring irá instanciar uma instância e colocá-la emModel. Como também declaramos o bean em@SessionAttributes, o Spring armazenará a instância.
Para uma discussão mais aprofundada de@ModelAttribute no Spring, consulte nossoarticle on the topic.
Primeiro, declaramos nosso bean fornecendo um método no controlador e anotamos o método com@ModelAttribute:
@ModelAttribute("todos")
public TodoList todos() {
return new TodoList();
}
Em seguida, informamos o controlador para tratar nossoTodoList como escopo de sessão usando@SessionAttributes:
@Controller
@RequestMapping("/sessionattributes")
@SessionAttributes("todos")
public class TodoControllerWithSessionAttributes {
// ... other methods
}
Finalmente, para usar o bean em uma solicitação, fornecemos uma referência a ele na assinatura do método de@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";
}
No método@PostMapping, injetamosRedirectAttributese chamamosaddFlashAttribute antes de retornar nossoRedirectView. Essa é uma diferença importante na implementação em comparação com o nosso primeiro exemplo:
@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 usa uma implementação especializadaRedirectAttributes deModel para cenários de redirecionamento para suportar a codificação de parâmetros de URL. Durante um redirecionamento, quaisquer atributos armazenados emModel normalmente só estariam disponíveis para a estrutura se fossem incluídos na URL.
By using addFlashAttribute we are telling the framework that we want our TodoList to survive the redirect sem a necessidade de codificá-lo na URL.
5.2. Teste de Unidade
O teste de unidade do método do controlador de exibição de formulário é idêntico ao teste que examinamos em nosso primeiro exemplo. O teste de@PostMapping, no entanto, é um pouco diferente porque precisamos acessar os atributos do flash para verificar o comportamento:
@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. Discussão
A estratégia@ModelAttributee@SessionAttributes para armazenar um atributo na sessão é uma solução direta querequires no additional context configuration or Spring-managed @Beans.
Ao contrário do nosso primeiro exemplo, é necessário injetarTodoList nos métodos@RequestMapping.
Além disso, devemos usar atributos de flash para cenários de redirecionamento.
6. Conclusão
Neste artigo, examinamos o uso de proxies com escopo e@SessionAttributes como 2 estratégias para trabalhar com atributos de sessão no Spring MVC. Observe que, neste exemplo simples, qualquer atributo armazenado na sessão sobreviverá apenas por toda a vida útil da sessão.
Se precisarmos persistir atributos entre as reinicializações do servidor ou o tempo limite da sessão, considere usar o Spring Session para lidar com o salvamento transparente das informações. Dê uma olhada emour article na Sessão Spring para mais informações.
Como sempre, todo o código usado neste artigo está disponívelover on GitHub.