Sessionsattributter i foråret MVC

1. Oversigt

Når vi udvikler webapplikationer, er vi ofte nødt til at henvise til de samme attributter i flere visninger. For eksempel har vi muligvis indhold i indkøbskurv, der skal vises på flere sider.

En god placering til at gemme disse attributter er i brugerens session.

I denne vejledning fokuserer vi på et simpelt eksempel og undersøge 2 forskellige strategier til at arbejde med en sessionsattribut:

  • Brug af en afgrænset proxy
  • Bruger @SessionAttributter kommentar

2. Maven-opsætning

Vi bruger Spring Boot-startere til at bootstrape vores projekt og bringe alle nødvendige afhængigheder ind.

Vores opsætning kræver en overordnet erklæring, en webstarter og en startblæser.

Vi inkluderer også forårsteststarteren for at give nogle ekstra hjælpeprogrammer i vores enhedstest:

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

De seneste versioner af disse afhængigheder kan findes på Maven Central.

3. Eksempel på brugssag

Vores eksempel implementerer en simpel “TODO” -applikation. Vi har en formular til oprettelse af forekomster af TodoItem og en listevisning, der viser alle TodoItems.

Hvis vi opretter en TodoItem ved hjælp af formularen forudbestilles efterfølgende adgang til formularen med værdierne for det senest tilføjede TodoItem. Vi bruger thans funktion til at demonstrere, hvordan man "husker" formværdier der er gemt i sessionsomfang.

Vores 2 modelklasser implementeres som enkle POJO'er:

