Appearance
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 = false
或 Optional
来处理可选参数。
4. 请求头和 Cookie 📨
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 应用程序。在实际开发中,建议根据具体需求选择最合适的参数类型,并充分利用响应式编程的优势。 🎉