12.2 定义函数式请求处理程序
Spring MVC 基于注解的编程模型从 Spring 2.5 开始就出现了,并且广受欢迎。不过,它也有一些缺点。
首先,任何基于注解的编程都涉及到注解应该对做什么以及如何做定义上区分。注解本身定义了什么;如何在框架代码的其他地方定义。当涉及到任何类型的定制或扩展时,这会使编程模型复杂化,因为这样的更改需要在注解外部的代码中工作。此外,调试这样的代码是很棘手的,因为不能在注解上设置断点。
另外,随着 Spring 的不断流行,来自其他语言和框架的新开发人员可能会发现基于注解的 Spring MVC(和 WebFlux)与他们已经知道的非常不同了。作为 WebFlux 的替代,Spring 引入了一个新的函数式编程模型来定义响应式 API。
这个新的编程模型更像是一个库,而不是一个框架,允许您将请求映射到不带注解的处理代码。使用 Spring 的函数式编程模型编写 API 涉及四种主要类型:
- RequestPredicate —— 声明将会被处理的请求类型
- RouteFunction —— 声明一个匹配的请求应该如何被路由到处理代码中
- ServerRequest —— 表示 HTTP 请求,包括对头和正文信息的访问
- ServerResponse —— 表示 HTTP 响应,包括头和正文信息
作为将所有这些类型组合在一起的简单示例,请考虑以下Hello World示例:
package hello;
import static org.springframework.web
.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web
.reactive.function.server.RouterFunctions.route;
import static org.springframework.web
.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
@Configuration
public class RouterFunctionConfig {
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
;
}
}
首先要注意的是,已经选择静态地导入几个 helper 类,可以使用这些类来创建前面提到的函数类型。还静态导入了 Mono,以使其余代码更易于阅读和理解。
在这个 @Configuration
类中,有一个类型为 RouterFunction<?>
。 如前所述,RouterFunction 声明一个或多个 RequestPredicate 对象与将处理匹配请求的函数之间的映射。
RouterFunctions 中的 route()
方法接受两个参数:RequestPredicate 和处理请求匹配的函数。在本例中,RequestPredicates 的 GET()
方法声明了一个 RequestPredicate,它与 /hello
路径的 HTTP GET 请求相匹配。
至于 handler 函数,它是作为 lambda 编写的,尽管它也可以是方法引用。虽然没有显式声明,但是处理程序 lambda 接受一个 ServerRequest 作为参数。它使用来自 ServerResponse 的 ok()
和来自 BodyBuilder 的 body()
返回一个 ServerResponse,后者是从 ok()
返回的。这样做是为了创建一个带有 HTTP 200(OK)状态代码和一个表示 Hello World 的 body 负载的响应!
如前所述,helloRouterFunction()
方法声明了一个仅处理单一类型请求的 RouterFunction。但是如果需要处理不同类型的请求,不必编写另一个 @Bean
方法。只需要调用 andRoute() 来声明另一个 RequestPredicate 到函数的映射。例如,下面介绍如何为/bye
的 GET 请求添加另一个处理程序:
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
.andRoute(GET("/bye"),
request -> ok().body(just("See ya!"), String.class))
;
}
Hello World 的例子可以让您接触到新的东西。但是让我们把它放大一点,看看如何使用 Spring 的函数式 web 编程模型来处理类似于真实场景的请求。
为了演示函数式编程模型如何在实际应用程序中使用,让我们将 DesignTacoController 的功能重新设计为函数式样式。以下配置类是 DesignTacoController 的功能模拟:
package tacos.web.api;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import tacos.Taco;
import tacos.data.TacoRepository;
@Configuration
public class RouterFunctionConfig {
@Autowired
private TacoRepository tacoRepo;
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/api/tacos").
and(queryParam("recent", t->t != null )),
this::recents)
.andRoute(POST("/api/tacos"), this::postTaco);
}
public Mono<ServerResponse> recents(ServerRequest request) {
return ServerResponse.ok()
.body(tacoRepo.findAll().take(12), Taco.class);
}
public Mono<ServerResponse> postTaco(ServerRequest request) {
return request.bodyToMono(Taco.class)
.flatMap(taco -> tacoRepo.save(taco))
.flatMap(savedTaco -> {
return ServerResponse
.created(URI.create(
"http://localhost:8080/api/tacos/" +
savedTaco.getId()))
.body(savedTaco, Taco.class);
});
}
}
如您所见,routerFunction()
方法声明了一个 routerFunction<?>
的 bean,就像 Hello World 的例子。但在处理哪些类型的请求以及如何处理这些请求方面有所不同。在本例中,创建 RouterFunction 来处理 /design/taco
的 GET 请求和 /design
的 POST 请求。
更突出的是路由是由方法引用处理的。当 RouterFunction 后面的行为相对简单和简短时,lambda 非常好。但是,在许多情况下,最好将该功能提取到单独的方法中(甚至在单独的类中提取到单独的方法中),以保持代码的可读性。
根据您的需要,/api/tacos?recent
的 GET 请求将由 recents()
方法处理。它使用注入的 TacoRepository 来获取一个 Mono<Taco>
,从中提取 12 个项目。然后将 Flux<Taco>
包装在 Mono<ServerResponse>
中,以确保响应显示 HTTP 200(OK)状态。这通过在 ServerResponse 上调用 OK()
来实现。尽管返回了多达 12 个玉米卷,但只有一个服务器响应,理解这一点是很重要的。这就是为什么以 Mono 而非 Flux 类型来返回。在内部,Spring 仍然会将 Flux<Taco>
流作为 Flux 返回到客户端。
同时,postTaco()
方法处理 /api/tacos
的 POST 请求,该方法从传入的 ServerRequest 中提取 Mono<Taco>
。然后 postTaco()
方法使用一系列 flatMap()
操作将 taco 保存到 TacoRepository,并创建服务器响应。带有 HTTP 201(已创建)状态码以及保存好的 Taco 对象在响应体中。
flatMap()
操作用于确保在流的每个步骤中,结果被包装在一个 Mono 中,从第一个 flatMap()
之后的 Mono<Taco>
开始,然后
最终以 postTaco()
返回的 Mono<ServerResponse>
结束。