Skip to content

Spring WebMvc.fn 函数式端点:告别注解,拥抱函数式编程 🚀

概述:为什么需要函数式端点?

在传统的 Spring MVC 开发中,我们习惯于使用 @Controller@RequestMapping 等注解来构建 Web 应用。但你是否想过,如果能用纯函数的方式来处理 HTTP 请求会是什么样子?

IMPORTANT

Spring WebMvc.fn 提供了一种全新的函数式编程模型,让你可以用函数来路由和处理请求,而不是依赖注解。这种方式具有更好的类型安全性、更清晰的控制流,以及更灵活的组合能力。

传统方式 vs 函数式方式

kotlin
@RestController
@RequestMapping("/person")
class PersonController(private val repository: PersonRepository) {
    
    @GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun getPerson(@PathVariable id: Int): Person? {
        return repository.getPerson(id)
    }
    
    @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
    fun listPeople(): List<Person> {
        return repository.allPeople()
    }
    
    @PostMapping
    fun createPerson(@RequestBody person: Person) {
        repository.savePerson(person)
    }
}
kotlin
class PersonHandler(private val repository: PersonRepository) {
    
    fun getPerson(request: ServerRequest): ServerResponse {
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { 
            ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(it) 
        } ?: ServerResponse.notFound().build()
    }
    
    fun listPeople(request: ServerRequest): ServerResponse {
        val people = repository.allPeople()
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(people)
    }
    
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        repository.savePerson(person)
        return ServerResponse.ok().build()
    }
}

// 路由配置
val route = router {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
        }
        POST(handler::createPerson)
    }
}

核心概念解析

HandlerFunction:处理函数的核心

HandlerFunction 是函数式端点的核心概念,它就像传统 Controller 方法的函数版本。

NOTE

HandlerFunction 接受一个 ServerRequest 参数,返回一个 ServerResponse。这种设计让函数具有了完整的输入输出类型信息,编译器可以提供更好的类型检查。

ServerRequest:不可变的请求对象

ServerRequest 提供了访问 HTTP 请求的所有信息,包括方法、URI、头部、查询参数和请求体。

kotlin
// 获取路径变量
val id = request.pathVariable("id")

// 获取查询参数
val params = request.params()
val name = request.param("name").orElse("默认值")

// 获取请求头
val contentType = request.headers().contentType()

// 解析请求体为字符串
val bodyString = request.body<String>()

// 解析请求体为对象
val person = request.body<Person>()

// 解析请求体为列表
val people = request.body<List<Person>>()

ServerResponse:构建器模式的响应

ServerResponse 使用构建器模式来创建 HTTP 响应,提供了流畅的 API。

kotlin
// 返回 JSON 响应
ServerResponse.ok()
    .contentType(MediaType.APPLICATION_JSON)
    .body(person)

// 返回创建成功响应
ServerResponse.created(location).build()

// 返回错误响应
ServerResponse.badRequest()
    .body("参数验证失败")

// 返回异步响应
val futurePerson = CompletableFuture.supplyAsync { 
    repository.getPerson(id) 
}
ServerResponse.ok()
    .contentType(MediaType.APPLICATION_JSON)
    .body(futurePerson)

RouterFunction:路由的艺术

RouterFunction 负责将请求路由到对应的处理函数,它就像传统的 @RequestMapping 注解,但提供了更强大的组合能力。

基础路由配置

kotlin
val handler = PersonHandler(repository)

val route = router {
    // GET /person/{id} - 获取单个用户
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    
    // GET /person - 获取用户列表  
    GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    
    // POST /person - 创建用户
    POST("/person", handler::createPerson)
    
    // PUT /person/{id} - 更新用户
    PUT("/person/{id}", handler::updatePerson)
    
    // DELETE /person/{id} - 删除用户
    DELETE("/person/{id}", handler::deletePerson)
}

嵌套路由:消除重复

当多个路由共享相同的路径前缀或条件时,可以使用嵌套路由来减少重复。

kotlin
val route = router {
    "/api/v1".nest {  
        "/person".nest {  
            accept(APPLICATION_JSON).nest {  
                GET("/{id}", handler::getPerson)
                GET("", handler::listPeople)
            }
            POST(handler::createPerson)
        }
        
        "/order".nest {
            GET("/{id}", orderHandler::getOrder)
            GET("", orderHandler::listOrders)
            POST(orderHandler::createOrder)
        }
    }
}

TIP

嵌套路由不仅可以基于路径,还可以基于任何谓词条件,比如请求头、内容类型等。这提供了极大的灵活性。

路由组合

可以将多个 RouterFunction 组合在一起:

kotlin
val userRoutes = router {
    "/user".nest {
        GET("/{id}", userHandler::getUser)
        POST("", userHandler::createUser)
    }
}

