Atribut Sesi di MVC Musim Semi

1. Ikhtisar

Saat mengembangkan aplikasi web, sering kali kita perlu merujuk ke atribut yang sama di beberapa tampilan. Misalnya, kami mungkin memiliki konten keranjang belanja yang perlu ditampilkan di beberapa halaman.

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

Dalam tutorial ini, kita akan fokus pada contoh sederhana dan memeriksa 2 strategi berbeda untuk bekerja dengan atribut sesi :

  • Menggunakan proxy terbatas
  • Menggunakan anotasi @ SessionAttributes

2. Pengaturan Maven

Kami akan menggunakan permulaan Spring Boot untuk mem-bootstrap proyek kami dan memasukkan semua dependensi yang diperlukan.

Penyiapan kami memerlukan deklarasi induk, web starter, dan starter timeleaf.

Kami juga akan menyertakan starter uji pegas untuk memberikan beberapa utilitas tambahan dalam pengujian 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 terbaru dari dependensi ini dapat ditemukan di Maven Central.

3. Contoh Kasus Penggunaan

Contoh kami akan menerapkan aplikasi "TODO" sederhana. Kami akan memiliki formulir untuk membuat instance TodoItem dan tampilan daftar yang menampilkan semua TodoItem .

Jika kita membuat TodoItem menggunakan formulir, akses formulir berikutnya akan diisi sebelumnya dengan nilai TodoItem yang paling baru ditambahkan . Kami akan menggunakan t fitur untuk menunjukkan bagaimana untuk “mengingat” nilai bentuk yang disimpan dalam ruang lingkup sesi.

2 kelas model kami diimplementasikan sebagai POJO sederhana:

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

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

Kita membutuhkan 2 kelas pengontrol: 1 untuk setiap strategi yang akan kita lihat. Mereka akan memiliki perbedaan halus tetapi fungsionalitas intinya akan terwakili di keduanya. Masing-masing akan memiliki 3 @RequestMapping :

  • @GetMapping ("/ form") - Metode ini akan bertanggung jawab untuk menginisialisasi formulir dan merender tampilan formulir. Metode ini akan mengisi formulir dengan TodoItem yang paling baru ditambahkanjika TodoList tidak kosong.
  • @PostMapping ("/ form") - Metode ini akan bertanggung jawab untuk menambahkan TodoItem yang dikirimkanke TodoList dan mengarahkan ke URL daftar.
  • @GetMapping (“/ todos.html”) - Metode ini hanya akan menambahkan TodoList ke Model untuk ditampilkan dan membuat tampilan daftar.

4. Menggunakan Proxy Cakupan

4.1. Mendirikan

Dalam konfigurasi ini, kami ToDoList dikonfigurasi sebagai sesi-scoped @Bean yang didukung oleh proxy. Fakta bahwa @Bean adalah proxy berarti kita dapat memasukkannya ke dalam @Controller dengan cakupan tunggal .

Karena tidak ada sesi saat konteks diinisialisasi, Spring akan membuat proxy TodoList untuk dimasukkan sebagai dependensi. Instance target TodoList akan dibuat sesuai kebutuhan bila diperlukan oleh permintaan.

Untuk diskusi yang lebih mendalam tentang lingkup kacang di Musim Semi, lihat artikel kami tentang topik tersebut.

Pertama, kami mendefinisikan kacang kami di dalam kelas @Configuration :

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

Selanjutnya, kami mendeklarasikan bean sebagai dependensi untuk @Controller dan menyuntikkannya seperti yang kami lakukan pada dependensi lainnya:

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

Akhirnya, menggunakan bean dalam sebuah request hanya dengan memanggil metodenya:

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

Untuk menguji implementasi kami menggunakan proxy terbatas , pertama-tama kami mengonfigurasi SimpleThreadScope . Ini akan memastikan bahwa pengujian unit kami secara akurat mensimulasikan kondisi runtime dari kode yang kami uji.

Pertama, kami mendefinisikan TestConfig dan CustomScopeConfigurer :

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

Sekarang kita bisa mulai dengan menguji bahwa permintaan awal formulir berisi TodoItem yang tidak diinisialisasi:

@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 mengonfirmasi bahwa pengiriman kami mengeluarkan pengalihan dan bahwa permintaan formulir berikutnya sudah diisi sebelumnya 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. Diskusi

Fitur utama dari menggunakan strategi proxy yang tercakup adalah bahwa hal itu tidak berdampak pada tanda tangan metode pemetaan permintaan. Hal ini menjaga keterbacaan pada tingkat yang sangat tinggi dibandingkan dengan strategi @SessionAttributes .

Akan sangat membantu untuk mengingat bahwa pengontrol memiliki lingkup tunggal secara default.

