Sessiekenmerken in Spring MVC

1. Overzicht

Bij het ontwikkelen van webapplicaties moeten we vaak in verschillende weergaven naar dezelfde attributen verwijzen. We kunnen bijvoorbeeld de inhoud van een winkelwagentje hebben die op meerdere pagina's moet worden weergegeven.

Een goede locatie om die attributen op te slaan, is in de sessie van de gebruiker.

In deze tutorial zullen we ons concentreren op een eenvoudig voorbeeld en onderzoek 2 verschillende strategieën voor het werken met een sessieattribuut:

  • Met behulp van een scoped proxy
  • De ... gebruiken @SessionAttributes annotatie

2. Maven-instellingen

We zullen Spring Boot-starters gebruiken om ons project op te starten en alle noodzakelijke afhankelijkheden in te voeren.

Onze setup vereist een ouderverklaring, webstarter en thymeleaf-starter.

We zullen ook de springteststarter opnemen om wat extra hulpprogramma te bieden in onze unit-tests:

 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- startertest-test 

De meest recente versies van deze afhankelijkheden zijn te vinden op Maven Central.

3. Voorbeeld van gebruiksscenario

In ons voorbeeld wordt een eenvoudige "TODO" -toepassing geïmplementeerd. We hebben een formulier voor het maken van instanties van TodoItem en een lijstweergave die alles weergeeft TodoItems.

Als we een TodoItem door gebruik te maken van het formulier, worden volgende toegangen tot het formulier vooraf ingevuld met de waarden van de meest recent toegevoegde TodoItem. We zullen gebruiken tzijn functie om te demonstreren hoe vormwaarden kunnen worden "onthouden" die zijn opgeslagen in sessiebereik.

Onze 2 modelklassen zijn geïmplementeerd als eenvoudige POJO's:

