11.2 定义函数式请求处理器

11.2 定义函数式请求处理器

Spring MVC基于注解的编程模型从Spring 2.5就存在了,而且这种模型非常流行,但是它也有一些缺点。

首先,所有基于注解的编程方式都会存在注解该做什么以及注解如何做之间的割裂。注解本身定义了该做什么,而具体如何去做则是在框架代码的其他部分定义的。如果想要进行自定义或扩展,编程模型就会变得很复杂,因为这样的变更需要修改注解之外的代码。除此之外,这种代码的调试也是比较麻烦的,因为我们无法在注解上设置断点。

其次,随着Spring变得越来越流行,很多熟悉其他语言和框架的Spring新手会觉得基于注解的Spring MVC(和WebFlux)与他们之前掌握的知识有很大的差异。作为注解式WebFlux的一种替代方案,Spring 5引入了一个新的函数式编程模型,用来定义反应式API。

这个新的编程模型使用起来更像一个库,而不是一个框架,能够让我们在不使用注解的情况下将请求映射到处理器代码中。使用Spring的函数式编程模型编写API会涉及4个主要的类型:

  • RequestPredicate:声明要处理的请求类型。
  • RouterFunction:声明如何将请求路由到处理器代码中。
  • ServerRequest:代表一个HTTP请求,包括对请求头和请求体的访问。
  • ServerResponse:代表一个HTTP响应,包括响应头和响应体信息。

下面是一个将所有类型组合在一起的Hello World样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package demo;
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));
}
}

我们需要注意的第一件事情就是,在这里静态导入了一些辅助类,可以使用它们来创建前文所述的函数式类型。我们还以静态方式导入了Mono,从而能够让剩余的代码更易于阅读和理解。

在这个@Configuration类中,我们有一个类型为RouterFunction<?>的@Bean方法。按照前文所述,RouterFunction能够声明一个或多个RequestPredicate对象和处理与之匹配的请求的函数之间的映射关系。

RouterFunctions的route()方法接受两个参数:RequestPredicate以及处理与之匹配的请求的函数。在本例中,RequestPredicates的GET()方法声明一个RequestPredicate,后者会匹配针对“/hello”的HTTP GET请求。

至于处理器函数,它写成了lambda表达式的形式,当然它也可以使用方法引用。尽管这里没有显式声明,但是处理器lambda表达式会接受一个ServerRequest作为参数。它通过ServerResponse的ok()方法和BodyBuilder的body()方法返回了一个ServerResponse。BodyBuilder对象是由ok()所返回的。这样的话,就会创建出状态码为HTTP 200 (OK)并且响应体载荷为“Hello World!”的响应。

按照这种编写形式,helloRouterFunction()方法所声明的RouterFunction只能处理一种类型的请求。如果想要处理不同类型的请求,那么我们没有必要编写另外一个@Bean(当然你也可以这样做),仅需调用andRoute()来声明另一个RequestPredicate到函数的映射。例如,为“/bye”的GET请求添加一个处理器:

1
2
3
4
5
6
7
@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的函数式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class RouterFunctionConfig {
@Autowired
private TacoRepository tacoRepo;
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/design/taco"), this::recents)
.andRoute(POST("/design"), this::postTaco);
}
public Mono<ServerResponse> recents(ServerRequest request) {
return ServerResponse.ok()
.body(tacoRepo.findAll().take(12), Taco.class);
}
public Mono<ServerResponse> postTaco(ServerRequest request) {
Mono<Taco> taco = request.bodyToMono(Taco.class);
Mono<Taco> savedTaco = tacoRepo.save(taco);
return ServerResponse
.created(URI.create(
"http://localhost:8080/design/taco/" +
savedTaco.getId()))
.body(savedTaco, Taco.class);
}
}

我们可以看到,routerFunction()方法声明了一个RouterFunction<?> bean,这与Hello World样例类似。但是,它们之间的差异在于要处理什么类型的请求以及如何处理。在本例中,我们创建的RouterFunction处理针对“/design/taco”的GET请求以及针对“/design”的POST请求。

更明显的差异在于,路由是由方法引用处理的。如果RouterFunction背后的行为相对简单和简洁,那么lambda是很不错的选择。在很多场景下,最好将功能抽取到一个单独的方法中(甚至抽取到一个独立类的方法中),以便于保持代码的可读性。

就我们的需求而言,针对“/design/taco”的GET请求将由recents()方法来处理。它使用注入的TacoRepository得到一个Flux<Taco>,然后从中得到12个条目。针对“/design”的POST请求会由postTaco()方法来处理,它会从传入的ServerRequest中抽取Mono<Taco>。postTaco()使用TacoRepository方法来进行保存,随后使用save()返回的Mono<Taco>作为响应。