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

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

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

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

示例

我们从我们的示例应用程序中摘录一些内容开始。下面是一个公开Person对象的响应式存储库。它与传统的非响应式存储库非常相似,不同之处在于它返回Flux<Person>(您通常会返回List<Person>),以及返回Mono<Person>(您通常会返回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://127.0.0.1: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 状态码和基于字符串的主体的响应。

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

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

需要注意的是,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()`将路由器函数转换为`HttpHandler`。`HttpHandler`是在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社区中所有即将举行的活动。

查看全部