1. 概述
在Web应用开发中,我们经常需要在多个视图间共享同一属性。例如购物车内容需要在多个页面展示。用户Session是存储这类属性的理想位置。
本文通过一个简单示例,重点探讨两种处理Session属性的策略:
- 使用作用域代理
- 使用@SessionAttributes注解
2. Maven配置
使用Spring Boot快速搭建项目,核心依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
最新版本可在Maven Central获取。
3. 示例场景
实现一个简单的TODO应用:包含创建TodoItem的表单和展示所有TodoItem的列表视图。核心功能:
- 通过表单创建TodoItem后,再次访问表单时自动填充最新项
- 通过此特性演示Session属性"记忆"表单值的能力
模型类设计为简单POJO:
public class TodoItem {
private String description;
private LocalDateTime createDate;
// getters and setters
}
public class TodoList extends ArrayDeque<TodoItem>{
}
TodoList
继承ArrayDeque
便于通过peekLast()
获取最新项。需要两个Controller类(分别对应两种策略),核心功能相同,均包含三个@RequestMapping
:
@GetMapping("/form")
:初始化表单并渲染视图,若TodoList
非空则预填充最新项@PostMapping("/form")
:提交TodoItem到TodoList
并重定向到列表页@GetMapping("/todos.html")
:添加TodoList
到Model并渲染列表视图
4. 使用作用域代理
4.1 配置
将TodoList
声明为Session作用域的@Bean
,并通过代理模式注入单例Controller。Spring在上下文初始化时创建代理对象,实际实例在请求需要时创建。
首先在@Configuration
类中定义Bean:
@Bean
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public TodoList todos() {
return new TodoList();
}
在Controller中注入:
@Controller
@RequestMapping("/scopedproxy")
public class TodoControllerWithScopedProxy {
private TodoList todos;
// constructor and request mappings
}
请求处理中直接调用方法:
@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 单元测试
使用SimpleThreadScope
模拟真实运行环境:
@Configuration
public class TestConfig {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("session", new SimpleThreadScope());
return configurer;
}
}
测试首次请求表单包含未初始化的TodoItem:
@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()));
}
}
测试提交后表单预填充最新项:
@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 关键点分析
✅ 核心优势:不影响请求映射方法签名,代码可读性高
⚠️ 注意事项:
- Controller默认为单例,必须使用代理注入Session作用域Bean
- 直接注入会抛出异常:
Scope 'session' is not active for the current thread
- 若将Controller设为Session作用域可避免代理,但创建成本高(每用户会话创建实例)
TodoList
可被其他组件注入,需按需评估利弊
5. 使用@SessionAttributes注解
5.1 配置
不将TodoList
声明为Spring管理的Bean,而是:
- 通过
@ModelAttribute
方法声明 - 用
@SessionAttributes
指定Session作用域
在Controller中声明:
@ModelAttribute("todos")
public TodoList todos() {
return new TodoList();
}
用注解标记Session作用域:
@Controller
@RequestMapping("/sessionattributes")
@SessionAttributes("todos")
public class TodoControllerWithSessionAttributes {
// ... other methods
}
请求处理中需显式注入:
@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";
}
重定向时使用addFlashAttribute
:
@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");
}
关键差异:重定向时通过Flash属性传递数据,避免URL编码。
5.2 单元测试
测试表单视图方法与代理策略相同,测试@PostMapping
需访问Flash属性:
@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 关键点分析
✅ 优势:无需额外配置或Spring管理的Bean
❌ 劣势:
- 请求方法必须显式注入
TodoList
- 重定向场景需处理Flash属性
6. 总结
本文对比了两种Spring MVC中管理Session属性的策略:
- 作用域代理:适合跨组件共享,需注意注入规则
- @SessionAttributes:实现简单,但作用域限于Controller
两种方案均依赖Session生命周期,若需跨服务器重启持久化,可考虑Spring Session。完整代码见GitHub仓库。