openbare klasse TodoItem {privé Stringbeschrijving; privé LocalDateTime createDate; // getters en setters}
openbare klasse TodoList breidt ArrayDeque {} uit

Onze Te doen lijst klasse breidt zich uit ArrayDeque om ons gemakkelijk toegang te geven tot het meest recent toegevoegde item via de laatste methode.

We hebben 2 controllerklassen nodig: 1 voor elk van de strategieën die we zullen bekijken. Ze zullen subtiele verschillen hebben, maar de kernfunctionaliteit zal in beide vertegenwoordigd zijn. Elk heeft er 3 @RequestMappings:

  • @GetMapping ("/ form") - Deze methode is verantwoordelijk voor het initialiseren van het formulier en het weergeven van de formulierweergave. De methode vult het formulier van tevoren in met het meest recent toegevoegde TodoItem als het Te doen lijst is niet leeg.
  • @PostMapping ("/ form") - Deze methode is verantwoordelijk voor het toevoegen van de ingezonden TodoItem naar de Te doen lijst en omleiden naar de lijst-URL.
  • @GetMapping ("/ todos.html") - Deze methode voegt gewoon de Te doen lijst naar de Model voor het weergeven en renderen van de lijstweergave.

4. Met behulp van een scoped proxy

4.1. Opstelling

In deze opstelling zijn onze Te doen lijst is geconfigureerd als een sessiebereik @Boon dat wordt ondersteund door een proxy. Het feit dat de @Boon is een proxy betekent dat we het in onze singleton-scoped kunnen injecteren @Controller.

Aangezien er geen sessie is wanneer de context wordt geïnitialiseerd, zal Spring een proxy maken van Te doen lijst injecteren als een afhankelijkheid. Het doelexemplaar van Te doen lijst wordt indien nodig geïnstantieerd wanneer dit door verzoeken wordt verlangd.

Raadpleeg ons artikel over het onderwerp voor een meer diepgaande bespreking van bonenscopes in het voorjaar.

Ten eerste definiëren we onze boon binnen een @Configuratie klasse:

@Bean @Scope (waarde = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) openbare TodoList todos () {retourneer nieuwe TodoList (); }

Vervolgens declareren we de boon als een afhankelijkheid voor het @Controller en injecteer het net zoals we elke andere afhankelijkheid zouden injecteren:

@Controller @RequestMapping ("/ scopedproxy") openbare klasse TodoControllerWithScopedProxy {privé TodoList-taken; // constructor en aanvraagtoewijzingen} 

Ten slotte betekent het gebruik van de boon in een verzoek eenvoudigweg het aanroepen van de methoden:

@GetMapping ("/ form") public String showForm (Modelmodel) {if (! Todos.isEmpty ()) {model.addAttribute ("todo", todos.peekLast ()); } anders {model.addAttribute ("todo", nieuwe TodoItem ()); } retourneer "scopedproxyform"; }

4.2. Testen van een eenheid

Om onze implementatie te testen met behulp van de scoped proxy, we configureren eerst een SimpleThreadScope. Dit zorgt ervoor dat onze unit-tests nauwkeurig de runtime-omstandigheden simuleren van de code die we testen.

Eerst definiëren we een TestConfig en een CustomScopeConfigurer:

@Configuration openbare klasse TestConfig {@Bean openbare CustomScopeConfigurer customScopeConfigurer () {CustomScopeConfigurer configurer = nieuwe CustomScopeConfigurer (); configurer.addScope ("sessie", nieuwe SimpleThreadScope ()); terugkeer configurator; }}

Nu kunnen we beginnen door te testen of een eerste verzoek van het formulier een niet-geïnitialiseerd TodoItem:

@RunWith (SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @Import (TestConfig.class) openbare klasse TodoControllerWithScopedProxyIntegrationTest {// ... @Test openbare leegte whenFirstRequest_thenContainsUnintializedTotodo (Mocking / Resultaat) form ")) .andExpect (status (). isOk ()) .andExpect (model (). attributeExists (" todo ")) .andReturn (); TodoItem item = (TodoItem) result.getModelAndView (). GetModel (). Get ("todo"); assertTrue (StringUtils.isEmpty (item.getDescription ())); }} 

We kunnen ook bevestigen dat onze inzending een omleiding geeft en dat een volgend formulierverzoek vooraf is ingevuld met het nieuw toegevoegde TodoItem:

@Test openbare leegte whenSubmit_thenSubsequentFormRequestContainsMostRecentTodo () gooit uitzondering {mockMvc.perform (post ("/ scopedproxy / form") .param ("description", "newtodo")) .andExpect (status (). Is3xxRedirection ()). ; MvcResult resultaat = 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. Discussie

Een belangrijk kenmerk van het gebruik van de scoped proxy-strategie is dat het heeft geen invloed op de handtekeningen van de methode voor het toewijzen van verzoeken. Dit houdt de leesbaarheid op een zeer hoog niveau in vergelijking met de @SessionAttributes strategie.

Het kan handig zijn om te onthouden dat controllers hebben singleton scope standaard.

Dit is de reden waarom we een proxy moeten gebruiken in plaats van simpelweg een niet-proxy-session-scoped bean te injecteren. We kunnen een boon met een kleinere omvang niet injecteren in een boon met een grotere omvang.

Als u dit probeert, zou in dit geval een uitzondering worden geactiveerd met een bericht met daarin: Scope ‘sessie 'is niet actief voor de huidige thread.

Als we bereid zijn om onze controller met sessiebereik te definiëren, kunnen we vermijden om een proxyMode. Dit kan nadelen hebben, vooral als de controller duur is om te maken, omdat er voor elke gebruikerssessie een controllerinstantie moet worden gemaakt.

Let daar op Te doen lijst is beschikbaar voor andere componenten voor injectie. Dit kan een voordeel of een nadeel zijn, afhankelijk van het gebruik. Als het problematisch is om de bean beschikbaar te maken voor de hele applicatie, kan de instantie in plaats daarvan naar de controller worden gestuurd met behulp van @SessionAttributes zoals we zullen zien in het volgende voorbeeld.

5. Gebruik de @SessionAttributes Annotatie

5.1. Opstelling

In deze opstelling definiëren we niet Te doen lijst als een door de lente beheerd @Boon. In plaats daarvan, wij verklaar het als een @ModelAttribute en specificeer het @SessionAttributes annotatie om het in de sessie voor de controller te plaatsen.

De eerste keer dat onze controller wordt geopend, zal Spring een instantie starten en deze in het Model. Omdat we de boon ook aangeven in @SessionAttributes, Spring zal de instantie opslaan.

Voor een meer diepgaande bespreking van @ModelAttribute Raadpleeg in het voorjaar ons artikel over het onderwerp.

Eerst declareren we onze boon door een methode op de controller op te geven en we annoteren de methode met @ModelAttribute:

@ModelAttribute ("todos") openbare TodoList todos () {retourneer nieuwe TodoList (); } 

Vervolgens informeren we de controller om onze te behandelen Te doen lijst als sessiebereik door @SessionAttributes:

@Controller @RequestMapping ("/ sessionattributes") @SessionAttributes ("todos") openbare klasse TodoControllerWithSessionAttributes {// ... andere methoden}

Om de bean in een verzoek te gebruiken, geven we ten slotte een verwijzing ernaar in de methodehandtekening van een @RequestMapping:

@GetMapping ("/ form") public String showForm (Model model, @ModelAttribute ("todos") TodoList todos) {if (! Todos.isEmpty ()) {model.addAttribute ("todo", todos.peekLast ()) ; } anders {model.addAttribute ("todo", nieuwe TodoItem ()); } retourneer "sessionattributesform"; } 

In de @PostMapping methode, we injecteren RedirectAttributes en bel addFlashAttribute voordat u ons retourneert RedirectView. Dit is een belangrijk verschil in implementatie vergeleken met ons eerste voorbeeld:

@PostMapping ("/ form") openbare RedirectView create (@ModelAttribute TodoItem todo, @ModelAttribute ("todos") TodoList todos, RedirectAttributes attributen) {todo.setCreateDate (LocalDateTime.now ()); todos.add (todo); attributes.addFlashAttribute ("todos", todos); retourneer nieuwe RedirectView ("/ sessionattributes / todos.html"); }

Spring maakt gebruik van een gespecialiseerd RedirectAttributes invoer van Model voor omleidingsscenario's om de codering van URL-parameters te ondersteunen. Tijdens een omleiding kunnen alle attributen die zijn opgeslagen op het Model zouden normaal gesproken alleen beschikbaar zijn voor het framework als ze in de URL waren opgenomen.

Door het gebruiken van addFlashAttribute we vertellen het raamwerk dat we onze willen Te doen lijst om de omleiding te overleven zonder het in de URL te hoeven coderen.

5.2. Testen van een eenheid

De eenheidstest van de formulierweergavecontroller-methode is identiek aan de test die we in ons eerste voorbeeld hebben bekeken. De test van de @PostMappingis echter een beetje anders omdat we toegang moeten hebben tot de flash-attributen om het gedrag te verifiëren:

@Test openbare leegte whenTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo () gooit uitzondering {FlashMap flashMap = mockMvc.perform (post ("/ sessionattributes / form") .param ("description", "newtodo")) .andExpect () status (). andReturn (). getFlashMap (); MvcResult resultaat = 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. Discussie

De @ModelAttribute en @SessionAttributes strategie voor het opslaan van een attribuut in de sessie is een eenvoudige oplossing die vereist geen aanvullende contextconfiguratie of Spring-managed @Boons.

In tegenstelling tot ons eerste voorbeeld, is het nodig om te injecteren Te doen lijst in de @RequestMapping methoden.

Bovendien moeten we gebruik maken van flash-attributen voor omleidingsscenario's.

6. Conclusie

In dit artikel hebben we gekeken naar het gebruik van scoped proxy's en @SessionAttributes als 2 strategieën voor het werken met sessieattributen in Spring MVC. Merk op dat in dit eenvoudige voorbeeld alle attributen die in de sessie zijn opgeslagen, alleen de levensduur van de sessie zullen overleven.

Als we kenmerken moesten behouden tussen het opnieuw opstarten van de server of sessietime-outs, zouden we kunnen overwegen om Spring Session te gebruiken om het opslaan van de informatie transparant af te handelen. Bekijk ons ​​artikel over Spring Session voor meer informatie.

Zoals altijd is alle code die in dit artikel wordt gebruikt, beschikbaar op GitHub.