Skip to content

Spring WebFlux 控制器方法参数详解 🚀

概述

在 Spring WebFlux 的响应式编程世界中,控制器方法参数是我们与 HTTP 请求进行交互的重要桥梁。它们就像是一把把"钥匙",帮助我们打开请求数据的"宝箱",获取我们需要的信息来处理业务逻辑。

NOTE

Spring WebFlux 支持丰富的方法参数类型,每种参数都有其特定的用途和适用场景。理解这些参数类型是掌握 WebFlux 开发的基础。

为什么需要这么多参数类型? 🤔

想象一下,如果我们只能用一种方式获取请求数据,就像只有一把万能钥匙去开所有不同的锁。虽然理论上可行,但实际使用中会非常不便:

  • 效率低下:需要手动解析各种数据格式
  • 代码冗余:重复的数据提取逻辑
  • 类型不安全:缺乏编译时检查
  • 可读性差:意图不明确

Spring WebFlux 通过提供多样化的方法参数类型,让我们能够:

  • 精确表达意图:一眼就能看出方法需要什么数据
  • 自动类型转换:框架自动处理数据转换
  • 减少样板代码:专注业务逻辑而非数据提取
  • 类型安全:编译时就能发现问题

核心概念:响应式类型支持

IMPORTANT

WebFlux 对需要阻塞 I/O 的参数(如读取请求体)支持响应式类型(Reactor、RxJava 等),这是与传统 Spring MVC 的重要区别。

方法参数分类详解

1. 基础请求对象参数 🔧

这些参数提供对底层 HTTP 请求和响应对象的直接访问:

kotlin
@RestController
class BasicController {
    
    @GetMapping("/exchange")
    fun handleWithExchange(exchange: ServerWebExchange): Mono<String> {
        // 获取完整的请求-响应交换对象
        val request = exchange.request
        val response = exchange.response
        val session = exchange.session
        
        return Mono.just("Method: ${request.method}, Path: ${request.path}")
    }
    
    @GetMapping("/request-response")
    fun handleRequestResponse(
        request: ServerHttpRequest, 
        response: ServerHttpResponse
    ): Mono<String> {
        // 直接访问请求和响应对象
        response.headers.add("Custom-Header", "WebFlux")
        return Mono.just("Handled by ${request.method}")
    }
}
kotlin
@RestController
class SessionController {
    
    @GetMapping("/session")
    fun handleSession(session: WebSession): Mono<String> { 
        // 访问会话,不会强制创建新会话
        return session.attributes["user"]?.let {
            Mono.just("Welcome back, $it")
        } ?: Mono.just("Hello, anonymous")
    }
    
    @GetMapping("/user")
    fun getCurrentUser(principal: Principal): Mono<String> { 
        // 获取当前认证用户,支持响应式类型
        return Mono.just("Current user: ${principal.name}")
    }
}

TIP

ServerWebExchange 是最全面的参数类型,包含了请求、响应、会话等所有信息。当你需要访问多种数据时,使用它可以避免声明多个参数。

2. 请求元数据参数 📋

这些参数帮助我们获取请求的基本信息:

kotlin
@RestController
class MetadataController {
    
    @RequestMapping("/info")
    fun getRequestInfo(
        method: HttpMethod, 
        locale: Locale, 
        timeZone: TimeZone, 
        zoneId: ZoneId
    ): Mono<Map<String, Any>> {
        
        return Mono.just(mapOf(
            "method" to method.name,
            "locale" to locale.toString(),
            "timeZone" to timeZone.id,
            "zoneId" to zoneId.toString()
        ))
    }
}

3. 路径和查询参数 🛣️

这是我们最常用的参数类型,用于获取 URL 中的数据:

kotlin
@RestController
@RequestMapping("/users")
class UserController {
    
    @GetMapping("/{userId}/posts/{postId}")
    fun getUserPost(
        @PathVariable userId: Long, 
        @PathVariable postId: String
    ): Mono<String> {
        return Mono.just("User $userId, Post $postId")
    }
    
    // 矩阵变量示例:/users/123;role=admin;dept=IT
    @GetMapping("/{userId}")
    fun getUserWithMatrix(
        @PathVariable userId: Long,
        @MatrixVariable(required = false) role: String?, 
        @MatrixVariable(required = false) dept: String? 
    ): Mono<String> {
        return Mono.just("User $userId, Role: $role, Dept: $dept")
    }
}
kotlin
@RestController
class SearchController {
    
    @GetMapping("/search")
    fun search(
        @RequestParam query: String, 
        @RequestParam(defaultValue = "0") page: Int, 
        @RequestParam(defaultValue = "10") size: Int, 
        @RequestParam(required = false) category: String? 
    ): Mono<String> {
        return Mono.just("Search: $query, Page: $page, Size: $size, Category: $category")
    }
    
