Skip to content

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 的几个核心设计理念:

  1. 声明式编程:通过注解声明需求,而不是命令式地编写获取逻辑
  2. 关注点分离:将会话管理与业务逻辑分离
  3. 类型安全:编译时就能确定类型,避免运行时类型转换错误

基本用法 📝

简单示例

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

以下情况需要特别注意:

  1. 会话属性必须预先存在@SessionAttribute 只能访问已存在的属性,不能创建新的会话属性
  2. 类型安全:确保会话中存储的对象类型与参数类型匹配
  3. 内存使用:避免在会话中存储大量数据,这会影响应用性能

CAUTION

在集群环境中,确保会话数据能够在不同节点间正确同步。

总结 📋

@SessionAttribute 是 Spring WebFlux 中一个简单而强大的注解,它:

简化了会话属性访问:无需手动从会话中获取属性
提供类型安全:编译时类型检查,减少运行时错误
增强代码可读性:声明式的方式更加清晰
支持可选属性:通过 required = false 处理可能不存在的属性

通过合理使用 @SessionAttribute,我们可以构建更加健壮和易维护的 Web 应用程序。记住,它最适合用于访问由其他组件(如过滤器、拦截器)管理的会话属性,而不是在控制器内部创建和管理会话数据。

TIP

在实际项目中,建议结合 Spring Security 或自定义的认证过滤器来管理用户会话,然后在控制器中使用 @SessionAttribute 来访问用户信息。