先行一步
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
是新定義的、不可變的介面,為底層 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 上找到該專案。
請告訴我們你的想法!