    // Optional 支持
    @GetMapping("/optional-search")
    fun optionalSearch(
        @RequestParam query: String,
        @RequestParam category: Optional<String> 
    ): Mono<String> {
        val categoryText = category.orElse("all")
        return Mono.just("Search: $query in $categoryText")
    }
}

WARNING

当使用 @RequestParam 时,如果参数是必需的但请求中没有提供,会抛出异常。使用 required = falseOptional 来处理可选参数。

kotlin
@RestController
class HeaderController {
    
    @GetMapping("/headers")
    fun handleHeaders(
        @RequestHeader("User-Agent") userAgent: String, 
        @RequestHeader(value = "X-Custom-Header", required = false) customHeader: String?, 
        @CookieValue(value = "sessionId", required = false) sessionId: String? 
    ): Mono<Map<String, String?>> {
        
        return Mono.just(mapOf(
            "userAgent" to userAgent,
            "customHeader" to customHeader,
            "sessionId" to sessionId
        ))
    }
}

5. 请求体处理 📦

这是 WebFlux 中最重要的特性之一,支持响应式类型:

kotlin
data class User(
    val name: String,
    val email: String,
    val age: Int
)

@RestController
class BodyController {
    
    @PostMapping("/users")
    fun createUser(@RequestBody user: Mono<User>): Mono<String> { 
        return user.map { "Created user: ${it.name}" }
    }
    
    @PostMapping("/users/sync")
    fun createUserSync(@RequestBody user: User): Mono<String> { 
        // 非响应式方式,框架会自动处理
        return Mono.just("Created user: ${user.name}")
    }
}
kotlin
@RestController
class EntityController {
    
    @PostMapping("/upload")
    fun handleUpload(
        entity: HttpEntity<String> 
    ): Mono<String> {
        val headers = entity.headers
        val body = entity.body
        
        return Mono.just("Received ${body?.length ?: 0} characters")
    }
}

NOTE

使用 Mono<T>Flux<T> 包装请求体参数可以实现真正的非阻塞处理,这是 WebFlux 的核心优势。

6. 文件上传处理 📁

kotlin
@RestController
class FileController {
    
    @PostMapping("/upload", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun uploadFile(
        @RequestPart("file") filePart: Mono<FilePart>, 
        @RequestPart("metadata") metadata: String
    ): Mono<String> {
        
        return filePart
            .flatMap { part ->
                // 响应式文件处理
                val filename = part.filename()
                part.transferTo(Paths.get("/tmp/$filename"))
                    .then(Mono.just("Uploaded: $filename, Metadata: $metadata"))
            }
    }
}

7. 模型和数据绑定 🔗

kotlin
data class UserForm(
    var name: String = "",
    var email: String = "",
    var age: Int = 0
)

@Controller
class FormController {
    
    @GetMapping("/form")
    fun showForm(model: Model): String { 
        model.addAttribute("user", UserForm())
        return "user-form"
    }
    
    @PostMapping("/form")
    fun processForm(
        @ModelAttribute user: UserForm, 
        bindingResult: BindingResult, 
        model: Model
    ): String {
        
        if (bindingResult.hasErrors()) {
            return "user-form"
        }
        
        model.addAttribute("message", "User ${user.name} created successfully")
        return "success"
    }
}

8. 属性访问参数 🏷️

kotlin
@RestController
class AttributeController {
    
    @GetMapping("/session-attr")
    fun getSessionAttribute(
        @SessionAttribute(required = false) username: String? 
    ): Mono<String> {
        return Mono.just("Session username: ${username ?: "not set"}")
    }
    
    @GetMapping("/request-attr")
    fun getRequestAttribute(
        @RequestAttribute(required = false) requestId: String? 
    ): Mono<String> {
        return Mono.just("Request ID: ${requestId ?: "not set"}")
    }
}

实际应用场景 🎯

让我们看一个综合性的实际应用示例:

完整的 RESTful API 示例
kotlin
data class Product(
    val id: Long? = null,
    val name: String,
    val price: BigDecimal,
    val category: String,
    val description: String? = null
)

data class ProductSearchRequest(
    val query: String? = null,
    val category: String? = null,
    val minPrice: BigDecimal? = null,
    val maxPrice: BigDecimal? = null,
    val page: Int = 0,
    val size: Int = 10
)

@RestController
@RequestMapping("/api/products")
class ProductController {
    
    // 获取单个产品
    @GetMapping("/{id}")
    fun getProduct(
        @PathVariable id: Long, 
        @RequestHeader(value = "Accept-Language", required = false) language: String? 
    ): Mono<Product> {
        // 根据语言返回本地化的产品信息
        return productService.findById(id, language)
    }
    
