Skip to content

Spring WebFlux 中的 @ModelAttribute 注解详解 🚀

什么是 @ModelAttribute?

在 Spring WebFlux 的响应式编程世界中,@ModelAttribute 是一个强大的注解,它就像是一个"数据预处理器",帮助我们在处理请求之前准备好所需的数据模型。

NOTE

@ModelAttribute 的核心价值在于将数据准备逻辑与业务处理逻辑分离,让代码更加清晰和可维护。

为什么需要 @ModelAttribute?

传统方式的痛点 😰

想象一下,如果没有 @ModelAttribute,我们可能需要在每个控制器方法中重复相同的数据准备逻辑:

kotlin
@RestController
class UserController(private val userRepository: UserRepository) {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: String): Mono<User> {
        // 每个方法都需要重复这些逻辑
        val user = userRepository.findById(id) 
        // 验证用户是否存在
        // 检查权限
        // 加载相关数据
        return user
    }
    
    @PostMapping("/users/{id}/update")
    fun updateUser(@PathVariable id: String, @RequestBody updateData: UserUpdateRequest): Mono<User> {
        // 又要重复相同的逻辑
        val user = userRepository.findById(id) 
        // 验证用户是否存在
        // 检查权限
        // 加载相关数据
        return userService.update(user, updateData)
    }
}
kotlin
@RestController
class UserController(
    private val userRepository: UserRepository,
    private val userService: UserService
) {
    
    // 统一的数据预处理逻辑
    @ModelAttribute("currentUser") 
    fun loadUser(@PathVariable id: String): Mono<User> { 
        return userRepository.findById(id) 
            .switchIfEmpty(Mono.error(UserNotFoundException("User not found: $id"))) 
    } 
    
    @GetMapping("/users/{id}")
    fun getUser(@ModelAttribute("currentUser") user: User): Mono<User> {
        // 直接使用已经准备好的用户数据
        return Mono.just(user)
    }
    
    @PostMapping("/users/{id}/update")
    fun updateUser(
        @ModelAttribute("currentUser") user: User, 
        @RequestBody updateData: UserUpdateRequest
    ): Mono<User> {
        // 直接使用已经准备好的用户数据,无需重复加载
        return userService.update(user, updateData)
    }
}

@ModelAttribute 的三种使用方式

1. 方法级注解 - 数据预处理器 🔧

这是最常用的方式,用于在控制器方法执行前准备数据:

kotlin
@RestController
class BookController(
    private val bookRepository: BookRepository,
    private val categoryRepository: CategoryRepository
) {
    
    // 为所有请求预加载图书分类数据
    @ModelAttribute("categories") 
    fun loadCategories(): Flux<Category> {
        return categoryRepository.findAll()
            .doOnNext { println("预加载分类: ${it.name}") }
    }
    
    // 根据请求参数预加载特定图书
    @ModelAttribute("book") 
    fun loadBook(@RequestParam(required = false) bookId: String?): Mono<Book> {
        return if (bookId != null) {
            bookRepository.findById(bookId)
                .doOnNext { println("预加载图书: ${it.title}") }
        } else {
            Mono.just(Book()) // 返回空的图书对象用于新建
        }
    }
    
    @GetMapping("/books/edit")
    fun editBook(
        @ModelAttribute("book") book: Book, 
        @ModelAttribute("categories") categories: Flux<Category> 
    ): Mono<BookEditView> {
        // 数据已经准备就绪,直接使用
        return categories.collectList()
            .map { BookEditView(book, it) }
    }
}

2. 参数注解 - 数据绑定器 📋

用于将请求数据绑定到模型对象:

kotlin
data class UserRegistration(
    var username: String = "",
    var email: String = "",
    var age: Int = 0
)

@RestController
class RegistrationController {
    
    @PostMapping("/register")
    fun register(
        @ModelAttribute userRegistration: UserRegistration, 
        bindingResult: BindingResult
    ): Mono<String> {
        // Spring 自动将表单数据绑定到 userRegistration 对象
        if (bindingResult.hasErrors()) {
            return Mono.just("注册失败:数据验证错误")
        }
        
        println("用户名: ${userRegistration.username}")
        println("邮箱: ${userRegistration.email}")
        println("年龄: ${userRegistration.age}")
        
        return Mono.just("注册成功")
    }
}

