先行一步
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 'http://localhost: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 且基于 String 的响应体
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 上找到该项目。
请告诉我们你的想法!