Appearance
Spring WebFlux 中的 @SessionAttribute 注解详解 🚀
概述
在现代 Web 应用开发中,会话管理是一个核心需求。想象一下,用户登录后,我们需要在多个请求之间保持用户的身份信息;或者在一个复杂的业务流程中,需要跨多个页面保存临时数据。这就是会话(Session)存在的意义。
Spring WebFlux 提供了 @SessionAttribute
注解,让我们能够优雅地访问会话中的属性,而无需直接操作底层的会话对象。
NOTE
@SessionAttribute
专门用于访问已存在的会话属性,这些属性通常由过滤器、拦截器或其他组件在控制器外部管理。
核心概念与设计哲学 💡
为什么需要 @SessionAttribute?
在没有 @SessionAttribute
之前,我们通常需要这样做:
kotlin
@GetMapping("/profile")
fun getUserProfile(exchange: ServerWebExchange): Mono<String> {
return exchange.session.map { session ->
val user = session.getAttribute("user") as? User
if (user != null) {
"Welcome, ${user.name}!"
} else {
"Please login first"
}
}
}
kotlin
@GetMapping("/profile")
fun getUserProfile(@SessionAttribute user: User): String {
return "Welcome, ${user.name}!"
}
可以看到,@SessionAttribute
大大简化了代码,让我们专注于业务逻辑而不是会话管理的细节。
设计哲学
@SessionAttribute
体现了 Spring 的几个核心设计理念:
- 声明式编程:通过注解声明需求,而不是命令式地编写获取逻辑
- 关注点分离:将会话管理与业务逻辑分离
- 类型安全:编译时就能确定类型,避免运行时类型转换错误
基本用法 📝
简单示例
kotlin
@RestController
class UserController {
@GetMapping("/dashboard")
fun dashboard(@SessionAttribute user: User): ResponseEntity<String> {
return ResponseEntity.ok("Welcome to dashboard, ${user.name}!")
}
@GetMapping("/settings")
fun settings(@SessionAttribute("currentUser") user: User): ResponseEntity<UserSettings> {
// 使用自定义属性名 "currentUser"
return ResponseEntity.ok(userService.getSettings(user.id))
}
}
处理可选的会话属性
kotlin
@RestController
class ShoppingController {
@GetMapping("/cart")
fun viewCart(
@SessionAttribute(required = false) cart: ShoppingCart?
): ResponseEntity<Any> {
return if (cart != null) {
ResponseEntity.ok(cart)
} else {
ResponseEntity.ok("Your cart is empty")
}
}
}
TIP
当会话属性可能不存在时,使用 required = false
参数,并将参数类型设为可空类型。
实际应用场景 🎯
场景1:用户认证与授权
kotlin
@RestController
class OrderController {
@PostMapping("/orders")
fun createOrder(
@RequestBody orderRequest: OrderRequest,
@SessionAttribute user: User
): Mono<ResponseEntity<Order>> {
// 验证用户权限
if (!user.hasPermission("CREATE_ORDER")) {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build())
}
return orderService.createOrder(orderRequest, user.id)
.map { order -> ResponseEntity.ok(order) }
}
}
场景2:多步骤表单处理
kotlin
@RestController
class RegistrationController {
@PostMapping("/registration/step2")
fun processStep2(
@RequestBody step2Data: RegistrationStep2,
@SessionAttribute registrationData: RegistrationData
): Mono<ResponseEntity<String>> {
// 合并步骤1和步骤2的数据
registrationData.apply {
address = step2Data.address
phoneNumber = step2Data.phoneNumber
}
return registrationService.validateStep2(registrationData)
.map { isValid ->
if (isValid) {
ResponseEntity.ok("Step 2 completed")
} else {
ResponseEntity.badRequest().body("Invalid data")
}
}
}
}
场景3:购物车功能
kotlin
@RestController
class CartController {
@PostMapping("/cart/add")
fun addToCart(
@RequestBody item: CartItem,
@SessionAttribute(required = false) cart: ShoppingCart?
): Mono<ResponseEntity<ShoppingCart>> {
val currentCart = cart ?: ShoppingCart()
currentCart.addItem(item)
return cartService.updateCart(currentCart)
.map { updatedCart -> ResponseEntity.ok(updatedCart) }
}
}
与其他会话管理方式的对比 📊
@SessionAttribute vs WebSession
kotlin
@GetMapping("/user-info")
fun getUserInfo(@SessionAttribute user: User): String {
return "User: ${user.name}, Email: ${user.email}"
}
kotlin
@GetMapping("/user-info")
fun getUserInfo(exchange: ServerWebExchange): Mono<String> {
return exchange.session.map { session ->
val user = session.getAttribute("user") as? User
if (user != null) {
"User: ${user.name}, Email: ${user.email}"
} else {
"User not found"
}
}
}
@SessionAttribute vs @SessionAttributes
IMPORTANT
这两个注解用途不同:
@SessionAttribute
:用于访问已存在的会话属性@SessionAttributes
:用于声明哪些模型属性应该存储在会话中
kotlin
// @SessionAttributes 示例
@Controller
@SessionAttributes("user")
class UserFormController {
@ModelAttribute("user")
fun createUser(): User = User()
@PostMapping("/update-profile")
fun updateProfile(@ModelAttribute user: User): String {
// user 会自动从会话中获取并更新
userService.update(user)
return "redirect:/profile"
}
}
异常处理与最佳实践 ⚠️
处理会话属性不存在的情况
kotlin
@RestController
class SecureController {
@GetMapping("/secure-data")
fun getSecureData(@SessionAttribute(required = false) user: User?): ResponseEntity<Any> {
return when {
user == null -> {
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Please login first")
}
!user.isActive -> {
ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Account is inactive")
}
else -> {
ResponseEntity.ok(secureService.getData(user.id))
}
}
}
}
全局异常处理
kotlin
@ControllerAdvice
class SessionExceptionHandler {
@ExceptionHandler(SessionAttributeRequiredException::class)
fun handleMissingSessionAttribute(ex: SessionAttributeRequiredException): ResponseEntity<String> {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Session expired or invalid. Please login again.")
}
}
完整的实战示例 🛠️
让我们通过一个完整的电商应用示例来展示 @SessionAttribute
的实际应用:
完整的电商购物车示例
kotlin
// 数据模型
data class User(
val id: Long,
val name: String,
val email: String,
val role: UserRole
)
data class ShoppingCart(
val items: MutableList<CartItem> = mutableListOf(),
var totalAmount: BigDecimal = BigDecimal.ZERO
) {
fun addItem(item: CartItem) {
items.add(item)
recalculateTotal()
}
fun removeItem(productId: Long) {
items.removeIf { it.productId == productId }
recalculateTotal()
}
private fun recalculateTotal() {
totalAmount = items.sumOf { it.price * it.quantity.toBigDecimal() }
}
}
data class CartItem(
val productId: Long,
val productName: String,
val price: BigDecimal,
val quantity: Int
)
// 控制器
@RestController
@RequestMapping("/api")
class ECommerceController(
private val productService: ProductService,
private val orderService: OrderService
) {
@PostMapping("/cart/add")
fun addToCart(
@RequestBody request: AddToCartRequest,
@SessionAttribute(required = false) cart: ShoppingCart?,
exchange: ServerWebExchange
): Mono<ResponseEntity<ShoppingCart>> {
val currentCart = cart ?: ShoppingCart()
return productService.getProduct(request.productId)
.map { product ->
val cartItem = CartItem(
productId = product.id,
productName = product.name,
price = product.price,
quantity = request.quantity
)
currentCart.addItem(cartItem)
// 更新会话中的购物车
exchange.session.subscribe { session ->
session.attributes["cart"] = currentCart
}
ResponseEntity.ok(currentCart)
}
.onErrorReturn(ResponseEntity.badRequest().build())
}
@DeleteMapping("/cart/remove/{productId}")
fun removeFromCart(
@PathVariable productId: Long,
@SessionAttribute cart: ShoppingCart,
exchange: ServerWebExchange
): ResponseEntity<ShoppingCart> {
cart.removeItem(productId)
// 更新会话
exchange.session.subscribe { session ->
session.attributes["cart"] = cart
}
return ResponseEntity.ok(cart)
}
@PostMapping("/checkout")
fun checkout(
@SessionAttribute user: User,
@SessionAttribute cart: ShoppingCart,
exchange: ServerWebExchange
): Mono<ResponseEntity<Order>> {
if (cart.items.isEmpty()) {
return Mono.just(ResponseEntity.badRequest().build())
}
return orderService.createOrder(user.id, cart)
.doOnSuccess {
// 清空购物车
exchange.session.subscribe { session ->
session.attributes.remove("cart")
}
}
.map { order -> ResponseEntity.ok(order) }
}
@GetMapping("/cart")
fun viewCart(
@SessionAttribute(required = false) cart: ShoppingCart?
): ResponseEntity<Any> {
return if (cart?.items?.isNotEmpty() == true) {
ResponseEntity.ok(cart)
} else {
ResponseEntity.ok(mapOf("message" to "Your cart is empty"))
}
}
}
// 请求模型
data class AddToCartRequest(
val productId: Long,
val quantity: Int
)
时序图:会话属性的生命周期 📈
注意事项与限制 ⚠️
WARNING
以下情况需要特别注意:
- 会话属性必须预先存在:
@SessionAttribute
只能访问已存在的属性,不能创建新的会话属性 - 类型安全:确保会话中存储的对象类型与参数类型匹配
- 内存使用:避免在会话中存储大量数据,这会影响应用性能
CAUTION
在集群环境中,确保会话数据能够在不同节点间正确同步。
总结 📋
@SessionAttribute
是 Spring WebFlux 中一个简单而强大的注解,它:
✅ 简化了会话属性访问:无需手动从会话中获取属性
✅ 提供类型安全:编译时类型检查,减少运行时错误
✅ 增强代码可读性:声明式的方式更加清晰
✅ 支持可选属性:通过 required = false
处理可能不存在的属性
通过合理使用 @SessionAttribute
,我们可以构建更加健壮和易维护的 Web 应用程序。记住,它最适合用于访问由其他组件(如过滤器、拦截器)管理的会话属性,而不是在控制器内部创建和管理会话数据。
TIP
在实际项目中,建议结合 Spring Security 或自定义的认证过滤器来管理用户会话,然后在控制器中使用 @SessionAttribute
来访问用户信息。