Spring 5 新特性:函数式 Web 框架

工程 | Arjen Poutsma | 2016 年 9 月 22 日 | ...

正如昨天在 Juergen 的博客文章中提到的,Spring Framework 5.0 的第二个里程碑版本引入了一个新的函数式 Web 框架。在这篇文章中,我将提供关于该框架的更多信息。

请记住,函数式 Web 框架是建立在我们 M1 版本提供的相同响应式基础之上的,在该基础上我们也支持基于注解(即 @Controller@RequestMapping)的请求处理,更多相关信息请参阅M1 博客文章

示例

我们先从我们的示例应用程序中摘录一些片段。下面是一个响应式仓库,它暴露了 Person 对象。它与传统的、非响应式仓库非常相似,不同之处在于,传统仓库返回 List<Person> 的地方,它返回 Flux<Person>;传统仓库返回 Person 的地方,它返回 Mono<Person>Mono<Void> 用作完成信号:表示保存何时完成。有关这些 Reactor 类型的更多信息,请参阅Dave 的博客文章

public interface PersonRepository {
  Mono<Person> getPerson(int id);
  Flux<Person> allPeople();
  Mono<Void> savePerson(Mono<Person> person);
}

下面是如何使用新的函数式 Web 框架来暴露该仓库

RouterFunction<?> route = route(GET("/person/{id}"),
  request -> {
    Mono<Person> person = Mono.justOrEmpty(request.pathVariable("id"))
      .map(Integer::valueOf)
      .then(repository::getPerson);
    return Response.ok().body(fromPublisher(person, Person.class));
  })
  .and(route(GET("/person"),
    request -> {
      Flux<Person> people = repository.allPeople();
      return Response.ok().body(fromPublisher(people, Person.class));
    }))
  .and(route(POST("/person"),
    request -> {
      Mono<Person> person = request.body(toMono(Person.class));
      return Response.ok().build(repository.savePerson(person));
    }));

下面是如何运行它,例如在 Reactor Netty 中

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter =
  new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create("localhost", 8080);
server.startAndAwait(adapter);

最后要做的是尝试一下

$ curl 'http://localhost:8080/person/1'
{"name":"John Doe","age":42}

这里有很多内容需要介绍,所以让我们深入挖掘!

关键组件

我将通过介绍其关键组件来解释该框架:HandlerFunctionRouterFunctionFilterFunction。这三个接口以及本文中描述的所有其他类型都可以在 org.springframework.web.reactive.function 包中找到。

HandlerFunction

这个新框架的起点是 HandlerFunction<T>,它本质上是一个 Function<Request, Response<T>>,其中 RequestResponse 是新定义的、不可变的接口,为底层 HTTP 消息提供了 JDK-8 友好的 DSL。有一个方便的构建器用于构建 Response 实例,与 ResponseEntity 中的构建器非常相似。与 HandlerFunction 对应的注解是带有 @RequestMapping 的方法。

这里是一个简单的“Hello World”处理函数示例,它返回一个状态为 200 且基于 String 的响应体

HandlerFunction<String> helloWorld =
  request -> Response.ok().body(fromObject("Hello World"));

正如我们在上面的示例中看到的,处理函数通过构建在 Reactor 之上而完全是响应式的:它们接受 FluxMono 或任何其他 Reactive Streams Publisher 作为响应类型。

重要的是要注意 HandlerFunction 本身是无副作用的,因为它 返回 响应,而不是将其作为参数(参考 Servlet.service(ServletRequest,ServletResponse),它本质上是一个 BiConsumer<ServletRequest,ServletResponse>)。无副作用的函数有很多好处:它们更容易测试、组合和优化

RouterFunction

传入的请求通过 RouterFunction<T>(即 Function<Request, Optional<HandlerFunction<T>>)路由到处理函数。如果路由函数匹配,则评估为处理函数;否则返回空结果。RouterFunction@RequestMapping 注解有相似的目的。然而,有一个重要的区别:使用注解时,你的路由 受限于注解值所能表达的内容,并且对这些注解的处理不易覆盖;而使用路由函数时,处理代码就在你眼前:你可以非常容易地覆盖或替换它。

这里是一个带有内联处理函数的路由函数示例。它看起来有点冗长,但请不要担心:我们将在下面找到使其更简洁的方法。

RouterFunction<String> helloWorldRoute = 
  request -> {
    if (request.path().equals("/hello-world")) {
      return Optional.of(r -> Response.ok().body(fromObject("Hello World")));
    } else {
      return Optional.empty();
    }
  };

