領先一步
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 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
,以便它可以與反應式 @Controllers
並排在 DispatcherHandler
中運行。
這結束了對 Spring 新的函數式風格 Web 框架的介紹。讓我通過給出一個簡短的摘要來總結一下
為了給您更完整的了解,我建立了一個使用函數式 Web 框架的簡單範例專案。您可以在 GitHub 上找到該專案。
請告訴我們您的想法!