Appearance
Spring WebFlux 请求映射:让 HTTP 请求找到正确的处理方法 🎯
什么是请求映射?为什么需要它?
想象一下,你正在经营一家餐厅,客人点菜时需要告诉服务员他们想要什么。服务员需要知道:
- 客人要的是什么菜?(URL路径)
- 是堂食还是外卖?(HTTP方法)
- 有什么特殊要求?(请求参数、头部信息)
在 Spring WebFlux 中,请求映射(Request Mapping) 就像这个"点菜系统",它帮助 Web 应用程序将传入的 HTTP 请求路由到正确的控制器方法进行处理。
IMPORTANT
请求映射是 Web 应用程序的"交通指挥员",它决定了每个 HTTP 请求应该由哪个方法来处理。
@RequestMapping:万能的请求映射注解
基础用法
@RequestMapping
是 Spring 中最基础的请求映射注解,它可以根据多种条件来匹配请求:
kotlin
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// 处理 GET /persons/{id} 请求
return personService.findById(id)
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// 处理 POST /persons 请求
personService.save(person)
}
}
HTTP 方法专用注解
Spring 提供了更简洁的 HTTP 方法专用注解:
kotlin
@RestController
class UserController {
@GetMapping("/users/{id}") // GET 请求
fun getUser(@PathVariable id: Long): User { /* ... */ }
@PostMapping("/users") // POST 请求
fun createUser(@RequestBody user: User): User { /* ... */ }
@PutMapping("/users/{id}") // PUT 请求
fun updateUser(@PathVariable id: Long, @RequestBody user: User): User { /* ... */ }
@DeleteMapping("/users/{id}") // DELETE 请求
fun deleteUser(@PathVariable id: Long) { /* ... */ }
@PatchMapping("/users/{id}") // PATCH 请求
fun patchUser(@PathVariable id: Long, @RequestBody updates: Map<String, Any>): User { /* ... */ }
}
kotlin
@RestController
class UserController {
@RequestMapping("/users/{id}", method = [RequestMethod.GET])
fun getUser(@PathVariable id: Long): User { /* ... */ }
@RequestMapping("/users", method = [RequestMethod.POST])
fun createUser(@RequestBody user: User): User { /* ... */ }
// 其他方法...
}
TIP
使用专用注解(如 @GetMapping
、@PostMapping
)比通用的 @RequestMapping
更清晰、更简洁,也更不容易出错。
URI 模式匹配:灵活的路径规则
通配符和模式
Spring WebFlux 支持多种 URI 匹配模式:
模式 | 描述 | 示例 |
---|---|---|
? | 匹配单个字符 | /pages/t?st.html 匹配 /pages/test.html |
* | 匹配路径段内的零个或多个字符 | /resources/*.png 匹配 /resources/file.png |
** | 匹配零个或多个路径段 | /resources/** 匹配 /resources/images/file.png |
{name} | 路径变量 | /users/{id} 匹配 /users/123 |
{name:regex} | 带正则表达式的路径变量 | /files/{name:[a-z]+} |
{*path} | 匹配剩余所有路径段 | /docs/{*path} |
路径变量的使用
kotlin
@RestController
@RequestMapping("/api/v1")
class ProductController {
// 基础路径变量
@GetMapping("/products/{id}")
fun getProduct(@PathVariable id: Long): Product {
return productService.findById(id)
}
// 多个路径变量
@GetMapping("/categories/{categoryId}/products/{productId}")
fun getProductInCategory(
@PathVariable categoryId: Long,
@PathVariable productId: Long
): Product {
return productService.findByIdAndCategory(productId, categoryId)
}
// 带正则表达式的路径变量
@GetMapping("/files/{filename:[a-zA-Z0-9._-]+\\.{extension:[a-z]+}")
fun downloadFile(
@PathVariable filename: String,
@PathVariable extension: String
): ResponseEntity<ByteArray> {
// 只允许特定格式的文件名和扩展名
return fileService.download("$filename.$extension")
}
// 捕获剩余路径
@GetMapping("/docs/{*path}")
fun getDocumentation(@PathVariable path: String): String {
// path 包含 /docs/ 之后的所有路径
return documentService.getContent(path)
}
}
类级别和方法级别的映射组合
kotlin
@RestController
@RequestMapping("/api/v1/shops/{shopId}")
class ShopProductController {
@GetMapping("/products") // 最终路径: /api/v1/shops/{shopId}/products
fun getShopProducts(@PathVariable shopId: Long): List<Product> {
return productService.findByShopId(shopId)
}
@GetMapping("/products/{productId}") // 最终路径: /api/v1/shops/{shopId}/products/{productId}
fun getShopProduct(
@PathVariable shopId: Long,
@PathVariable productId: Long
): Product {
return productService.findByShopIdAndProductId(shopId, productId)
}
}
内容类型匹配:精确控制请求和响应格式
消费的媒体类型(Consumes)
通过 consumes
属性限制请求的 Content-Type
:
kotlin
@RestController
@RequestMapping("/api/files")
class FileController {
// 只接受 JSON 格式的请求
@PostMapping(value = ["/upload"], consumes = ["application/json"])
fun uploadFileInfo(@RequestBody fileInfo: FileInfo): ResponseEntity<String> {
fileService.saveFileInfo(fileInfo)
return ResponseEntity.ok("File info saved")
}
// 只接受多部分表单数据
@PostMapping(value = ["/upload"], consumes = ["multipart/form-data"])
fun uploadFile(@RequestParam("file") file: MultipartFile): ResponseEntity<String> {
fileService.saveFile(file)
return ResponseEntity.ok("File uploaded")
}
// 拒绝纯文本格式
@PostMapping(value = ["/data"], consumes = ["!text/plain"])
fun processData(@RequestBody data: Any): ResponseEntity<String> {
// 接受除 text/plain 之外的任何格式
return ResponseEntity.ok("Data processed")
}
}
产生的媒体类型(Produces)
通过 produces
属性控制响应的 Content-Type
:
kotlin
@RestController
@RequestMapping("/api/users")
class UserController {
// 返回 JSON 格式
@GetMapping(value = ["/{id}"], produces = ["application/json"])
fun getUserAsJson(@PathVariable id: Long): User {
return userService.findById(id)
}
// 返回 XML 格式
@GetMapping(value = ["/{id}"], produces = ["application/xml"])
fun getUserAsXml(@PathVariable id: Long): User {
return userService.findById(id)
}
// 根据 Accept 头部自动选择格式
@GetMapping(value = ["/{id}"], produces = ["application/json", "application/xml"])
fun getUser(@PathVariable id: Long): User {
return userService.findById(id)
}
}
NOTE
Spring 会根据客户端的 Accept
头部自动选择最合适的响应格式。
参数和头部条件:更精细的匹配控制
查询参数条件
kotlin
@RestController
@RequestMapping("/api/products")
class ProductSearchController {
// 必须包含 version 参数且值为 v2
@GetMapping(params = ["version=v2"])
fun getProductsV2(): List<Product> {
return productService.findAllV2()
}
// 必须包含 featured 参数(任意值)
@GetMapping(params = ["featured"])
fun getFeaturedProducts(): List<Product> {
return productService.findFeatured()
}
// 不能包含 beta 参数
@GetMapping(params = ["!beta"])
fun getStableProducts(): List<Product> {
return productService.findStable()
}
// 组合条件:必须有 category 参数,不能有 debug 参数
@GetMapping(params = ["category", "!debug"])
fun getProductsByCategory(@RequestParam category: String): List<Product> {
return productService.findByCategory(category)
}
}
请求头部条件
kotlin
@RestController
@RequestMapping("/api/content")
class ContentController {
// 必须包含特定的 API 版本头部
@GetMapping(value = ["/data"], headers = ["X-API-Version=1.0"])
fun getDataV1(): ContentData {
return contentService.getDataV1()
}
// 必须是移动端请求
@GetMapping(value = ["/mobile"], headers = ["User-Agent=*Mobile*"])
fun getMobileContent(): MobileContent {
return contentService.getMobileContent()
}
// 必须包含认证头部
@GetMapping(value = ["/secure"], headers = ["Authorization"])
fun getSecureContent(@RequestHeader("Authorization") auth: String): SecureContent {
return contentService.getSecureContent(auth)
}
}
实际应用场景演示
让我们通过一个完整的博客 API 来展示请求映射的实际应用:
完整的博客 API 控制器示例
kotlin
@RestController
@RequestMapping("/api/v1/blogs")
class BlogController(
private val blogService: BlogService,
private val commentService: CommentService
) {
// 获取所有博客(支持分页和搜索)
@GetMapping
fun getAllBlogs(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int,
@RequestParam(required = false) keyword: String?
): Page<Blog> {
return if (keyword != null) {
blogService.searchBlogs(keyword, page, size)
} else {
blogService.getAllBlogs(page, size)
}
}
// 获取特定博客
@GetMapping("/{id}")
fun getBlog(@PathVariable id: Long): ResponseEntity<Blog> {
val blog = blogService.findById(id)
return if (blog != null) {
ResponseEntity.ok(blog)
} else {
ResponseEntity.notFound().build()
}
}
// 创建博客(只接受 JSON)
@PostMapping(consumes = ["application/json"])
@ResponseStatus(HttpStatus.CREATED)
fun createBlog(
@RequestBody @Valid blog: CreateBlogRequest,
@RequestHeader("X-User-ID") userId: Long
): Blog {
return blogService.createBlog(blog, userId)
}
// 更新博客
@PutMapping("/{id}", consumes = ["application/json"])
fun updateBlog(
@PathVariable id: Long,
@RequestBody @Valid blog: UpdateBlogRequest,
@RequestHeader("X-User-ID") userId: Long
): ResponseEntity<Blog> {
return try {
val updatedBlog = blogService.updateBlog(id, blog, userId)
ResponseEntity.ok(updatedBlog)
} catch (e: BlogNotFoundException) {
ResponseEntity.notFound().build()
} catch (e: UnauthorizedException) {
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
}
}
// 删除博客
@DeleteMapping("/{id}")
fun deleteBlog(
@PathVariable id: Long,
@RequestHeader("X-User-ID") userId: Long
): ResponseEntity<Void> {
return try {
blogService.deleteBlog(id, userId)
ResponseEntity.noContent().build()
} catch (e: BlogNotFoundException) {
ResponseEntity.notFound().build()
} catch (e: UnauthorizedException) {
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
}
}
// 获取博客评论
@GetMapping("/{blogId}/comments")
fun getBlogComments(
@PathVariable blogId: Long,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
): Page<Comment> {
return commentService.getCommentsByBlogId(blogId, page, size)
}
// 添加评论
@PostMapping("/{blogId}/comments", consumes = ["application/json"])
@ResponseStatus(HttpStatus.CREATED)
fun addComment(
@PathVariable blogId: Long,
@RequestBody @Valid comment: CreateCommentRequest,
@RequestHeader("X-User-ID") userId: Long
): Comment {
return commentService.addComment(blogId, comment, userId)
}
// 获取草稿博客(需要特殊权限)
@GetMapping(params = ["status=draft"], headers = ["X-Admin-Token"])
fun getDraftBlogs(@RequestHeader("X-Admin-Token") adminToken: String): List<Blog> {
// 验证管理员权限
adminService.validateToken(adminToken)
return blogService.getDraftBlogs()
}
// 发布博客
@PatchMapping("/{id}/publish")
fun publishBlog(
@PathVariable id: Long,
@RequestHeader("X-User-ID") userId: Long
): ResponseEntity<Blog> {
return try {
val publishedBlog = blogService.publishBlog(id, userId)
ResponseEntity.ok(publishedBlog)
} catch (e: BlogNotFoundException) {
ResponseEntity.notFound().build()
} catch (e: UnauthorizedException) {
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
}
}
}
请求映射的工作流程
让我们通过时序图来理解请求映射的工作原理:
高级特性
自定义注解
你可以创建自己的组合注解来简化代码:
kotlin
// 自定义注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@GetMapping
@ResponseBody
annotation class JsonGetMapping(
val value: String = "",
val produces: Array<String> = ["application/json"]
)
// 使用自定义注解
@RestController
class UserController {
@JsonGetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): User {
return userService.findById(id)
}
}
程序化注册
对于动态场景,可以通过编程方式注册映射:
kotlin
@Configuration
class DynamicMappingConfig {
@Autowired
fun configureDynamicMappings(
mapping: RequestMappingHandlerMapping,
handler: DynamicHandler
) {
// 动态创建映射信息
val info = RequestMappingInfo
.paths("/dynamic/{id}")
.methods(RequestMethod.GET)
.produces("application/json")
.build()
// 获取处理方法
val method = DynamicHandler::class.java.getMethod("handleRequest", String::class.java)
// 注册映射
mapping.registerMapping(info, handler, method)
}
}
最佳实践和注意事项
1. 版本控制策略
kotlin
@RestController
@RequestMapping("/api/v1/users")
class UserV1Controller {
// v1 版本的用户 API
}
@RestController
@RequestMapping("/api/v2/users")
class UserV2Controller {
// v2 版本的用户 API
}
kotlin
@RestController
@RequestMapping("/api/users")
class UserController {
@GetMapping(headers = ["X-API-Version=1.0"])
fun getUserV1(@PathVariable id: Long): UserV1 { /* ... */ }
@GetMapping(headers = ["X-API-Version=2.0"])
fun getUserV2(@PathVariable id: Long): UserV2 { /* ... */ }
}
2. 错误处理
kotlin
@RestController
@RequestMapping("/api/products")
class ProductController {
@GetMapping("/{id}")
fun getProduct(@PathVariable id: Long): ResponseEntity<Product> {
return try {
val product = productService.findById(id)
ResponseEntity.ok(product)
} catch (e: ProductNotFoundException) {
ResponseEntity.notFound().build()
} catch (e: Exception) {
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()
}
}
}
3. 性能优化提示
WARNING
避免过于复杂的正则表达式路径变量,这可能影响路由性能。
TIP
将更具体的映射放在更通用的映射之前,Spring 会按照特异性排序,但明确的顺序更清晰。
总结
Spring WebFlux 的请求映射系统为我们提供了强大而灵活的路由机制:
✅ 简单易用:通过注解就能快速定义路由规则
✅ 功能丰富:支持路径变量、通配符、内容类型匹配等
✅ 高度可定制:可以根据参数、头部、HTTP 方法等多种条件进行匹配
✅ 性能优化:内置的模式匹配算法确保高效的路由解析
掌握请求映射是构建 RESTful API 的基础,它让我们能够构建结构清晰、易于维护的 Web 服务。记住,好的 API 设计不仅要功能完整,更要直观易懂! 🚀