通常,你不会编写完整的路由函数,而是(静态)导入 RouterFunctions.route(),它允许你使用 RequestPredicate(即 Predicate<Request>)和 HandlerFunction 创建一个 RouterFunction。如果谓词适用,则返回处理函数;否则返回空结果。使用 route,我们可以将上面改写如下

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(request -> request.path().equals("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

你可以(静态)导入 RequestPredicates.* 来访问常用的谓词,例如基于路径、HTTP 方法、内容类型等的匹配。有了它,我们可以让 helloWorldRoute 更加简单

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(RequestPredicates.path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

组合函数

两个路由函数可以组合成一个新的路由函数,该函数可以路由到任一处理函数:如果第一个函数不匹配,则评估第二个函数。你可以通过调用 RouterFunction.and() 来组合两个路由函数,如下所示

RouterFunction<?> route =
  route(path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(path("/the-answer"),
    request -> Response.ok().body(fromObject("42"))));

如果路径匹配 /hello-world,上面的代码将响应 "Hello World";如果匹配 /the-answer,则响应 "42"。如果两者都不匹配,则返回一个空的 Optional。请注意,组合的路由函数是按顺序评估的,因此将特定函数放在通用函数之前是有意义的。

你也可以通过调用 andor 来组合请求谓词。它们按预期工作:对于 and,只有当两个给定的谓词都匹配时,结果谓词才匹配;对于 or,只要任一谓词匹配,结果谓词就匹配。例如

RouterFunction<?> route =
  route(method(HttpMethod.GET).and(path("/hello-world")), 
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(method(HttpMethod.GET).and(path("/the-answer")), 
    request -> Response.ok().body(fromObject("42"))));

实际上,RequestPredicates 中的大多数谓词都是组合!例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String) 的组合。所以我们可以将上面改写为

RouterFunction<?> route =
  route(GET("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(GET("/the-answer"),
    request -> Response.ok().body(fromObject(42))));

方法引用

附带一提:到目前为止,我们已经将所有处理函数写成了内联 lambda 表达式。虽然这对于演示和简短示例来说没问题,但它往往会变得“混乱”,因为你混合了两个关注点:请求路由和请求处理。所以让我们看看是否能让事情更清晰。首先我们创建一个包含处理代码的类

class DemoHandler {
  public Response<String> helloWorld(Request request) {
    return Response.ok().body(fromObject("Hello World"));
  }
  public Response<String> theAnswer(Request request) {
    return Response.ok().body(fromObject("42"));
  }
}

注意,这两个方法都有一个与处理函数兼容的签名。这允许我们使用方法引用

DemoHandler handler = new DemoHandler(); // or obtain via DI
RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer));

FilterFunction

通过调用 RouterFunction.filter(FilterFunction<T, R>),可以过滤由路由函数映射的路由,其中 FilterFunction<T,R> 本质上是一个 BiFunction<Request, HandlerFunction<T>, Response<R>>。处理函数参数表示链中的下一个项目:这通常是一个 HandlerFunction,但如果应用了多个过滤器,也可以是另一个 FilterFunction。让我们为我们的路由添加一个日志过滤器

RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer))
  .filter((request, next) -> {
    System.out.println("Before handler invocation: " + request.path());
    Response<?> response = next.handle(request);
    Object body = response.body();
    System.out.println("After handler invocation: " + body);
    return response;
  });

请注意,调用下一个处理程序是可选的。这在安全或缓存场景中很有用(例如,只有当用户拥有足够的权限时才调用 next)。

因为 route 是一个无界路由函数,我们不知道下一个处理程序将返回什么类型的响应。这就是为什么我们的过滤器中会得到一个 Response<?>,以及一个 Object 响应体。在我们的处理程序类中,两个方法都返回一个 Response<String>,因此应该有可能得到一个 String 响应体。我们可以通过使用 RouterFunction.andSame() 代替 and() 来实现这一点。这个组合方法要求参数路由函数是相同类型的。例如,我们可以将所有响应转换为大写

RouterFunction<String> route =
  route(GET("/hello-world"), handler::helloWorld)
  .andSame(route(GET("/the-answer"), handler::theAnswer))
  .filter((request, next) -> {
    Response<String> response = next.handle(request);
    String newBody = response.body().toUpperCase();
    return Response.from(response).body(fromObject(newBody));
  });

使用注解,可以使用 @ControllerAdvice 和/或 ServletFilter 实现类似的功能。

运行服务器

这一切都很好,但还缺少一块:我们如何在 HTTP 服务器中实际运行这些函数?答案不出所料,是通过调用另一个函数。你可以使用 RouterFunctions.toHttpHandler() 将路由函数转换为 HttpHandlerHttpHandler 是 Spring 5.0 M1 中引入的响应式抽象:它允许你在各种响应式运行时上运行:Reactor Netty、RxNetty、Servlet 3.1+ 和 Undertow。在示例中,我们已经展示了在 Reactor Netty 中运行 route 是什么样的。对于 Tomcat,它看起来像这样

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
Context rootContext = server.addContext("",
  System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(rootContext, "servlet", servlet);
rootContext.addServletMapping("/", "servlet");
tomcatServer.start();

需要注意的一点是,上述方法不依赖于 Spring 应用程序上下文。就像 JdbcTemplate 和其他 Spring 工具类一样,使用应用程序上下文是可选的:你可以在一个上下文中连接你的处理程序和路由函数,但这不是必需的。另请注意,你还可以将路由函数转换为 HandlerMapping,以便它可以在 DispatcherHandler 中运行(可能与响应式 @Controllers 并行运行)。

结论

至此,Spring 新函数式 Web 框架的介绍结束。让我总结一下要点:

  • 处理函数通过返回响应来处理请求,
  • 路由函数路由到处理函数,并且可以与其他路由函数组合,
  • 路由函数可以通过过滤器函数进行过滤,
  • 路由函数可以在响应式 Web 运行时中运行。

为了给你更完整的了解,我创建了一个使用函数式 Web 框架的简单示例项目。你可以在 GitHub 上找到该项目。

请告诉我们你的想法!

订阅 Spring 资讯

保持与 Spring 资讯的连接

订阅

先行一步

VMware 提供培训和认证,助你加速进步。

了解更多

获得支持

Tanzu Spring 通过一项简单的订阅即可为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部