Atribut Sesi di Spring MVC

1. Gambaran keseluruhan

Semasa mengembangkan aplikasi web, kita sering perlu merujuk kepada atribut yang sama dalam beberapa paparan. Sebagai contoh, kami mungkin mempunyai isi keranjang belanja yang perlu dipaparkan di beberapa halaman.

Lokasi yang baik untuk menyimpan atribut tersebut adalah dalam sesi pengguna.

Dalam tutorial ini, kita akan memfokuskan pada contoh mudah dan mengkaji 2 strategi yang berbeza untuk bekerja dengan atribut sesi :

  • Menggunakan proksi skop
  • Menggunakan anotasi @ SessionAttributes

2. Persediaan Maven

Kami akan menggunakan permulaan Boot Musim Semi untuk memulakan projek kami dan membawa semua pergantungan yang diperlukan.

Penyediaan kami memerlukan pengisytiharan ibu bapa, pemula web, dan pemula thymeleaf.

Kami juga akan memasukkan starter ujian musim bunga untuk menyediakan beberapa utiliti tambahan dalam ujian unit kami:

 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  

Versi ketergantungan terbaru boleh didapati di Maven Central.

3. Contoh Kes Penggunaan

Contoh kami akan melaksanakan aplikasi "TODO" sederhana. Kami akan mempunyai borang untuk membuat contoh TodoItem dan paparan senarai yang memaparkan semua TodoItem .

Sekiranya kita membuat TodoItem menggunakan borang, akses borang seterusnya akan diisi dengan nilai TodoItem yang baru ditambahkan . Kami akan menggunakan t ciri untuk menunjukkan bagaimana untuk "ingat" nilai-nilai borang yang disimpan di dalam skop sesi.

Kelas 2 model kami dilaksanakan sebagai POJO sederhana:

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

Kelas TodoList kami meluaskan ArrayDeque untuk memberi kami akses mudah ke item yang paling baru ditambahkan melalui kaedah peekLast .

Kami memerlukan 2 kelas pengawal: 1 untuk setiap strategi yang akan kami lihat. Mereka akan mempunyai perbezaan yang halus tetapi fungsi inti akan ditunjukkan dalam kedua-duanya. Masing-masing akan mempunyai 3 @RequestMapping s:

  • @GetMapping (“/ form”) - Kaedah ini akan bertanggungjawab untuk menginisialisasi bentuk dan membuat tampilan borang. Kaedah ini akan mengisi borang dengan TodoItem yang paling baru ditambahkansekiranya TodoList tidak kosong.
  • @PostMapping ("/ form") - Kaedah ini akan bertanggungjawab untuk menambahkan TodoItem yang dihantarke TodoList dan mengarahkan ke URL senarai.
  • @GetMapping (“/ todos.html”) - Kaedah ini hanya akan menambahkan TodoList ke Model untuk dipaparkan dan membuat paparan senarai.

4. Menggunakan Proksi Terkumpul

4.1. Persediaan

Dalam penyediaan ini, TodoList kami dikonfigurasikan sebagai @Bean sesi-sesi yang disokong oleh proksi. Fakta bahawa @Bean adalah proksi bermaksud bahawa kita dapat memasukkannya ke dalam @Controller tunggal-scoped kami .

Oleh kerana tidak ada sesi ketika konteks diinisialisasi, Spring akan membuat proksi TodoList untuk disuntik sebagai ketergantungan. Contoh sasaran TodoList akan dibuat seperti yang diperlukan ketika diminta oleh permintaan.

Untuk perbincangan yang lebih mendalam mengenai bidang kacang pada musim bunga, rujuk artikel kami mengenai topik ini.

Pertama, kami menentukan kacang dalam kelas @Configuration :

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

Seterusnya, kami menyatakan kacang sebagai pergantungan untuk @Controller dan menyuntikkannya seperti yang kita lakukan seperti kebergantungan lain:

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