offentlig klasse TodoItem {privat beskrivelse af streng; privat LocalDateTime createDate; // getters og setters}
offentlig klasse TodoList udvider ArrayDeque {}

Vores TodoList klasse udvides ArrayDeque for at give os praktisk adgang til det senest tilføjede emne via kig sidst metode.

Vi har brug for to controller-klasser: 1 for hver af de strategier, vi vil se på. De har subtile forskelle, men kernefunktionaliteten vil være repræsenteret i begge. Hver vil have 3 @RequestMappings:

  • @GetMapping (“/ form”) - Denne metode er ansvarlig for initialisering af formularen og gengivelse af formularvisningen. Metoden forudfylder formularen med den senest tilføjede TodoItem hvis den TodoList er ikke tom.
  • @PostMapping (“/ form”) - Denne metode er ansvarlig for at tilføje den indsendte TodoItem til TodoList og omdirigere til listen URL.
  • @GetMapping (“/ todos.html”) - Denne metode vil blot tilføje TodoList til Model til visning og gengivelse af listevisningen.

4. Brug af en Scoped Proxy

4.1. Opsætning

I denne opsætning er vores TodoList er konfigureret som en session-scoped @Bønne der er bakket op af en proxy. Det faktum, at @Bønne er en proxy betyder, at vi er i stand til at injicere det i vores singleton-scoped @Kontrol.

Da der ikke er nogen session, når konteksten initialiseres, opretter Spring en proxy for TodoList at injicere som en afhængighed. Målet forekomst af TodoList vil blive instantificeret efter behov, når det kræves af anmodninger.

For en mere detaljeret diskussion af bønneromfang i foråret henvises til vores artikel om emnet.

For det første definerer vi vores bønne inden for en @Konfiguration klasse:

@Bean @Scope (værdi = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) offentlige TodoList todos () {returner ny TodoList (); }

Dernæst erklærer vi bønnen som en afhængighed af @Kontrol og indsprøjt det ligesom enhver anden afhængighed:

@Controller @RequestMapping ("/ scopedproxy") offentlig klasse TodoControllerWithScopedProxy {private TodoList todos; // konstruktør- og anmodningstilknytninger} 

Endelig indebærer brug af bønnen i en anmodning blot at kalde dens metoder:

@GetMapping ("/ form") public String showForm (Model model) {if (! Todos.isEmpty ()) {model.addAttribute ("todo", todos.peekLast ()); } andet {model.addAttribute ("todo", nyt TodoItem ()); } returner "scopedproxyform"; }

4.2. Enhedstest

For at teste vores implementering ved hjælp af den scoped proxy, vi konfigurerer først en SimpleThreadScope. Dette vil sikre, at vores enhedstest nøjagtigt simulerer kørselsforhold for den kode, vi tester.

For det første definerer vi en TestConfig og en CustomScopeConfigurer:

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

Nu kan vi starte med at teste, at en første anmodning fra formularen indeholder en ikke-initialiseret TodoItem:

@RunWith (SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @Import (TestConfig.class) offentlig klasse TodoControllerWithScopedProxyIntegrationTest {// ... @Test offentlig ugyldig, nårFirstRequest_thenContainsUnintializedTodo { form ")) .andExpect (status (). isOk ()). andExpect (model (). attributeExists (" todo ")). og Retur (); TodoItem-element = (TodoItem) resultat.getModelAndView (). GetModel (). Get ("todo"); assertTrue (StringUtils.isEmpty (item.getDescription ())); }} 

Vi kan også bekræfte, at vores indsendelse udsteder en omdirigering, og at en efterfølgende formularanmodning forudbestilles med det nyligt tilføjede TodoItem:

@Test offentlig ugyldig nårSubmit_thenSubsequentFormRequestContainsMostRecentTodo () kaster undtagelse {mockMvc.perform (post ("/ scopedproxy / form") .param ("beskrivelse", "newtodo")). Og Expect (status (). Is3xxRedirection ()). ; MvcResult-resultat = mockMvc.perform (get ("/ scopedproxy / form")). OgExpect (status (). IsOk ()). Og Expect (model (). AttributeExists ("todo")). AndReturn (); TodoItem-element = (TodoItem) resultat.getModelAndView (). GetModel (). Get ("todo"); assertEquals ("newtodo", item.getDescription ()); }

4.3. Diskussion

Et nøglefunktion ved brug af den scoped proxy-strategi er, at det har ingen indflydelse på signaturer til anmodningskortlægningsmetoder. Dette holder læsbarheden på et meget højt niveau sammenlignet med @SessionAttributter strategi.

Det kan være nyttigt at huske, at controllere har singleton omfang som standard.

Dette er grunden til, at vi skal bruge en proxy i stedet for blot at injicere en ikke-proxied session-scoped bønne. Vi kan ikke injicere en bønne med et mindre omfang i en bønne med større omfang.

Forsøg på at gøre det, i dette tilfælde, vil udløse en undtagelse med en besked, der indeholder: Scope 'session' er ikke aktiv for den aktuelle tråd.

Hvis vi er villige til at definere vores controller med sessionsomfang, kan vi undgå at specificere en proxyMode. Dette kan have ulemper, især hvis controlleren er dyr at oprette, fordi en controllerinstans skulle oprettes for hver brugersession.

Noter det TodoList er tilgængelig for andre komponenter til injektion. Dette kan være en fordel eller en ulempe afhængigt af brugssagen. Hvis det er problematisk at stille bønnen til rådighed for hele applikationen, kan forekomsten scopes til controlleren i stedet for @SessionAttributter som vi vil se i det næste eksempel.

5. Brug af @SessionAttributter Kommentar

5.1. Opsætning

I denne opsætning definerer vi ikke TodoList som en Spring-managed @Bønne. I stedet for erklær det som en @ModelAttribute og specificer @SessionAttributter kommentar til at omfatte den til controlleren.

Første gang vores controller får adgang, springer Spring en instans og placerer den i Model. Da vi også erklærer bønnen i @SessionAttributter, Spring gemmer instansen.

For en mere detaljeret diskussion af @ModelAttribute om foråret henvises til vores artikel om emnet.

Først erklærer vi vores bønne ved at give en metode på controlleren, og vi kommenterer metoden med @ModelAttribute:

@ModelAttribute ("todos") offentlige TodoList todos () {returner ny TodoList (); } 

Dernæst informerer vi controlleren om at behandle vores TodoList som session-scoped ved hjælp af @SessionAttributter:

@Controller @RequestMapping ("/ sessionattributter") @SessionAttributter ("todos") offentlig klasse TodoControllerWithSessionAttribute {// ... andre metoder}

Endelig giver vi en henvisning til den for at bruge bønnen i en anmodning i metodesignaturen til a @RequestMapping:

@GetMapping ("/ form") public String showForm (Model model, @ModelAttribute ("todos") TodoList todos) {if (! Todos.isEmpty ()) {model.addAttribute ("todo", todos.peekLast ()) ; } andet {model.addAttribute ("todo", nyt TodoItem ()); } returner "sessionattributform"; } 

I @PostMapping metode, vi injicerer RedirectAttributter og ring addFlashAttribute inden du returnerer vores RedirectView. Dette er en vigtig forskel i implementeringen sammenlignet med vores første eksempel:

@PostMapping ("/ form") public RedirectView create (@ModelAttribute TodoItem todo, @ModelAttribute ("todos") TodoList todos, RedirectAttribute attributter) {todo.setCreateDate (LocalDateTime.now ()); todos.add (todo); attributter.addFlashAttribute ("todos", todos); returner ny RedirectView ("/ sessionattributter / todos.html"); }

Foråret bruger en specialiseret RedirectAttributter gennemførelse af Model til omdirigeringsscenarier til understøttelse af kodning af URL-parametre. Under en omdirigering gemmes alle attributter, der er gemt på Model ville normalt kun være tilgængelige for rammen, hvis de var inkluderet i URL'en.

Ved hjælp af addFlashAttribute vi fortæller rammen, som vi ønsker vores TodoList for at overleve omdirigering uden at skulle kode det i URL'en.

5.2. Enhedstest

Enhedstesten af ​​metoden med formularvisningskontrol er identisk med den test, vi så på i vores første eksempel. Testen af @PostMappinger dog lidt anderledes, fordi vi har brug for adgang til flashattributterne for at verificere adfærden:

@Test offentlig ugyldig, nårTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo () kaster undtagelse {FlashMap flashMap = mockMvc.perform (post ("/ sessionattributter / form") .param ("beskrivelse", "newtodo")). Og Expect (status () .3) .xx. andReturn (). getFlashMap (); MvcResult resultat = mockMvc.perform (get ("/ sessionattributter / form"). SessionAttrs (flashMap)). Og Expect (status (). IsOk ()). Og Expect (model (). AttributeExists ("todo")). Og Retur ( ); TodoItem-element = (TodoItem) resultat.getModelAndView (). GetModel (). Get ("todo"); assertEquals ("newtodo", item.getDescription ()); }

5.3. Diskussion

Det @ModelAttribute og @SessionAttributter strategi til lagring af en attribut i sessionen er en ligetil løsning, der kræver ingen yderligere kontekstkonfiguration eller Spring-managed @Bønnes.

I modsætning til vores første eksempel er det nødvendigt at injicere TodoList i @RequestMapping metoder.

Derudover skal vi bruge flash-attributter til omdirigeringsscenarier.

6. Konklusion

I denne artikel så vi på brugen af ​​scoped-fuldmagter og @SessionAttributter som 2 strategier til at arbejde med sessionsattributter i Spring MVC. Bemærk, at i dette enkle eksempel vil alle attributter, der er gemt i sessionen, kun overleve i løbet af sessionen.

Hvis vi har brug for at fastholde attributter mellem serverstart eller sessionstimeouts, kan vi overveje at bruge Spring Session til at håndtere lagring af oplysningerne transparent. Se vores artikel om forårssession for mere information.

Som altid er al kode, der bruges i denne artikel, tilgængelig på GitHub.