    // 搜索产品
    @GetMapping
    fun searchProducts(
        @RequestParam(required = false) query: String?, 
        @RequestParam(required = false) category: String?, 
        @RequestParam(required = false) minPrice: BigDecimal?, 
        @RequestParam(required = false) maxPrice: BigDecimal?, 
        @RequestParam(defaultValue = "0") page: Int, 
        @RequestParam(defaultValue = "10") size: Int, 
        principal: Principal? 
    ): Mono<Page<Product>> {
        
        val searchRequest = ProductSearchRequest(
            query, category, minPrice, maxPrice, page, size
        )
        
        // 根据用户身份返回个性化结果
        return productService.search(searchRequest, principal?.name)
    }
    
    // 创建产品
    @PostMapping
    fun createProduct(
        @RequestBody productMono: Mono<Product>, 
        @RequestHeader("Content-Type") contentType: String, 
        principal: Principal
    ): Mono<Product> {
        
        return productMono
            .doOnNext { product -> 
                // 验证请求体
                require(product.name.isNotBlank()) { "Product name is required" }
                require(product.price > BigDecimal.ZERO) { "Price must be positive" }
            }
            .flatMap { product -> 
                productService.create(product, principal.name)
            }
    }
    
    // 更新产品
    @PutMapping("/{id}")
    fun updateProduct(
        @PathVariable id: Long, 
        @RequestBody productMono: Mono<Product>, 
        @RequestHeader(value = "If-Match", required = false) etag: String?, 
        principal: Principal
    ): Mono<Product> {
        
        return productMono
            .flatMap { product ->
                productService.update(id, product, etag, principal.name)
            }
    }
    
    // 上传产品图片
    @PostMapping("/{id}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun uploadProductImage(
        @PathVariable id: Long, 
        @RequestPart("image") imagePart: Mono<FilePart>, 
        @RequestPart(value = "alt", required = false) altText: String?, 
        principal: Principal
    ): Mono<String> {
        
        return imagePart
            .flatMap { part ->
                // 验证文件类型
                val contentType = part.headers().contentType
                if (contentType?.type != "image") {
                    return@flatMap Mono.error<String>(
                        IllegalArgumentException("Only image files are allowed")
                    )
                }
                
                productService.uploadImage(id, part, altText, principal.name)
            }
    }
}

最佳实践建议 💡

1. 参数选择原则

TIP

明确性原则:选择最能表达你意图的参数类型

  • 需要路径中的值?使用 @PathVariable
  • 需要查询参数?使用 @RequestParam
  • 需要请求体?使用 @RequestBody

2. 响应式类型使用

kotlin
// ✅ 推荐:对于可能阻塞的操作使用响应式类型
@PostMapping("/users")
fun createUser(@RequestBody userMono: Mono<User>): Mono<User> {
    return userMono
        .flatMap { user -> userService.save(user) }
}

// ❌ 不推荐:同步方式会阻塞线程
@PostMapping("/users-sync")
fun createUserSync(@RequestBody user: User): Mono<User> {
    return userService.save(user) // 可能会阻塞
}

3. 错误处理

kotlin
@RestController
class SafeController {
    
    @GetMapping("/users/{id}")
    fun getUser(
        @PathVariable id: Long,
        @RequestParam(required = false) includeDetails: Boolean = false
    ): Mono<User> {
        
        return userService.findById(id)
            .switchIfEmpty(
                Mono.error(UserNotFoundException("User $id not found")) 
            )
            .flatMap { user ->
                if (includeDetails) {
                    userService.loadDetails(user)
                } else {
                    Mono.just(user)
                }
            }
    }
}

4. 参数验证

kotlin
data class CreateUserRequest(
    @field:NotBlank
    val name: String,
    
    @field:Email
    val email: String,
    
    @field:Min(18)
    val age: Int
)

@PostMapping("/users")
fun createUser(
    @Valid @RequestBody request: Mono<CreateUserRequest> 
): Mono<User> {
    return request.flatMap { userService.create(it) }
}

总结 📝

Spring WebFlux 的方法参数系统为我们提供了强大而灵活的请求处理能力:

  • 🎯 类型丰富:覆盖了 Web 开发的各种需求
  • 🚀 响应式支持:真正的非阻塞处理
  • 🔧 自动转换:减少样板代码
  • 💡 意图明确:代码即文档

IMPORTANT

掌握这些参数类型不仅能提高开发效率,更重要的是能帮助你构建高性能的响应式 Web 应用。记住,选择合适的参数类型就像选择合适的工具一样重要!

通过合理使用这些方法参数,你可以构建出既高效又易维护的 WebFlux 应用程序。在实际开发中,建议根据具体需求选择最合适的参数类型,并充分利用响应式编程的优势。 🎉