Appearance
Spring MVC @RequestAttribute
注解详解 🎯
概述
@RequestAttribute
是 Spring MVC 中一个强大而实用的注解,它允许我们在控制器方法中直接访问 HTTP 请求中预先存在的属性。这些属性通常由 Servlet Filter 或 HandlerInterceptor 在请求处理的早期阶段创建和设置。
NOTE
@RequestAttribute
与 @SessionAttribute
类似,但作用域不同:前者针对单个请求,后者针对整个会话。
核心原理与设计哲学 💡
问题背景
在 Web 应用开发中,我们经常遇到这样的场景:
- 认证信息传递:用户登录后,需要在整个请求链中传递用户信息
- 请求预处理:Filter 或 Interceptor 对请求进行预处理,需要将处理结果传递给控制器
- 跨层数据共享:不同组件之间需要共享请求级别的数据
传统解决方案的痛点
kotlin
@GetMapping("/user/profile")
fun getUserProfile(request: HttpServletRequest): String {
// 需要手动从 request 中获取属性
val user = request.getAttribute("currentUser") as? User
?: throw IllegalStateException("用户信息未找到")
// 需要进行类型转换和空值检查
val permissions = request.getAttribute("userPermissions") as? List<String>
?: emptyList()
// 代码冗长,容易出错
return "profile"
}
kotlin
@GetMapping("/user/profile")
fun getUserProfile(
@RequestAttribute currentUser: User,
@RequestAttribute userPermissions: List<String>
): String {
// 直接使用,无需手动获取和转换
// Spring 自动处理类型转换和注入
return "profile"
}
设计哲学
@RequestAttribute
的设计遵循了以下原则:
- 声明式编程:通过注解声明需要的数据,而非命令式地获取
- 类型安全:编译时确定类型,避免运行时类型转换错误
- 关注点分离:控制器专注业务逻辑,数据获取由框架处理
- 代码简洁:减少样板代码,提高开发效率
技术交互流程 🔄
实际应用场景 🚀
场景一:用户认证信息传递
kotlin
@Component
class AuthenticationFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val token = extractToken(request)
if (token != null) {
val user = validateAndGetUser(token)
// 将用户信息设置到请求属性中
request.setAttribute("currentUser", user)
request.setAttribute("userRoles", user.roles)
}
filterChain.doFilter(request, response)
}
private fun extractToken(request: HttpServletRequest): String? {
return request.getHeader("Authorization")?.removePrefix("Bearer ")
}
private fun validateAndGetUser(token: String): User {
// JWT 验证逻辑
return jwtService.parseToken(token)
}
}
kotlin
@RestController
@RequestMapping("/api/user")
class UserController {
@GetMapping("/profile")
fun getProfile(
@RequestAttribute currentUser: User,
@RequestAttribute userRoles: List<String>
): ResponseEntity<UserProfileDto> {
// 直接使用注入的用户信息,无需手动获取
val profile = UserProfileDto(
id = currentUser.id,
username = currentUser.username,
email = currentUser.email,
roles = userRoles
)
return ResponseEntity.ok(profile)
}
@PostMapping("/update")
fun updateProfile(
@RequestAttribute currentUser: User,
@RequestBody updateRequest: UpdateProfileRequest
): ResponseEntity<String> {
// 使用当前用户信息进行更新
userService.updateProfile(currentUser.id, updateRequest)
return ResponseEntity.ok("Profile updated successfully")
}
}
场景二:请求日志和监控
kotlin
@Component
class MonitoringInterceptor : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
val requestId = UUID.randomUUID().toString()
val startTime = System.currentTimeMillis()
// 设置请求追踪信息
request.setAttribute("requestId", requestId)
request.setAttribute("startTime", startTime)
request.setAttribute("clientIp", getClientIp(request))
return true
}
private fun getClientIp(request: HttpServletRequest): String {
return request.getHeader("X-Forwarded-For")
?: request.getHeader("X-Real-IP")
?: request.remoteAddr
}
}
kotlin
@RestController
@RequestMapping("/api/orders")
class OrderController(private val orderService: OrderService) {
@PostMapping
fun createOrder(
@RequestAttribute requestId: String,
@RequestAttribute clientIp: String,
@RequestAttribute currentUser: User,
@RequestBody orderRequest: CreateOrderRequest
): ResponseEntity<OrderDto> {
// 使用请求追踪信息记录日志
logger.info("Creating order - RequestId: $requestId, User: ${currentUser.id}, IP: $clientIp")
val order = orderService.createOrder(
userId = currentUser.id,
request = orderRequest,
metadata = mapOf(
"requestId" to requestId,
"clientIp" to clientIp
)
)
return ResponseEntity.ok(order.toDto())
}
}
场景三:多租户应用
多租户场景完整示例
kotlin
// 租户识别过滤器
@Component
class TenantFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val tenantId = extractTenantId(request)
val tenant = tenantService.getTenant(tenantId)
// 设置租户信息到请求属性
request.setAttribute("currentTenant", tenant)
request.setAttribute("tenantConfig", tenant.config)
try {
// 设置租户上下文
TenantContext.setCurrentTenant(tenant)
filterChain.doFilter(request, response)
} finally {
// 清理租户上下文
TenantContext.clear()
}
}
private fun extractTenantId(request: HttpServletRequest): String {
// 从子域名、请求头或路径中提取租户ID
return request.getHeader("X-Tenant-ID")
?: extractFromSubdomain(request)
?: "default"
}
}
// 控制器中使用租户信息
@RestController
@RequestMapping("/api/products")
class ProductController(private val productService: ProductService) {
@GetMapping
fun getProducts(
@RequestAttribute currentTenant: Tenant,
@RequestAttribute tenantConfig: TenantConfig,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
): ResponseEntity<Page<ProductDto>> {
// 根据租户配置调整查询行为
val products = if (tenantConfig.enableAdvancedSearch) {
productService.getProductsWithAdvancedFilters(
tenantId = currentTenant.id,
page = page,
size = size
)
} else {
productService.getBasicProducts(
tenantId = currentTenant.id,
page = page,
size = size
)
}
return ResponseEntity.ok(products)
}
}
高级特性与最佳实践 ⭐
1. 可选属性处理
kotlin
@GetMapping("/optional-demo")
fun handleOptionalAttribute(
@RequestAttribute(required = false) optionalData: String?,
@RequestAttribute defaultData: String = "default"
): String {
val data = optionalData ?: "未提供可选数据"
return "处理结果: $data, 默认数据: $defaultData"
}
TIP
使用 required = false
可以让属性变为可选,避免属性不存在时抛出异常。
2. 自定义属性名称
kotlin
@GetMapping("/custom-name")
fun handleCustomName(
@RequestAttribute("custom_user_info") userInfo: UserInfo,
@RequestAttribute(name = "request_metadata") metadata: Map<String, Any>
): ResponseEntity<String> {
return ResponseEntity.ok("用户: ${userInfo.name}, 元数据: $metadata")
}
3. 类型转换和验证
kotlin
// 自定义类型转换器
@Component
class StringToUserConverter : Converter<String, User> {
override fun convert(source: String): User {
return objectMapper.readValue(source, User::class.java)
}
}
// 在控制器中使用
@GetMapping("/converted")
fun handleConvertedAttribute(
@RequestAttribute @Valid userJson: User
): ResponseEntity<String> {
return ResponseEntity.ok("转换后的用户: ${userJson.name}")
}
常见陷阱与解决方案 ⚠️
陷阱1:属性不存在
kotlin
// 错误示例
@GetMapping("/risky")
fun riskyMethod(@RequestAttribute nonExistentAttr: String): String {
return nonExistentAttr // 可能抛出异常
}
// 正确示例
@GetMapping("/safe")
fun safeMethod(
@RequestAttribute(required = false) safeAttr: String?
): String {
return safeAttr ?: "默认值"
}
WARNING
如果请求属性不存在且未设置 required = false
,Spring 会抛出 ServletRequestBindingException
。
陷阱2:类型不匹配
kotlin
// 在 Filter 中设置错误类型
request.setAttribute("userId", "123") // String 类型
// 在控制器中期望不同类型
@GetMapping("/type-mismatch")
fun handleTypeMismatch(@RequestAttribute userId: Long): String {
return "用户ID: $userId" // 类型转换失败
}
// 解决方案:确保类型一致或提供转换器
@GetMapping("/type-safe")
fun handleTypeSafe(@RequestAttribute userId: String): String {
val id = userId.toLongOrNull() ?: 0L
return "用户ID: $id"
}
性能考虑 🚀
属性缓存策略
kotlin
@Component
class CachingInterceptor : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
// 避免重复计算,使用缓存
val expensiveData = request.getAttribute("expensiveData")
?: computeExpensiveData().also {
request.setAttribute("expensiveData", it)
}
return true
}
private fun computeExpensiveData(): Any {
// 昂贵的计算操作
return "computed result"
}
}
总结 📝
@RequestAttribute
注解是 Spring MVC 中一个优雅的解决方案,它:
✅ 简化代码:消除了手动获取请求属性的样板代码
✅ 类型安全:编译时确定类型,减少运行时错误
✅ 提高可读性:方法签名清晰表达了所需的依赖
✅ 促进解耦:控制器专注业务逻辑,数据获取由框架处理
IMPORTANT
记住:@RequestAttribute
用于访问请求级别的属性,这些属性通常由 Filter 或 Interceptor 预先设置。它与 @RequestParam
(URL参数)和 @RequestBody
(请求体)有本质区别。
通过合理使用 @RequestAttribute
,我们可以构建更加清晰、可维护的 Web 应用程序! 🎉