领先一步
VMware提供培训和认证,以加速您的进步。
了解更多正如昨天在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}
这里有很多内容需要介绍,所以让我们深入探讨!
我将通过介绍其关键组件来解释该框架:HandlerFunction
、RouterFunction
和FilterFunction
。这三个接口以及本文中描述的所有其他类型,都可以在org.springframework.web.reactive.function
包中找到。
这个新框架的起点是HandlerFunction<T>
,它本质上是一个Function<Request, Response<T>>
,其中Request
和Response
是新定义的不可变接口,它们为底层 HTTP 消息提供了 JDK-8 友好的 DSL。有一个方便的构建器用于构建Response
实例,这与ResponseEntity
中的构建器非常相似。HandlerFunction
的注解对应项是一个带有@RequestMapping
的方法。
这是一个简单的“Hello World”处理程序函数示例,它返回一个具有 200 状态码和基于字符串的主体的响应。
HandlerFunction<String> helloWorld =
request -> Response.ok().body(fromObject("Hello World"));
正如我们在上面的示例中看到的,处理程序函数通过构建在 Reactor 之上而完全具有反应性:它们接受Flux
、Mono
或任何其他Reactive StreamsPublisher
作为响应类型。
需要注意的是,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上找到该项目。
请告诉我们您的想法!