领先一步
VMware 提供培训和认证,助您加速进步。
了解更多正如昨天在 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}
这里有很多内容需要涵盖,所以让我们深入探讨!
我将通过其关键组件:HandlerFunction、RouterFunction和FilterFunction来解释该框架。这三个接口以及本文中描述的所有其他类型都可以在org.springframework.web.reactive.function包中找到。
这个新框架的起点是 HandlerFunction<T>,它本质上是一个Function<Request, Response<T>>,其中Request和Response是新定义的不可变接口,它们提供了 JDK-8 友好的 DSL 来处理底层的 HTTP 消息。有一个方便的构建器用于构建Response实例,与ResponseEntity中的构建器非常相似。HandlerFunction的注解对应物是带有@RequestMapping的方法。
这是一个简单的“Hello World”处理函数示例,它返回一个状态为 200 且基于字符串的主体的响应
HandlerFunction<String> helloWorld =
request -> Response.ok().body(fromObject("Hello World"));
正如我们在上面的示例中看到的,处理函数通过构建在 Reactor 之上而完全具有响应性:它们接受Flux、Mono或任何其他Reactive Streams Publisher作为响应类型。
需要注意的是,HandlerFunction本身是无副作用的,因为它*返回*响应,而不是将其作为参数(参考Servlet.service(ServletRequest,ServletResponse),它本质上是一个BiConsumer<ServletRequest,ServletResponse>)。无副作用函数具有许多优点:它们更容易测试、组合和优化。
传入请求通过 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。请注意,组合的路由器函数是按顺序评估的,因此将特定函数放在通用函数之前是有意义的。
您还可以通过调用and或or来组合请求谓词。这些谓词按预期工作:对于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));
通过调用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 框架的简单示例项目。您可以在GitHub上找到该项目。
让我们知道您的想法!