3. 返回值注解 - 模型属性标记 🏷️

标记方法返回值作为模型属性:

kotlin
@Controller
class ProductController(private val productService: ProductService) {
    
    @GetMapping("/products/{id}")
    @ModelAttribute("product") 
    fun getProduct(@PathVariable id: String): Mono<Product> {
        // 返回值会被添加到模型中,属性名为 "product"
        return productService.findById(id)
    }
    
    // 如果不指定名称,会根据返回类型自动生成名称(如 "product")
    @GetMapping("/products/featured")
    @ModelAttribute
    fun getFeaturedProduct(): Mono<Product> {
        return productService.findFeaturedProduct()
    }
}

WebFlux 中的响应式特性 ⚡

Spring WebFlux 对响应式类型提供了特殊支持:

kotlin
@RestController
class OrderController(
    private val orderRepository: OrderRepository,
    private val customerRepository: CustomerRepository
) {
    
    // 返回响应式类型,WebFlux 会自动处理
    @ModelAttribute("customer") 
    fun loadCustomer(@RequestParam customerId: String): Mono<Customer> {
        return customerRepository.findById(customerId)
            .doOnNext { println("异步加载客户: ${it.name}") }
    }
    
    @ModelAttribute("recentOrders") 
    fun loadRecentOrders(@RequestParam customerId: String): Flux<Order> {
        return orderRepository.findByCustomerIdOrderByCreatedAtDesc(customerId)
            .take(5) // 只取最近5个订单
            .doOnNext { println("加载订单: ${it.id}") }
    }
    
    @PostMapping("/orders")
    fun createOrder(
        @ModelAttribute("customer") customer: Customer, 
        @RequestBody orderRequest: OrderRequest
    ): Mono<Order> {
        // customer 已经从 Mono<Customer> 中解析出来
        println("为客户 ${customer.name} 创建订单")
        
        return orderRepository.save(
            Order(
                customerId = customer.id,
                items = orderRequest.items,
                totalAmount = orderRequest.totalAmount
            )
        )
    }
}

执行时序图

让我们通过时序图了解 @ModelAttribute 的执行流程:

跨控制器共享 - @ControllerAdvice 🌐

使用 @ControllerAdvice 可以在多个控制器间共享 @ModelAttribute 方法:

kotlin
@ControllerAdvice
class GlobalModelAttributes(
    private val configService: ConfigService,
    private val userService: UserService
) {
    
    // 为所有控制器提供系统配置
    @ModelAttribute("systemConfig") 
    fun addSystemConfig(): Mono<SystemConfig> {
        return configService.getSystemConfig()
            .doOnNext { println("全局加载系统配置") }
    }
    
    // 为需要用户信息的控制器提供当前用户
    @ModelAttribute("currentUser") 
    fun addCurrentUser(serverRequest: ServerRequest): Mono<User> {
        return serverRequest.principal()
            .cast(JwtAuthenticationToken::class.java)
            .flatMap { token -> 
                userService.findByUsername(token.name)
            }
            .doOnNext { println("全局加载当前用户: ${it.username}") }
    }
}

// 所有控制器都可以使用全局的模型属性
@RestController
class DashboardController {
    
    @GetMapping("/dashboard")
    fun dashboard(
        @ModelAttribute("currentUser") user: User, 
        @ModelAttribute("systemConfig") config: SystemConfig
    ): Mono<DashboardView> {
        // 直接使用全局提供的数据
        return Mono.just(DashboardView(user, config))
    }
}

实际应用场景 💼

场景1:电商商品详情页