val orderRoutes = router {
    "/order".nest {
        GET("/{id}", orderHandler::getOrder)
        POST("", orderHandler::createOrder)
    }
}

// 组合多个路由
val allRoutes = userRoutes.and(orderRoutes)  

// 或者在构建时组合
val combinedRoutes = router {
    add(userRoutes)  
    add(orderRoutes)  
    
    // 添加额外的路由
    GET("/health", healthHandler::check)
}

实际业务场景示例

让我们通过一个完整的用户管理系统来看看函数式端点在实际项目中的应用。

数据模型和仓库

kotlin
data class Person(
    val id: Int? = null,
    val name: String,
    val email: String,
    val age: Int
)

interface PersonRepository {
    fun getPerson(id: Int): Person?
    fun allPeople(): List<Person>
    fun savePerson(person: Person): Person
    fun updatePerson(id: Int, person: Person): Person?
    fun deletePerson(id: Int): Boolean
}

处理器类

kotlin
@Component
class PersonHandler(private val repository: PersonRepository) {
    
    fun getPerson(request: ServerRequest): ServerResponse {
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { person ->
            ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(person)
        } ?: ServerResponse.notFound().build()
    }
    
    fun listPeople(request: ServerRequest): ServerResponse {
        // 支持分页查询
        val page = request.param("page").map { it.toInt() }.orElse(0)
        val size = request.param("size").map { it.toInt() }.orElse(10)
        
        val people = repository.allPeople()
            .drop(page * size)
            .take(size)
            
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(people)
    }
    
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        val savedPerson = repository.savePerson(person)
        
        return ServerResponse.created(
            URI.create("/person/${savedPerson.id}")
        ).contentType(MediaType.APPLICATION_JSON)
         .body(savedPerson)
    }
    
    fun updatePerson(request: ServerRequest): ServerResponse {
        val personId = request.pathVariable("id").toInt()
        val person = request.body<Person>()
        
        return repository.updatePerson(personId, person)?.let { updated ->
            ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(updated)
        } ?: ServerResponse.notFound().build()
    }
    
    fun deletePerson(request: ServerRequest): ServerResponse {
        val personId = request.pathVariable("id").toInt()
        return if (repository.deletePerson(personId)) {
            ServerResponse.noContent().build()
        } else {
            ServerResponse.notFound().build()
        }
    }
}

路由配置

kotlin
@Configuration
class RouterConfig {
    
    @Bean
    fun personRoutes(handler: PersonHandler): RouterFunction<ServerResponse> {
        return router {
            "/api/v1/person".nest {
                accept(APPLICATION_JSON).nest {
                    GET("/{id}", handler::getPerson)
                    GET("", handler::listPeople)
                }
                contentType(APPLICATION_JSON).nest {
                    POST("", handler::createPerson)
                    PUT("/{id}", handler::updatePerson)
                }
                DELETE("/{id}", handler::deletePerson)
            }
        }
    }
}

数据验证

函数式端点同样支持数据验证,可以使用 Spring 的验证机制。

kotlin
@Component
class PersonHandler(
    private val repository: PersonRepository,
    private val validator: Validator
) {
    
    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        
        // 验证数据
        validate(person)  
        
        val savedPerson = repository.savePerson(person)
        return ServerResponse.created(URI.create("/person/${savedPerson.id}"))
            .contentType(MediaType.APPLICATION_JSON)
            .body(savedPerson)
    }
    
    private fun validate(person: Person) {  
        val errors = BeanPropertyBindingResult(person, "person")
        validator.validate(person, errors)
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString())  
        }
    }
}

过滤器:横切关注点的处理

函数式端点提供了强大的过滤器机制来处理横切关注点,如日志记录、安全检查、CORS 等。

请求/响应过滤器

kotlin
val route = router {
    "/api".nest {
        // 请求前过滤器 - 添加请求头
        before { request ->
            ServerRequest.from(request)
                .header("X-Request-Time", Instant.now().toString())
                .build()
        }
        
        // 响应后过滤器 - 记录日志
        after { request, response ->
            logger.info("请求: ${request.methodName()} ${request.path()} - 响应: ${response.statusCode()}")
            response
        }
        
        GET("/person/{id}", handler::getPerson)
        POST("/person", handler::createPerson)
    }
}

安全过滤器

kotlin
class SecurityManager {
    fun allowAccessTo(path: String): Boolean {
        // 实现安全检查逻辑
        return !path.contains("/admin") || hasAdminRole()
    }
    
    private fun hasAdminRole(): Boolean {
        // 检查当前用户是否有管理员权限
        return true // 简化实现
    }
}

