反應性和非反應性程式碼的分離 - DZone
避免在使用 Project Reactor 時因混合反應性和非反應性邏輯而導致的意外行為。
在使用 Project Reactor 或任何其他反應式流實現時要記住的最重要區別之一是程式碼執行中組裝assembly時間與訂閱時間之間的區別:
換句話說,反應式釋出者(Flux和Mono)是惰性的,因此在有人訂閱之前不會發布或處理任何元素。
瞭解這種區別至關重要,因為在編寫實際應用程式時,我們希望所有(或大部分)業務邏輯在訂閱時執行。在這篇文章中,我們將展示不遵守此規則會出現什麼樣的問題以及如何緩解這些問題。
租車服務示例
為了舉例說明這一點,我們將使用一個非常簡單的虛擬汽車租賃服務實現。該服務接受包含客戶姓名、年齡和電子郵件地址以及汽車型號的輸入。它首先檢查客戶是否年滿 18 歲(因此在法律上允許租車),然後將租賃請求儲存到資料庫中,最後生成 PDF 收據並將其通過電子郵件傳送給客戶。
該流程由以下rentCar方法實現:
private static Mono<UUID> rentCar(CarRentalRequest request) { if (request.getCustomerAge() > 18) { UUID rentalId = UUID.randomUUID(); // Generate an ID for the new rental return saveCarRental(rentalId, request) // Save the rental entity to the database .then(buildAndSendPdfReceipt(rentalId, request)) // Generate and send PDF report .then(Mono.just(rentalId)); // Return the ID of the new rental } else { return Mono.error(new RuntimeException("Must be 18 to rent a car")); } } private static Mono<Void> buildAndSendPdfReceipt(UUID rentalId, CarRentalRequest carRentalRequest) { byte[] pdfReceipt = buildPdfReceipt(rentalId, carRentalRequest); return sendPdfReceipt(pdfReceipt, carRentalRequest.getCustomerEmail()); } |
然後我們可以呼叫這個方法來建立釋出者。此外,我們希望確保將工作委託給單獨的排程程式,以便主執行緒可以繼續處理其他請求。我們可以使用subscribeOn操作符來實現這一點(它改變了整個管道的執行上下文,包括上文和下文,因此頂級釋出者將通過Scheduler生成集合上的元素)。最後,我們提供了一個訂閱者,它定義了成功和錯誤響應時要執行的邏輯(subscribe()分別是方法中的兩個 lambda 引數)。
下面我們提供了一個訂閱者,它定義了成功和錯誤響應時要執行的邏輯(subscribe()分別是方法中的兩個 lambda 引數)。
CarRentalRequest request = new CarRentalRequest("Alice", 30, "Hyundai i30", "alice@mail.com"); rentCar(request) .subscribeOn(Schedulers.boundedElastic()) .subscribe(s -> log.info("Car rented successfully, rental ID: {}", s), e -> log.error("Could not rent car: {}", e.getMessage(), e)); |
陷阱 1:不正確的執行上下文
通過仔細觀察第一段程式碼中buildAndSendPdfReceipt方法,人們很容易猜到buildPdfReceipt是一種同步的、非響應式的方法:它不返回任何響應式型別。
但是,如果我們執行這個例子,我們會得到以下輸出:
21:25:38.961 [main] INFO com.reactordemo.carrental.CarRentalService - Build PDF receipt 21:25:38.986 [boundedElastic-1] INFO com.reactordemo.carrental.CarRentalService - Car rented successfully, rental ID: d5b689dd-fa91-486c-b835-44bc2583d53a |
如果我們注意顯示每個語句的當前執行緒的日誌部分(在方括號中),我們會注意到訂閱者邏輯在方括號boundedElastic-1中的執行緒上正確執行 ;然而,建立PDF的工作似乎是在main執行緒上執行的!那麼為什麼會這樣呢?
答案在於上述組裝assembly和訂閱之間的區別。
我們再來看看這個buildAndSendPdfReceipt方法:
private static Mono<Void> buildAndSendPdfReceipt(UUID rentalId, CarRentalRequest carRentalRequest) { byte[] pdfReceipt = buildPdfReceipt(rentalId, carRentalRequest); return sendPdfReceipt(pdfReceipt, carRentalRequest.getCustomerEmail()); } |
執行此方法時,我們只需要組裝反應式管道,即以宣告方式定義要執行的步驟以建立 PDF 報告。在這個階段,我們不應該做生成這個報告的實際工作,這只是在有人訂閱這個釋出者時才會發生。
不幸的是,這裡的情況並非如此 :呼叫buildPdfReceipt是在這個方法的主體中進行的。這樣做的非常不幸的後果之一是我們在上面看到的不正確的執行上下文:
整個管道在main執行緒上組裝assembly,而釋出的元素在boundedElastic排程程式上處理。
解決此問題的一種方法是使用fromCallable,以下方法:
private static Mono<Void> buildAndSendPdfReceipt(UUID rentalId, CarRentalRequest carRentalRequest) { return Mono.fromCallable(() -> buildPdfReceipt(rentalId, carRentalRequest)) .flatMap(pdfReceipt -> sendPdfReceipt(pdfReceipt, carRentalRequest.getCustomerEmail())); |
正如我們所知,釋出者只會在有人訂閱時(即在訂閱時)開始生成元素,因此buildPdfReceipt現在在所需的排程程式上將呼叫作為整個管道的一部分進行。事實上,再次執行應用程式會產生以下結果:
21:54:49.955 [boundedElastic-1] INFO com.reactordemo.carrental.CarRentalService - Build PDF receipt 21:54:49.956 [boundedElastic-1] INFO com.reactordemo.carrental.CarRentalService - Car rented successfully, rental ID: a3bb873e-4943-407a-967f-9fa1c1d0d235 |
在許多複雜的現實生活應用程式中,這種問題很難發現。避免它們的一種好方法是確保反應式方法(即組裝管道的方法,通常具有反應式返回型別)不直接呼叫非反應式方法。相反,它們應該只組裝反應管道,優選地在單一的流利語句,以及非反應性的方法的所有呼叫應該從反應性運算子(內部進行fromCallable,fromRunnable,map,filter,等等)。
陷阱 2:不正確的異常處理
在設計和實現任何型別的應用程式時,我們總是希望通過嘗試恢復或以其他方式向使用者顯示正確的錯誤訊息來確保我們可以優雅地處理錯誤。在我們簡單的汽車租賃服務中,我們建立了一個帶有錯誤處理程式 lambda 的訂閱者,用於記錄上游的錯誤。預期管道中任何地方可能發生的任何錯誤都將導致描述問題的日誌語句。
為了測試這一點,讓我們考慮以下輸入:
CarRentalRequest request = new CarRentalRequest("Bob", null, "Hyundai i30", "bob@mail.com") |
請注意,在這種情況下,客戶的年齡被錯誤地設定為null。即便如此,我們希望這可能導致的任何錯誤都將被正確攔截和記錄。不幸的是,現在執行此程式碼會產生以下輸出:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "com.reactordemo.carrental.CarRentalService$CarRentalRequest.getCustomerAge()" is null at com.reactordemo.carrental.CarRentalService.rentCar(CarRentalService.java:27) at com.reactordemo.carrental.CarRentalService.entryPoint(CarRentalService.java:19) at com.reactordemo.carrental.ReactorDemoApplication.main(ReactorDemoApplication.java:10) |
這表明我們的無效輸入產生了一個在任何地方都沒有被捕獲的 NPE。但為什麼?為什麼沒有為這個異常呼叫我們的錯誤處理程式?為了理解這一點,讓我們再看看我們的主要反應管道:
private static Mono<UUID> rentCar(CarRentalRequest request) { if (request.getCustomerAge() > 18) { UUID rentalId = UUID.randomUUID(); return saveCarRental(rentalId, request) .then(buildAndSendPdfReceipt(rentalId, request)) .then(Mono.just(rentalId)); } else { return Mono.error(new RuntimeException("Must be 18 to rent a car")); } } |
很明顯異常發生在if語句的條件中,我們檢查年齡是否大於 18。但請注意,作為管道執行的一部分,此檢查不會正確發生。相反,檢查是作為組裝管道的一部分進行的。因此,這裡發生的任何錯誤都不會被視為處理管道中的元素失敗,而是組裝管道失敗。再一次,這個問題可以通過簡單地定義所有特定於反應管道內元素處理(包括檢查)的邏輯來避免。
private static Mono<UUID> rentCar(CarRentalRequest request) { return Mono.just(request) .<CarRentalRequest>handle((req, sink) -> { if (req.getCustomerAge() > 18) { sink.next(req); } else { sink.error(new RuntimeException("Must be 18 to rent a car")); } }) .flatMap(req -> { UUID rentalId = UUID.randomUUID(); return saveCarRental(rentalId, req) .then(buildAndSendPdfReceipt(rentalId, req)) .then(Mono.just(rentalId)); }); } |
在最初的實現中,有兩個功能與處理在組裝時執行的請求相關:年齡檢查和 ID 生成。我們現在已經將它們分別移到管道中,分別在handle和flatMap運算子中。應用此修復程式後,執行會產生以下輸出:
12:48:46.627 [boundedElastic-1] ERROR com.reactordemo.carrental.CarRentalService - Could not rent car: Cannot invoke "java.lang.Integer.intValue()" because the return value of "com.reactordemo.carrental.CarRentalService$CarRentalRequest.getCustomerAge()" is null java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "com.reactordemo.carrental.CarRentalService$CarRentalRequest.getCustomerAge()" is null at com.reactordemo.carrental.CarRentalService.lambda$rentCarFixed$2(CarRentalService.java:40) |
當然,丟擲 NPE 而不是驗證輸入併產生更有意義的錯誤是不理想的。儘管如此,我們仍然可以看到異常現在在管道內的訂閱時丟擲,這意味著它最終會被我們的錯誤處理程式捕獲,正如預期的那樣。
結論
在這篇博文中,我們分析了兩種情況,其中不正確地分離彙編時間和訂閱時間邏輯會導致我們的應用程式出現不良行為。為了減輕此類問題和其他問題,我們建議明確分離如下:
- 作為反應式方法(組裝反應式管道的方法,即具有反應式返回型別)的一部分,避免執行除嚴格構建管道之外的任務。
- 此類方法的一個好的做法是確保它們只組裝和返回反應式管道,最好在單個流利風格的語句中。
- 任何重要的邏輯(通常與元素處理有關,而不是組裝管道),例如輸入驗證或對映、對其他同步方法的呼叫等,都應作為管道的一部分執行。這可以使用大量運算子來實現,本文舉例說明了其中的幾個,例如handle, flatMap, fromCallable。
相關文章
- JavaScript函式的反應性JavaScript函式
- 微信域名檢測中反應速度的重要性
- [譯] 以 Vue 為例,解釋 JavaScript 的反應性VueJavaScript
- 利用 K8S 的反親和性構建高可用應用K8S
- win10右鍵此電腦屬性沒反應怎麼辦 win10電腦右鍵屬性沒反應修復方法Win10
- 使用RSocket進行服務通訊的反應性服務 - 負載平衡和可恢復性 | Rafał Kowalski負載
- 《反應式應用開發》之“什麼是反應式應用”
- vue 響應性程式碼demoVue
- .Net 中的反應式程式設計程式設計
- 滑鼠反應遲鈍與反應慢故障解決方法
- Node.JS程式設計師的反應Node.js程式設計師
- 程式猿被提bug之後的反應
- 常見的反爬蟲和應對方法爬蟲
- 非易失性NV-SRAM的應用
- Java的Void方法是反模式的? - DZoneJava模式
- 使用RSocket進行服務通訊的反應性服務簡介 - Rafał Kowalski
- 什麼是反應式程式設計?程式設計
- 反爬蟲的應對措施爬蟲
- Java反應式框架Reactor中的Mono和FluxJava框架ReactMonoUX
- android make 沒反應Android
- 反爬蟲應對策略爬蟲
- 《反應式宣言》——TheReactiveManifestoReact
- 不同人對BUG的反應,程式設計師:誰動了我的程式碼?程式設計師
- 聊聊Spring Reactor反應式程式設計SpringReact程式設計
- 反應式程式設計讀書筆記程式設計筆記
- 反應式程式設計在微服務下的重生程式設計微服務
- 程式設計師遇到Bug時的30個反應程式設計師
- 程式設計師遇到bug後的七種反應程式設計師
- Android反編譯和程式碼混淆Android編譯
- 原始碼、反碼和補碼原始碼
- 滑鼠右鍵沒反應怎麼處理 滑鼠右鍵點了沒反應
- 什麼是反應式應用開發?
- 如何應對反爬蟲措施?爬蟲
- “企業應急響應和反滲透”之真實案例分析
- 反應式程式設計是正確的方法嗎? - JAXenter程式設計
- Go Web 應用中常見的反模式GoWeb模式
- 空間反演對稱性 (Spatial Inversion Symmetry) 和非線性響應 (Non-linear Response)
- [BUG反饋]登陸沒反應,審查元素提示錯誤