定义函数式请求处理器

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

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

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

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

  • RequestPredicate,声明要处理的请求类型;

  • RouterFunction,声明如何将请求路由到处理器代码中;

  • 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))
      ;
  }
}

需要注意的第一件事情是,这里静态导入了一些辅助类,可以使用它们来创建前文所述的函数式类型。我们还以静态方式导入了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请求添加一个处理器:

@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编程模型处理接近真实场景的请求。

为了阐述如何在真实应用中使用函数式编程模型,我们会使用函数式风格重塑TacoController的功能。如下的配置类是TacoController的函数式实现:

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处理针对“/api/tacos?recent”的GET请求和针对“/api/tacos”的POST请求。

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

就我们的需求而言,针对“/api/tacos?recent”的GET请求将由recents()方法来处理。它使用注入的TacoRepository得到一个Flux<Taco>,然后从中得到12个数据项。随后,它将Flux<Taco>包装到一个Mono<ServerResponse>中,这样我们就可以通过调用ServerResponse中的ok()来返回一个HTTP 200 (OK)状态的响应。有很重要的一点需要我们注意:即使有多达12个Taco需要返回,我们也只会有一个服务器响应,这就是为什么它会以Mono而不是Flux的形式返回。在内部,Spring仍然会将Flux<Taco>作为Flux流向客户端。

针对“/api/tacos”的POST请求由postTaco()方法处理,它从传入的ServerRequest的请求体中提取Mono<Taco>。随后postTaco()方法使用一系列flatMap()操作将该taco保存到TacoRepository中,并创建一个状态码为HTTP 201 (CREATED)的ServerResponse,在响应体中包含了已保存的Taco对象。

flatMap()操作能够确保在流程中的每一步中映射的结果都包装在一个Mono中,从第一个flatMap()之后的Mono<Taco>开始,以postTaco()返回的Mono<ServerResponse>结束。