Skip to content

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 设计不仅要功能完整,更要直观易懂! 🚀