Attributi di sessione in Spring MVC

1. Panoramica

Quando si sviluppano applicazioni Web, spesso è necessario fare riferimento agli stessi attributi in più visualizzazioni. Ad esempio, potremmo avere i contenuti del carrello degli acquisti che devono essere visualizzati su più pagine.

Una buona posizione per memorizzare questi attributi è nella sessione dell'utente.

In questo tutorial, ci concentreremo su un semplice esempio ed esamineremo 2 diverse strategie per lavorare con un attributo di sessione :

  • Utilizzo di un proxy con ambito
  • Utilizzando l'@ SessionAttributes annotazione

2. Installazione di Maven

Useremo gli starter Spring Boot per avviare il nostro progetto e portare tutte le dipendenze necessarie.

La nostra configurazione richiede una dichiarazione del genitore, un web starter e un thymeleaf starter.

Includeremo anche lo starter del test di primavera per fornire qualche utilità aggiuntiva nei nostri test unitari:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE     org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-thymeleaf   org.springframework.boot spring-boot-starter-test test  

Le versioni più recenti di queste dipendenze possono essere trovate su Maven Central.

3. Caso d'uso di esempio

Il nostro esempio implementerà una semplice applicazione "TODO". Avremo un modulo per creare istanze di TodoItem e una visualizzazione elenco che mostra tutti i TodoItem .

Se creiamo un TodoItem utilizzando il form, i successivi accessi al form verranno prepopolati con i valori del TodoItem aggiunto più di recente . Useremo t la sua funzione per dimostrare come “ricordare” i valori della forma che sono memorizzati in ambito sessione.

Le nostre 2 classi di modelli sono implementate come semplici POJO:

public class TodoItem { private String description; private LocalDateTime createDate; // getters and setters }
public class TodoList extends ArrayDeque{ }

La nostra classe TodoList estende ArrayDeque per darci un comodo accesso all'elemento aggiunto più di recente tramite il metodo peekLast .

Avremo bisogno di 2 classi di controller: 1 per ciascuna delle strategie che esamineremo. Avranno sottili differenze ma la funzionalità principale sarà rappresentata in entrambi. Ciascuno avrà 3 @RequestMapping :

  • @GetMapping ("/ form") - Questo metodo sarà responsabile dell'inizializzazione del modulo e del rendering della vista del modulo. Il metodo prepopolerà il modulo con il TodoItem aggiunto più di recentese TodoList non è vuoto.
  • @PostMapping ("/ form") - Questo metodo sarà responsabile dell'aggiunta del TodoItem inviatoalla TodoList e del reindirizzamento all'URL dell'elenco.
  • @GetMapping ("/ todos.html") - Questo metodo aggiungerà semplicemente la TodoList al modello per la visualizzazione e il rendering della visualizzazione elenco.

4. Utilizzo di un proxy con ambito

4.1. Impostare

In questa configurazione, il nostro ToDoList è configurato come una sessione con ambito @Bean che è sostenuta da un proxy. Il fatto che @Bean sia un proxy significa che siamo in grado di iniettarlo nel nostro @Controller con ambito singleton .

Poiché non c'è sessione quando il contesto viene inizializzato, Spring creerà un proxy di TodoList da iniettare come dipendenza. L'istanza di destinazione di TodoList verrà istanziata secondo necessità quando richiesto dalle richieste.

Per una discussione più approfondita sui bean scopes in primavera, fare riferimento al nostro articolo sull'argomento.

Innanzitutto, definiamo il nostro bean all'interno di una classe @Configuration :

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

Successivamente, dichiariamo il bean come dipendenza per @Controller e lo iniettiamo proprio come faremmo con qualsiasi altra dipendenza:

@Controller @RequestMapping("/scopedproxy") public class TodoControllerWithScopedProxy { private TodoList todos; // constructor and request mappings } 

Infine, l'utilizzo del bean in una richiesta implica semplicemente la chiamata dei suoi metodi:

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

Per testare la nostra implementazione utilizzando il proxy con ambito, configuriamo prima un SimpleThreadScope . Ciò garantirà che i nostri unit test simulino accuratamente le condizioni di runtime del codice che stiamo testando.

Innanzitutto, definiamo un TestConfig e un CustomScopeConfigurer :

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

Ora possiamo iniziare testando che una richiesta iniziale del modulo contiene un TodoItem non inizializzato:

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

Possiamo anche confermare che il nostro submit emette un reindirizzamento e che una successiva richiesta di modulo è precompilata con il TodoItem appena aggiunto :

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

Una caratteristica fondamentale dell'utilizzo della strategia proxy con ambito è che non ha alcun impatto sulle firme del metodo di mappatura delle richieste. Ciò mantiene la leggibilità a un livello molto alto rispetto alla strategia @SessionAttributes .

Può essere utile ricordare che i controller hanno l' ambito singleton per impostazione predefinita.

Questo è il motivo per cui dobbiamo usare un proxy invece di iniettare semplicemente un bean con ambito di sessione non proxy. Non possiamo iniettare un bean con uno scope minore in un bean con uno scope maggiore.

Tentare di farlo, in questo caso, attiverebbe un'eccezione con un messaggio contenente: L' ambito 'sessione' non è attivo per il thread corrente .

Se siamo disposti a definire il nostro controller con l'ambito della sessione, potremmo evitare di specificare un proxyMode . Ciò può avere degli svantaggi, soprattutto se il controller è costoso da creare perché è necessario creare un'istanza del controller per ogni sessione utente.

Nota che TodoList è disponibile per altri componenti per l'iniezione. Questo può essere un vantaggio o uno svantaggio a seconda del caso d'uso. Se rendere il bean disponibile per l'intera applicazione è problematico, l'istanza può essere definita come ambito per il controller invece utilizzando @SessionAttributes come vedremo nel prossimo esempio.

5. Utilizzando la @SessionAttributes Annotazione

5.1. Impostare

In questa configurazione, noi non definiamo ToDoList come una molla gestiti @Bean . Invece, lo dichiariamo come @ModelAttribute e specifichiamo l' annotazione @SessionAttributes per renderlo ambito alla sessione per il controller .

La prima volta che si accede al nostro controller, Spring creerà un'istanza e la inserirà nel modello . Dato che dichiariamo anche il bean in @SessionAttributes , Spring memorizzerà l'istanza.

Per una discussione più approfondita su @ModelAttribute in primavera, fare riferimento al nostro articolo sull'argomento.

Innanzitutto, dichiariamo il nostro bean fornendo un metodo sul controller e annotiamo il metodo con @ModelAttribute :

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

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

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

@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("/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.

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.

5.2. Unit Testing

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:

@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

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.

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

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

6. Conclusion

In questo articolo, abbiamo esaminato l'utilizzo di proxy con ambito e @SessionAttributes come 2 strategie per lavorare con gli attributi di sessione in Spring MVC. Si noti che in questo semplice esempio, qualsiasi attributo memorizzato nella sessione sopravviverà solo per la durata della sessione.

Se avessimo bisogno di rendere persistenti gli attributi tra i riavvii del server o i timeout della sessione, potremmo prendere in considerazione l'utilizzo di Spring Session per gestire in modo trasparente il salvataggio delle informazioni. Dai un'occhiata al nostro articolo sulla sessione primaverile per ulteriori informazioni.

Come sempre, tutto il codice utilizzato in questo articolo è disponibile su GitHub.