@Bean
fun secureRoutes(
    handler: PersonHandler,
    securityManager: SecurityManager
): RouterFunction<ServerResponse> {
    return router {
        "/api".nest {
            // 安全过滤器
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next.handle(request)  
                } else {
                    ServerResponse.status(HttpStatus.UNAUTHORIZED).build()  
                }
            }
            
            GET("/person/{id}", handler::getPerson)
            POST("/person", handler::createPerson)
            DELETE("/admin/person/{id}", handler::deletePerson)
        }
    }
}

静态资源服务

函数式端点也支持静态资源的服务,这对于单页应用(SPA)特别有用。

kotlin
@Bean
fun staticResourceRoutes(): RouterFunction<ServerResponse> {
    return router {
        // 服务静态资源
        resources("/static/**", ClassPathResource("static/"))  
        
        // SPA 路由回退
        val index = ClassPathResource("static/index.html")
        val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
        val spaPredicate = !(path("/api/**") or path("/error") or 
                           pathExtension(extensions::contains))
        resource(spaPredicate, index)  
    }
}

服务器发送事件(SSE)

函数式端点对 SSE 提供了优雅的支持:

kotlin
@Component
class EventHandler {
    
    fun streamEvents(request: ServerRequest): ServerResponse {
        return ServerResponse.sse { sseBuilder ->
            // 在其他线程中发送事件
            Thread {
                repeat(10) { i ->
                    sseBuilder.send("事件 $i")  
                    Thread.sleep(1000)
                }
                sseBuilder.complete()  
            }.start()
        }
    }
    
    fun streamJsonEvents(request: ServerRequest): ServerResponse {
        return ServerResponse.sse { sseBuilder ->
            Thread {
                repeat(5) { i ->
                    val event = mapOf("id" to i, "message" to "Hello $i")
                    sseBuilder.id(i.toString())  
                        .event("custom-event")  
                        .data(event)  
                    Thread.sleep(2000)
                }
                sseBuilder.complete()
            }.start()
        }
    }
}

配置和启动

Spring Boot 配置

kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
    
    @Bean
    fun mainRoutes(
        personHandler: PersonHandler,
        eventHandler: EventHandler
    ): RouterFunction<ServerResponse> {
        return router {
            "/api/v1".nest {
                "/person".nest {
                    accept(APPLICATION_JSON).nest {
                        GET("/{id}", personHandler::getPerson)
                        GET("", personHandler::listPeople)
                    }
                    contentType(APPLICATION_JSON).nest {
                        POST("", personHandler::createPerson)
                        PUT("/{id}", personHandler::updatePerson)
                    }
                    DELETE("/{id}", personHandler::deletePerson)
                }
                
                GET("/events", eventHandler::streamEvents)
                GET("/json-events", eventHandler::streamJsonEvents)
            }
        }
    }
    
    // 配置消息转换器
    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        converters.add(MappingJackson2HttpMessageConverter())
    }
    
    // 配置 CORS
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
    }
}

函数式端点 vs 注解式端点

特性函数式端点注解式端点
类型安全✅ 编译时类型检查⚠️ 运行时反射
组合能力✅ 函数组合,灵活性高❌ 注解固定,组合困难
测试友好✅ 纯函数,易于单元测试⚠️ 需要 Spring 上下文
性能✅ 无反射,性能更好⚠️ 反射调用有开销
学习曲线⚠️ 需要函数式编程思维✅ 更符合传统习惯
IDE 支持✅ 完整的代码提示和重构✅ 注解支持良好

最佳实践建议

何时选择函数式端点?

  1. 新项目:如果你正在开始一个新项目,函数式端点是很好的选择
  2. 高性能要求:当你需要最佳性能时,避免反射的函数式端点更优
  3. 复杂路由逻辑:当你需要动态路由或复杂的路由组合时
  4. 函数式编程风格:如果团队偏好函数式编程风格

注意事项

  • 函数式端点需要团队对函数式编程有一定了解
  • 调试时堆栈跟踪可能不如注解式直观
  • 某些 Spring 特性(如 @ControllerAdvice)需要用过滤器替代

总结

Spring WebMvc.fn 函数式端点为我们提供了一种全新的 Web 开发方式。它将路由和处理逻辑分离,提供了更好的类型安全性和组合能力。虽然学习曲线稍陡,但一旦掌握,你会发现它带来的灵活性和表达力是传统注解方式难以比拟的。

函数式端点特别适合:

  • 🎯 需要复杂路由逻辑的应用
  • 🚀 对性能有高要求的系统
  • 🔧 需要高度可测试性的代码
  • 🎨 偏好函数式编程风格的团队

选择函数式端点还是注解式端点,最终取决于你的项目需求和团队偏好。两种方式都可以在同一个应用中共存,你可以根据具体场景选择最适合的方式。 🎉