Akhirnya, menggunakan kacang dalam permintaan hanya melibatkan memanggil kaedahnya:

@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. Ujian Unit

Untuk menguji pelaksanaan kami menggunakan proksi scoped, pertama-tama kami mengkonfigurasi SimpleThreadScope . Ini akan memastikan bahawa ujian unit kami mensimulasikan keadaan runtime kod yang kami uji dengan tepat.

Pertama, kami menentukan TestConfig dan CustomScopeConfigurer :

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

Sekarang kita boleh mulai dengan menguji bahawa permintaan awal borang mengandungi TodoItem yang belum dimulakan:

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

Kami juga dapat mengesahkan bahawa penyerahan kami mengeluarkan pengalihan dan bahawa permintaan borang berikutnya diisi dengan TodoItem yang baru ditambahkan :

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

Ciri utama menggunakan strategi proksi scoped adalah bahawa ia tidak memberi kesan pada tandatangan kaedah pemetaan permintaan Ini mengekalkan kebolehbacaan pada tahap yang sangat tinggi berbanding dengan strategi @SessionAttributes .

Adalah berguna untuk mengingat bahawa pengawal mempunyai skop tunggal secara lalai.

Inilah sebab mengapa kita mesti menggunakan proksi dan bukannya hanya menyuntikkan kacang tanpa sesi tanpa proksi. Kami tidak dapat memasukkan kacang dengan skop yang lebih rendah ke dalam kacang dengan skop yang lebih besar.

Mencuba untuk melakukannya, dalam hal ini, akan mencetuskan pengecualian dengan mesej yang mengandungi: Skop 'sesi' tidak aktif untuk utas semasa .

Sekiranya kita bersedia menentukan pengawal kita dengan skop sesi, kita tidak boleh menentukan kaedah proxyMode . Ini boleh mempunyai keburukan, terutama jika pengawal mahal dibuat kerana contoh pengawal harus dibuat untuk setiap sesi pengguna.

Perhatikan bahawa TodoList tersedia untuk komponen lain untuk suntikan. Ini mungkin memberi kebaikan atau keburukan bergantung pada kes penggunaan. Sekiranya membuat kacang tersedia untuk seluruh aplikasi bermasalah, contohnya dapat dilampirkan ke pengawal dan bukan menggunakan @SessionAttributes seperti yang akan kita lihat dalam contoh berikutnya.

5. Menggunakan Anotasi @SessionAttributes

5.1. Persediaan

Dalam persediaan ini, kita tidak menentukan TodoList sebagai Spring diurus @Bean . Sebagai gantinya, kami mengisytiharkannya sebagai @ModelAttribute dan menentukan anotasi @SessionAttributes untuk merangkumnya ke sesi untuk pengawal .

Kali pertama pengawal kami diakses, Spring akan memberi contoh dan memasukkannya ke dalam Model . Oleh kerana kami juga menyatakan kacang di @SessionAttributes , Spring akan menyimpan contohnya.

Untuk perbincangan yang lebih mendalam mengenai @ModelAttribute di Spring, rujuk artikel kami mengenai topik ini.

Pertama, kami menyatakan kacang dengan memberikan kaedah pada pengawal dan kami memberi penjelasan kaedah dengan @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

Dalam artikel ini, kami melihat menggunakan proksi scoped dan @SessionAttribut sebagai 2 strategi untuk bekerja dengan atribut sesi di Spring MVC. Perhatikan bahawa dalam contoh mudah ini, setiap atribut yang disimpan dalam sesi hanya akan bertahan sepanjang hayat sesi tersebut.

Sekiranya kita perlu mengekalkan atribut antara restart pelayan atau tamat waktu sesi, kita boleh mempertimbangkan untuk menggunakan Spring Session untuk menangani penyimpanan maklumat secara telus. Lihat artikel kami pada Spring Session untuk maklumat lebih lanjut.

Seperti biasa, semua kod yang digunakan dalam artikel ini tersedia di GitHub.