kotlin
@Controller
class ProductDetailController(
    private val productService: ProductService,
    private val reviewService: ReviewService,
    private val recommendationService: RecommendationService
) {
    
    @ModelAttribute("product") 
    fun loadProduct(@PathVariable productId: String): Mono<Product> {
        return productService.findById(productId)
            .switchIfEmpty(Mono.error(ProductNotFoundException()))
    }
    
    @ModelAttribute("reviews") 
    fun loadReviews(@PathVariable productId: String): Flux<Review> {
        return reviewService.findByProductId(productId)
            .take(10) // 只显示前10个评论
    }
    
    @ModelAttribute("recommendations") 
    fun loadRecommendations(@PathVariable productId: String): Flux<Product> {
        return recommendationService.findSimilarProducts(productId)
            .take(5)
    }
    
    @GetMapping("/products/{productId}")
    fun showProductDetail(
        @ModelAttribute("product") product: Product,
        @ModelAttribute("reviews") reviews: Flux<Review>,
        @ModelAttribute("recommendations") recommendations: Flux<Product>
    ): Mono<String> {
        // 所有数据都已准备就绪,可以直接渲染页面
        return Mono.just("product-detail")
    }
}

场景2:用户权限验证

完整的权限验证示例
kotlin
@ControllerAdvice
class SecurityModelAttributes(
    private val userService: UserService,
    private val permissionService: PermissionService
) {
    
    @ModelAttribute("userPermissions")
    fun loadUserPermissions(authentication: Authentication?): Mono<Set<String>> {
        return if (authentication?.isAuthenticated == true) {
            permissionService.getUserPermissions(authentication.name)
                .collectList()
                .map { it.toSet() }
        } else {
            Mono.just(emptySet())
        }
    }
}

@RestController
class AdminController {
    
    @GetMapping("/admin/users")
    fun listUsers(
        @ModelAttribute("userPermissions") permissions: Set<String>
    ): Mono<List<User>> {
        if (!permissions.contains("USER_READ")) {
            return Mono.error(AccessDeniedException("权限不足"))
        }
        
        return userService.findAll().collectList()
    }
    
    @DeleteMapping("/admin/users/{id}")
    fun deleteUser(
        @PathVariable id: String,
        @ModelAttribute("userPermissions") permissions: Set<String>
    ): Mono<Void> {
        if (!permissions.contains("USER_DELETE")) {
            return Mono.error(AccessDeniedException("权限不足"))
        }
        
        return userService.deleteById(id)
    }
}

最佳实践与注意事项 ⚠️

✅ 推荐做法

TIP

  1. 合理使用缓存:对于不经常变化的数据,考虑添加缓存
  2. 异常处理:在 @ModelAttribute 方法中妥善处理异常
  3. 性能考虑:避免在 @ModelAttribute 中执行耗时操作
kotlin
@RestController
class BestPracticeController(
    private val cacheManager: CacheManager,
    private val dataService: DataService
) {
    
    @ModelAttribute("cachedData")
    @Cacheable("commonData") 
    fun loadCachedData(): Mono<CommonData> {
        return dataService.loadCommonData()
            .timeout(Duration.ofSeconds(5)) 
            .onErrorReturn(CommonData.empty()) 
            .doOnNext { println("数据加载完成") }
    }
}

❌ 避免的做法

WARNING

  1. 避免阻塞操作:不要在 @ModelAttribute 方法中使用阻塞调用
  2. 避免过度使用:不要为每个简单数据都创建 @ModelAttribute 方法
  3. 避免循环依赖:确保 @ModelAttribute 方法之间没有循环依赖
kotlin
// ❌ 错误示例
@ModelAttribute("badExample")
fun badExample(): User {
    Thread.sleep(1000) // [!code error] // 阻塞操作
    return userRepository.findById("123").block() // [!code error] // 阻塞调用
}

// ✅ 正确示例
@ModelAttribute("goodExample")
fun goodExample(): Mono<User> {
    return userRepository.findById("123") 
        .timeout(Duration.ofSeconds(2)) 
        .onErrorReturn(User.empty()) 
}

总结 📝

@ModelAttribute 是 Spring WebFlux 中一个强大而灵活的工具,它帮助我们:

  • 🔄 分离关注点:将数据准备逻辑与业务逻辑分离
  • 🚀 提高复用性:避免重复的数据加载代码
  • 支持响应式:完美支持 Mono 和 Flux 等响应式类型
  • 🌐 全局共享:通过 @ControllerAdvice 实现跨控制器数据共享

通过合理使用 @ModelAttribute,我们可以构建更加清晰、可维护和高性能的响应式 Web 应用! 🎉