Sesjonsattributter i vår-MVC

1. Oversikt

Når vi utvikler webapplikasjoner, trenger vi ofte å referere til de samme attributtene i flere visninger. Vi kan for eksempel ha innhold i handlekurven som må vises på flere sider.

Et godt sted å lagre disse attributtene er i brukerens økt.

I denne opplæringen vil vi fokusere på et enkelt eksempel og undersøke to forskjellige strategier for å jobbe med en sesjonsattributt:

  • Ved hjelp av en fullmakt fullmektig
  • Bruker @SessionAttributter kommentar

2. Maven-oppsett

Vi bruker Spring Boot-startere for å starte opp prosjektet vårt og bringe inn alle nødvendige avhengigheter.

Vårt oppsett krever en foreldredeklarasjon, en webstarter og en startblokk.

Vi vil også inkludere vårteststarteren for å gi litt ekstra verktøy i enhetstestene våre:

 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 nyeste versjonene av disse avhengighetene finnes på Maven Central.

3. Eksempel på brukstilfelle

Eksemplet vårt vil implementere en enkel “TODO” -applikasjon. Vi har et skjema for å lage forekomster av TodoItem og en listevisning som viser alt TodoItems.

Hvis vi lager en TodoItem ved hjelp av skjemaet, vil etterfølgende tilganger til skjemaet forhåndsutfylles med verdiene til det sist lagt til TodoItem. Vi bruker thans funksjon for å demonstrere hvordan man kan "huske" formverdier som er lagret i sesjonsomfang.

Våre to modellklasser er implementert som enkle POJOer:

offentlig klasse TodoItem {privat strengbeskrivelse; privat LocalDateTime createDate; // getters og setters}
offentlig klasse TodoList utvider ArrayDeque {}

Våre Gjøremålsliste klasse utvider ArrayDeque for å gi oss praktisk tilgang til det sist lagt til varen via peekLast metode.

Vi trenger to kontrollerklasser: 1 for hver av strategiene vi skal se på. De vil ha subtile forskjeller, men kjernefunksjonaliteten vil være representert i begge. Hver vil ha 3 @RequestMappings:

  • @GetMapping (“/ form”) - Denne metoden vil være ansvarlig for å initialisere skjemaet og gjengi skjemavisningen. Metoden vil fylle ut skjemaet med det sist lagt til TodoItem hvis Gjøremålsliste er ikke tom.
  • @PostMapping (“/ form”) - Denne metoden vil være ansvarlig for å legge til den innsendte TodoItem til Gjøremålsliste og omdirigere til URL-en til listen.
  • @GetMapping (“/ todos.html”) - Denne metoden vil ganske enkelt legge til Gjøremålsliste til Modell for visning og gjengivelse av listevisningen.

4. Bruke en Scoped Proxy

4.1. Oppsett

I dette oppsettet, vår Gjøremålsliste er konfigurert som et sesjonsomfang @Bønne som støttes av en fullmektig. Det faktum at @Bønne er en fullmektig betyr at vi er i stand til å injisere den i vår singleton-scoped @Kontrollør.

Siden det ikke er noen økt når konteksten initialiseres, oppretter Spring en proxy for Gjøremålsliste å injisere som en avhengighet. Målet forekomst av Gjøremålsliste vil bli instansert etter behov når forespørsler krever det.

For en mer inngående diskusjon av bønnevirksomhet om våren, se vår artikkel om emnet.

Først definerer vi vår bønne innenfor en @Konfigurasjon klasse:

@Bean @Scope (verdi = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) offentlig TodoList todos () {returner ny TodoList (); }

Deretter erklærer vi bønnen som en avhengighet for @Kontrollør og injiser det akkurat som vi ville gjort med annen avhengighet:

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

Til slutt innebærer bruk av bønnen i en forespørsel bare å kalle metodene:

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

4.2. Enhetstesting

For å teste implementeringen vår ved hjelp av en omfattende proxy, vi konfigurerer først en SimpleThreadScope. Dette vil sikre at enhetstestene våre simulerer kjøretidsforholdene til koden vi tester nøyaktig.

Først definerer vi a TestConfig og en CustomScopeConfigurer:

@Configuration public class TestConfig {@Bean public CustomScopeConfigurer customScopeConfigurer () {CustomScopeConfigurer configurer = new CustomScopeConfigurer (); configurer.addScope ("økt", nytt SimpleThreadScope ()); retur konfigurator; }}

Nå kan vi starte med å teste at en første forespørsel fra skjemaet inneholder en uinitialisert TodoItem:

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

Vi kan også bekrefte at innsendingen vår sender en viderekobling, og at en etterfølgende skjemaforespørsel er forhåndsutfylt med det nylig lagt til TodoItem:

@Test offentlig ugyldig nårSubmit_thenSubsequentFormRequestContainsMostRecentTodo () kaster unntak {mockMvc.perform (post ("/ scopedproxy / form") .param ("description", "newtodo")). Og Expect (status (). Is3xxRedirection ()). ; MvcResult result = mockMvc.perform (get ("/ scopedproxy / form")). AndExpect (status (). IsOk ()). And Exppect (model (). AttributeExists ("todo")). AndReturn (); TodoItem item = (TodoItem) result.getModelAndView (). GetModel (). Get ("todo"); assertEquals ("newtodo", item.getDescription ()); }

4.3. Diskusjon

Et sentralt trekk ved bruk av den omfattende proxy-strategien er at det har ingen innvirkning på signaturer for forespørsel om kartleggingsmetoder. Dette holder lesbarheten på et veldig høyt nivå sammenlignet med @SessionAttributes strategi.

Det kan være nyttig å huske at kontrollere har singleton omfang som standard.

Dette er grunnen til at vi må bruke en proxy i stedet for bare å injisere en ikke-nærliggende økt-scoped bønne. Vi kan ikke injisere en bønne med mindre omfang i en bønne med større omfang.

Forsøk på å gjøre det, i dette tilfellet, vil utløse et unntak med en melding som inneholder: Omfang ‘økt’ er ikke aktivt for den gjeldende tråden.

Hvis vi er villige til å definere kontrolleren vår med øktomfang, kan vi unngå å spesifisere en proxyMode. Dette kan ha ulemper, spesielt hvis kontrolleren er dyr å opprette fordi det må opprettes en kontrollerforekomst for hver brukersesjon.

Noter det Gjøremålsliste er tilgjengelig for andre komponenter for injeksjon. Dette kan være en fordel eller en ulempe avhengig av brukssaken. Hvis det er problematisk å gjøre bønnen tilgjengelig for hele applikasjonen, kan forekomsten tas til kontrolleren i stedet for @SessionAttributes som vi får se i neste eksempel.

5. Bruke @SessionAttributes Kommentar

5.1. Oppsett

I dette oppsettet definerer vi ikke Gjøremålsliste som en vårstyrt @Bønne. I stedet vi erklære det som en @ModelAttribute og spesifiser @SessionAttributes kommentar for å omfatte den til økten for kontrolleren.

Første gang vi får tilgang til kontrolleren vår, vil Spring starte en forekomst og plassere den i Modell. Siden vi også erklærer bønnen i @SessionAttributes, Spring lagrer forekomsten.

For en mer inngående diskusjon av @ModelAttribute om våren, se vår artikkel om emnet.

Først erklærer vi vår bønne ved å gi en metode på kontrolleren, og vi kommenterer metoden med @ModelAttribute:

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

Deretter informerer vi kontrolleren om å behandle vår Gjøremålsliste som sesjonsomfang ved bruk av @SessionAttributes:

@Controller @RequestMapping ("/ sessionattributes") @SessionAttributes ("todos") offentlig klasse TodoControllerWithSessionAttributes {// ... andre metoder}

Til slutt, for å bruke bønnen i en forespørsel, gir vi en referanse til den i metodesignaturen til a @RequestMapping:

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

I @PostMapping metode, injiserer vi RedirectAttributter og ring addFlashAttribute før du returnerer vår RedirectView. Dette er en viktig forskjell i implementering sammenlignet med vårt 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 ("/ sessionattributes / todos.html"); }

Våren bruker en spesialisert RedirectAttributter Implementering av Modell for omdirigeringsscenarier for å støtte koding av URL-parametere. Under en viderekobling lagres alle attributter som er lagret på Modell ville normalt bare være tilgjengelig for rammeverket hvis de var inkludert i URL-en.

Ved bruk av addFlashAttribute vi forteller rammene som vi vil ha vår Gjøremålsliste for å overleve omdirigering uten å måtte kode det i URL-en.

5.2. Enhetstesting

Enhetstesting av metoden for skjemavisningskontroll er identisk med testen vi så på i vårt første eksempel. Testen av @PostMappinger imidlertid litt annerledes fordi vi trenger tilgang til flash-attributtene for å bekrefte atferden:

@Test offentlig ugyldig nårTodoExists_thenSubsequentFormRequestContainsesMostRecentTodo () kaster unntak {FlashMap flashMap = mockMvc.perform (post ("/ sessionattributes / form") .param ("beskrivelse", "newtodo")). Og Expect (status () .3). 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. Diskusjon

De @ModelAttribute og @SessionAttributes strategi for å lagre et attributt i økten er en grei løsning som krever ingen ekstra kontekstkonfigurasjon eller fjærstyrt @Bønnes.

I motsetning til vårt første eksempel, er det nødvendig å injisere Gjøremålsliste i @RequestMapping metoder.

I tillegg må vi bruke flash-attributter for omdirigeringsscenarier.

6. Konklusjon

I denne artikkelen så vi på å bruke scoped fullmakter og @SessionAttributes som to strategier for å jobbe med sesjonsattributter i Spring MVC. Merk at i dette enkle eksemplet vil alle attributter som er lagret i økten bare overleve i løpet av økten.

Hvis vi trengte å vedvare attributter mellom serverstart på nytt eller tidsavbrudd for økter, kan vi vurdere å bruke Spring Session for å håndtere lagring av informasjonen på en transparent måte. Ta en titt på artikkelen vår om vårsession for mer informasjon.

Som alltid er all kode som brukes i denne artikkelen tilgjengelig på GitHub.