Spring WebFlux使用函數(shù)式編程模型構建異步非阻塞服務
1 前言
上文引入了 Spring 框架中專門用于構建響應式 Web 服務的 WebFlux 框架,同時我也給出了兩種創(chuàng)建 RESTful 風格 HTTP 端點實現(xiàn)方法中的一種,即注解編程模型。
本文介紹另一種實現(xiàn)方法——如何使用函數(shù)式編程模型創(chuàng)建響應式 RESTful 服務,這種編程模型與傳統(tǒng)的基于 Spring MVC 構建 RESTful 服務的方法有較大差別。
2 WebFlux 函數(shù)式編程模型
回顧Spring WebFlux系統(tǒng)架構圖:
圖后半部分,Spring WebFlux 中,函數(shù)式編程模型的核心概念Router Functions,對標 Spring MVC 的 @Controller、@RequestMapping 等標準注解。而 Router Functions 則提供一套函數(shù)式風格API,最重要的就是 Router、Handler 接口??珊唵螌ⅲ?/p>
- Router 對應成 RequestMapping
- Controller 對應為 Handler
當我發(fā)起一個遠程調用,傳入的 HTTP 請求由 HandlerFunction 處理, HandlerFunction 本質上是一個接收 ServerRequest 并返回 Mono 的函數(shù)。ServerRequest 和 ServerResponse 是一對不可變接口,用來提供對底層 HTTP 消息的友好訪問。
3 ServerRequest
代表請求對象,可訪問各種 HTTP 請求元素,包括請求方法、URI 和參數(shù),以及通過單獨的 ServerRequest.Headers 獲取 HTTP 請求頭信息。
ServerRequest 通過一系列 bodyToMono() 和 bodyToFlux() 方法提供對請求消息體進行訪問的途徑。例如,如果我們希望將請求消息體提取為 Mono 類型的對象,可以使用如下方法。
Mono<String> string = request.bodyToMono(String.class);
而如果我們希望將請求消息體提取為 Flux 類型的對象,可以使用如下方法,其中 Order 是可以從請求消息體反序列化的實體類。
Flux<Order> order = request.bodyToFlux(Order.class);
上述的 bodyToMono() 和 bodyToFlux() 兩個方法實際上是通用的 ServerRequest.body(BodyExtractor) 工具方法的快捷方式,該方法如下所示。
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor);
BodyExtractor 是一種請求消息體的提取器,允許我們編寫自己的提取邏輯。請注意 BodyExtractor 提取的對象是一個 ServerHttpRequest 類型的實例,而這個 ServerHttpRequest 是非阻塞的,與之對應的還有一個 ServerHttpResponse 對象。
響應式 Web 操作的正是這組非阻塞的:
- ServerHttpRequest
- ServerHttpResponse
而不再是 Spring MVC 里的傳統(tǒng):
- HttpServletRequest
- HttpServletResponse
若不需要實現(xiàn)定制化的提取邏輯,可使用框架提供的常用的 BodyExtractors 實例。通過 BodyExtractors,上例可替換為。
Mono<String> string = request.body(BodyExtractors.toMono(String.class); Flux<Person> Order= request.body(BodyExtractors.toFlux(Order.class);
ServerResponse
與ServerRequest 對應,ServerResponse 提供對 HTTP 響應的訪問。由于不可變,因此可用構建器創(chuàng)建一個新 ServerResponse。
構建器允許設置響應狀態(tài)、添加響應標題并提供響應的具體內容。如下示例演示如何通過 ok() 方法創(chuàng)建代表 200 狀態(tài)碼的響應,其中我將響應體的類型設置為 JSON 格式,響應具體內容是 Mono 對象。
Mono<Order> order = …; ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) .body(order);
通過 body() 方法來加載響應內容是構建 ServerResponse 最常見的方法,這里我們將 Order 對象作為返回值。如果想要返回各種類型的對象,我們也可以使用 BodyInserters 工具類所提供的構建方法,如常見的 fromObject() 和 fromPublisher() 方法等。以下示例代碼中,我們通過 fromObject() 方法直接返回一個 “Hello World”。
ServerResponse.ok().body(BodyInserters.fromObject("Hello World"));
上述方法的背后實際上是利用 BodyBuilder 接口中的一組 body() 方法,來構建一個 ServerResponse 對象,典型的 body() 方法如下所示。
Mono<ServerResponse> body(BodyInserter<?, ? super ServerHttpResponse> inserter);
這里我們同樣看到了非阻塞式的 ServerHttpResponse 對象。這種 body() 方法比較常見的用法是返回新增和更新操作的結果,你在本講后續(xù)的內容中將會看到這種使用方法。
HandlerFunction
將 ServerRequest 和 ServerResponse 組合在一起就可創(chuàng)建 HandlerFunction。
public interface HandlerFunction<T extends ServerResponse> { Mono<T> handle(ServerRequest request); }
可通過實現(xiàn) HandlerFunction#handle() 創(chuàng)建定制化的請求響應處理機制。
簡單的“Hello World”處理函數(shù)。
public class HelloWorldHandlerFunction implements HandlerFunction<ServerResponse> { @Override public Mono<ServerResponse> handle(ServerRequest request) { return ServerResponse.ok().body( BodyInserters.fromObject("Hello World")); } };
這里使用了前面介紹的 ServerResponse 所提供的 body() 方法返回一個 String 類型的消息體。
通常,針對某個領域實體都存在 CRUD 等常見的操作,所以需要編寫多個類似的處理函數(shù),煩瑣。推薦將多個處理函數(shù)分組到一個專門 Handler 類。
RouterFunction
已可通過 HandlerFunction 創(chuàng)建請求的處理邏輯,接下來需要把具體請求與這種處理邏輯關聯(lián)起來,RouterFunction 可以幫助我們實現(xiàn)這一目標。RouterFunction 與傳統(tǒng) SpringMVC 中的 @RequestMapping 注解功能類似。
創(chuàng)建 RouterFunction 最常見做法是使用 route 方法,該方法通過使用請求謂詞和處理函數(shù)創(chuàng)建一個 ServerResponse 對象。
public static <T extends ServerResponse> RouterFunction<T> route( RequestPredicate predicate, HandlerFunction<T> handlerFunction) { return new DefaultRouterFunction<>(predicate, handlerFunction); }
RouterFunction 的核心邏輯位于這里的 DefaultRouterFunction 類中,該類的 route() 方法如下所示。
public Mono<HandlerFunction<T>> route(ServerRequest request) { if (this.predicate.test(request)) { if (logger.isTraceEnabled()) { String logPrefix = request.exchange().getLogPrefix(); logger.trace(logPrefix + String.format("Matched %s", this.predicate)); } return Mono.just(this.handlerFunction); } else { return Mono.empty(); } }
該方法將傳入的 ServerRequest 路由到具體的處理函數(shù) HandlerFunction。如果請求與特定路由匹配,則返回處理函數(shù)的結果,否則就返回空Mono。
RequestPredicates 工具類提供了常用的謂詞,能夠實現(xiàn)包括基于路徑、HTTP 方法、內容類型等條件的自動匹配。
簡單 RouterFunction 示例,實現(xiàn)對 "/hello-world"請求路徑的自動路由:
RouterFunction<ServerResponse> helloWorldRoute = RouterFunctions.route(RequestPredicates.path("/hello-world"), new HelloWorldHandlerFunction());
類似的,我們應該把 RouterFunction 和各種 HandlerFunction 按照需求結合起來一起使用,常見的做法也是根據(jù)領域對象來設計對應的 RouterFunction。
路由機制的優(yōu)勢在于它的組合型。兩個路由功能可以組合成一個新的路由功能,并通過一定的評估方法路由到其中任何一個處理函數(shù)。如果第一個路由的謂詞不匹配,則第二個謂詞會被評估。請注意組合的路由器功能會按照順序進行評估,因此在通用功能之前放置一些特定功能是一項最佳實踐。在 RouterFunction 中,同樣提供了對應的組合方法來實現(xiàn)這一目標,請看下面的代碼。
default RouterFunction<T> and(RouterFunction<T> other) { return new RouterFunctions.SameComposedRouterFunction<>(this, other); } default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) { return and(RouterFunctions.route(predicate, handlerFunction)); }
我們可以通過調用上述兩個方法中的任意一個來組合兩個路由功能,其中后者相當于 RouterFunction.and() 方法與 RouterFunctions.route() 方法的集成。以下代碼演示了 RouterFunctions 的組合特性。
RouterFunction<ServerResponse> personRoute = route(GET("/orders/{id}").and(accept(APPLICATION_JSON)), personHandler::getOrderById) .andRoute(GET("/orders").and(accept(APPLICATION_JSON)), personHandler::getOrders) .andRoute(POST("/orders").and(contentType(APPLICATION_JSON)), personHandler::createOrder);
RequestPredicates 工具類所提供的大多數(shù)謂詞也具備組合特性。例如, RequestPredicates.GET(String) 方法的實現(xiàn)如下所示。
public static RequestPredicate GET(String pattern) { return method(HttpMethod.GET).and(path(pattern)); }
可以看到,該方法是 RequestPredicates.method(HttpMethod.GET) 和 RequestPredicates.path(String) 的組合。我們可以通過調用 RequestPredicate.and(RequestPredicate) 方法或 RequestPredicate.or(RequestPredicate) 方法來構建復雜的請求謂詞。
案例集成:ReactiveSpringCSS 中的 Web 服務
customer-service 分別需要訪問 account-service 和 order-service 服務中的 Web 服務。
已基于注解編程模型實現(xiàn)了 account-service 中的 AccountController。今天演示 order-service 中 Web 服務實現(xiàn)過程。
基于函數(shù)式編程模型,在 order-service 中,編寫 OrderHandler 專門實現(xiàn)根據(jù) OrderNumber 獲取 Order 領域實體的處理函數(shù)
@Configuration public class OrderHandler { @Autowired private OrderService orderService; public Mono<ServerResponse> getOrderByOrderNumber(ServerRequest request) { String orderNumber = request.pathVariable("orderNumber"); return ServerResponse.ok().body(this.orderService.getOrderByOrderNumber(orderNumber), Order.class); } }
上述代碼示例中,我們創(chuàng)建了一個 OrderHandler 類,然后注入 OrderService 并實現(xiàn)了一個 getOrderByOrderNumber() 處理函數(shù)。
現(xiàn)在我們已經(jīng)具備了 OrderHandler,就可以創(chuàng)建對應的 OrderRouter 了,示例代碼如下。
@Configuration public class OrderRouter { @Bean public RouterFunction<ServerResponse> routeOrder(OrderHandler orderHandler) { return RouterFunctions.route( RequestPredicates.GET("/orders/{orderNumber}") .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), orderHandler::getOrderByOrderNumber); } }
在這個示例中,我們通過訪問“/orders/{orderNumber}”端點就會自動觸發(fā) orderHandler 中的 getOrderByOrderNumber() 方法并返回相應的 ServerResponse。
接下來,假設我們已經(jīng)分別通過遠程調用獲取了目標 Account 對象和 Order 對象,那么 generateCustomerTicket 方法的執(zhí)行流程就可以明確了?;陧憫骄幊痰膶崿F(xiàn)方法,我們可以得到如下所示的示例代碼。
public Mono<CustomerTicket> generateCustomerTicket(String accountId, String orderNumber) { // 創(chuàng)建 CustomerTicket 對象 CustomerTicket customerTicket = new CustomerTicket(); customerTicket.setId("C_" + UUID.randomUUID().toString()); // 從遠程 account-service 獲取 Account 對象 Mono<AccountMapper> accountMapper = getRemoteAccountByAccountId(accountId); // 從遠程 order-service 中獲取 Order 對象 Mono<OrderMapper> orderMapper = getRemoteOrderByOrderNumber(orderNumber); Mono<CustomerTicket> monoCustomerTicket = Mono.zip(accountMapper, orderMapper).flatMap(tuple -> { AccountMapper account = tuple.getT1(); OrderMapper order = tuple.getT2(); if(account == null || order == null) { return Mono.just(customerTicket); } // 設置 CustomerTicket 對象屬性 customerTicket.setAccountId(account.getId()); customerTicket.setOrderNumber(order.getOrderNumber()); customerTicket.setCreateTime(new Date()); customerTicket.setDescription("TestCustomerTicket"); return Mono.just(customerTicket); }); // 保存 CustomerTicket 對象并返回 return monoCustomerTicket.flatMap(customerTicketRepository::save); }
顯然,這里的 getRemoteAccountById 和 getRemoteOrderByOrderNumber 方法都涉及了非阻塞式的遠程 Web 服務的調用,這一過程我們將放在下一講中詳細介紹。
請注意,到這里時使用了 Reactor 框架中的 zip 操作符,將 accountMapper 流中的元素與 orderMapper 流中的元素按照一對一的方式進行合并,合并之后得到一個 Tuple2 對象。然后,我們再分別從這個 Tuple2 對象中獲取 AccountMapper 和 OrderMapper 對象,并將它們的屬性填充到所生成的 CustomerTicket 對象中。最后,我們通過 flatMap 操作符調用了 customerTicketRepository 的 save 方法完成了數(shù)據(jù)的持久化。這是 zip 和 flatMap 這兩個操作符非常經(jīng)典的一種應用場景,你需要熟練掌握。
總結
好了,那么本講內容就說到這。延續(xù)上一講,我們接著討論了 Spring WebFlux 的使用方法,并給出了基于函數(shù)式編程模型的 RESTful 端點創(chuàng)建方法。在這種開發(fā)模型中,重點把握:
- ServerRequest
- ServerResponse
- HandlerFunction
- RouterFunction
核心對象的使用方法。
FAQ
WebFlux 函數(shù)式編程模型中包含哪些核心編程對象嗎?
現(xiàn)在,我們已經(jīng)通過 WebFlux 構建了響應式 Web 服務,下一步就是如何來消費它們了。Spring 也專門提供了一個非阻塞式的 WebClient 工具類來完成這一目標,下一講我就來和你系統(tǒng)地討論這個工具類的使用方法,到時見。
到此這篇關于Spring WebFlux使用函數(shù)式編程模型構建異步非阻塞服務的文章就介紹到這了,更多相關Spring WebFlux構建異步非阻塞服務內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java 注冊時發(fā)送激活郵件和激活的實現(xiàn)示例
這篇文章主要介紹了Java 注冊時發(fā)送激活郵件和激活的實現(xiàn)示例的相關資料,需要的朋友可以參考下2017-07-07SpringBoot環(huán)境Druid數(shù)據(jù)源使用及特點
Druid 是目前比較流行的高性能的,分布式列存儲的OLAP框架(具體來說是MOLAP)。本文給大家分享SpringBoot環(huán)境Druid數(shù)據(jù)源使用及特點介紹,感興趣的朋友跟隨小編一起看看吧2021-07-07圖解Spring Security 中用戶是如何實現(xiàn)登錄的
這篇文章主要介紹了圖解Spring Security 中用戶是如何實現(xiàn)登錄的,文中通過示例代碼和圖片介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07如何解決java.lang.NoClassDefFoundError:Could not initi
文章講述了在Java服務器中處理圖形元素時遇到的常見問題,即需要運行X-server,通過在Tomcat/bin/catalina.sh中增加JAVA_OPTS環(huán)境變量并設置-Djava.awt.headless=true,可以解決這個問題,使服務器能夠在沒有圖形界面的情況下運行2024-11-11