Inilah alasan mengapa kita harus menggunakan proxy alih-alih hanya menyuntikkan kacang dengan cakupan sesi yang tidak diproksikan. Kita tidak bisa menyuntikkan kacang dengan cakupan yang lebih kecil ke dalam kacang dengan cakupan yang lebih besar.

Mencoba melakukannya, dalam kasus ini, akan memicu pengecualian dengan pesan yang berisi: Cakupan 'sesi' tidak aktif untuk utas saat ini .

Jika kita ingin mendefinisikan controller kita dengan cakupan sesi, kita bisa menghindari menentukan proxyMode . Ini dapat memiliki kelemahan, terutama jika pengontrol mahal untuk dibuat karena instance pengontrol harus dibuat untuk setiap sesi pengguna.

Perhatikan bahwa TodoList tersedia untuk komponen lain untuk injeksi. Ini mungkin keuntungan atau kerugian tergantung pada kasus penggunaan. Jika membuat bean tersedia untuk seluruh aplikasi bermasalah, instance dapat dicakup ke controller alih-alih menggunakan @SessionAttributes seperti yang akan kita lihat di contoh berikutnya.

5. Menggunakan Anotasi @SessionAttributes

5.1. Mendirikan

Dalam konfigurasi ini, kita tidak mendefinisikan ToDoList sebagai Spring-dikelola @Bean . Sebagai gantinya, kami mendeklarasikannya sebagai @ModelAttribute dan menetapkan anotasi @SessionAttributes untuk cakupannya ke sesi untuk pengontrol .

Pertama kali pengontrol kita diakses, Spring akan membuat instance dan menempatkannya di Model . Karena kami juga mendeklarasikan bean di @SessionAttributes , Spring akan menyimpan instance tersebut.

Untuk diskusi yang lebih mendalam tentang @ModelAttribute di Spring, lihat artikel kami tentang topik tersebut.

Pertama, kami mendeklarasikan bean kami dengan menyediakan metode pada pengontrol dan kami menganotasi metode tersebut dengan @ModelAttribute :

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

Selanjutnya, kami memberi tahu pengontrol untuk memperlakukan TodoList kami sebagai cakupan sesi dengan menggunakan @SessionAttributes :

@Controller @RequestMapping("/sessionattributes") @SessionAttributes("todos") public class TodoControllerWithSessionAttributes { // ... other methods }

Terakhir, untuk menggunakan kacang dalam permintaan, kami memberikan referensi padanya di tanda tangan metode pada @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"; } 

Dalam metode @PostMapping , kami memasukkan RedirectAttributes dan memanggil addFlashAttribute sebelum mengembalikan RedirectView kami . Ini adalah perbedaan penting dalam implementasi dibandingkan dengan contoh pertama kami:

@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 menggunakan implementasi RedirectAttributes khusus Model untuk skenario pengalihan guna mendukung pengkodean parameter URL. Selama pengalihan, atribut apa pun yang disimpan pada Model biasanya hanya akan tersedia untuk kerangka kerja jika disertakan dalam URL.

Dengan menggunakan addFlashAttribute, kami memberi tahu kerangka kerja bahwa kami ingin TodoList kami bertahan dari pengalihan tanpa perlu menyandikannya di URL.

5.2. Pengujian Unit

Pengujian unit metode pengontrol tampilan formulir identik dengan pengujian yang kami lihat di contoh pertama kami. Namun, pengujian @PostMapping sedikit berbeda karena kita perlu mengakses atribut flash untuk memverifikasi perilakunya:

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

Strategi @ModelAttribute dan @SessionAttributes untuk menyimpan atribut dalam sesi adalah solusi langsung yang tidak memerlukan konfigurasi konteks tambahan atau @Bean s yang dikelola Spring .

Tidak seperti contoh pertama kami, perlu untuk menyuntikkan TodoList dalam metode @RequestMapping .

Selain itu, kita harus menggunakan atribut flash untuk skenario pengalihan.

6. Kesimpulan

Dalam artikel ini, kami melihat penggunaan proxy terbatas dan @SessionAttributes sebagai 2 strategi untuk bekerja dengan atribut sesi di Spring MVC. Perhatikan bahwa dalam contoh sederhana ini, atribut apa pun yang disimpan dalam sesi hanya akan bertahan selama sesi berlangsung.

Jika kami perlu mempertahankan atribut antara memulai ulang server atau waktu tunggu sesi, kami dapat mempertimbangkan untuk menggunakan Sesi Musim Semi untuk menangani penyimpanan informasi secara transparan. Bacalah artikel kami tentang Sesi Musim Semi untuk informasi lebih lanjut.

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