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 'https://:8080/person/1'
{"name":"John Doe","age":42}

这里有很多内容需要涵盖,所以让我们深入探讨!

关键组件

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

HandlerFunction

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

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

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 社区所有即将举行的活动。

查看所有