領先一步
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上找到該專案。
讓我們知道您的想法!