使用反應式程式設計替換Java自動資源管理 - Arvind
自動資源管理(Automatic resource management 簡稱ARM)在 Java 7 中首次引入時是一個受歡迎的特性,也就是通常說的無需finally的try()用法。
然後ARM 繼續以意想不到的方式汙染程式碼。這樣做的一個重要原因是try塊的結尾代表了需要釋放資源的時刻,該塊的結束也代表異常處理程式、區域性變數作用域等的結束。
因此,雖然 ARM 確保可靠地釋放事物,但它在資源有資格被釋放時失去了一些控制權. 試圖改變釋出的時間會干擾整個程式碼流。
以下是 ARM 如何引入非預期程式碼塊的示例。
try (val client = HttpClientBuilder.create().build()) { HttpGet request = new HttpGet(urlPath); request.setHeader("content-type", "application/json"); try (val httpResponse = client.execute(request)) { if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String entityString = EntityUtils.toString(httpResponse.getEntity()); PaginatedResponse<City> siloResponse = gson.fromJson(entityString, new TypeToken<PaginatedResponse<City>>(){}.getType()); cities.addAll(siloResponse.getResults()); if (siloResponse.getNext() != null) { recursiveRead(cities, siloResponse.getNext()); } else { log.info("Fetched:", cities.size()); } } else { log.error("Failed to get Cities"); } } } |
請注意,物件client和httpResponse物件都在 ARM 下。這引入的縮排和變數作用域實際上是一種眼中釘並損害了易用性。這讓我想到了下一點……
有一個非常微妙的效能錯誤
注意以下幾點
- 頁面的遍歷使用遞迴實現
- 僅當方法本身完成時,託管資源如client和httpResponse才有資格被釋放
這意味著如果一個呼叫跨越 200 個頁面,那麼有 200 個client物件同時處於活動狀態,並且在整個結果集被迭代之前它們沒有資格被釋放。事實證明,這個模組嵌入在一個微服務中,可以擴充套件到超過一百個例項。
所以,如果有一個查詢確實返回了 200 頁的結果,這段程式碼就會在遠端伺服器上建立一個 200 倍的網路放大攻擊!
即使在命令式世界中,也可以直接解決此問題。困難的部分是首先知道存在問題。那麼這在反應式世界中是如何實現的呢?
Mono<PaginatedResponse<City>> siloResponse = WebClient.create().get() .uri(urlPath) .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(new ParameterizedTypeReference<PaginatedResponse<City>>() { }); |
這是使用流暢的功能風格不僅僅是語法糖的部分。Spring webclient 的建立者做了一些聰明的事情,其中bodyToMono充當隱式訊號,表示您已完成與該 HTTP 請求相關的所有網路互動,並且我們已完成根據需要提取響應。
程式碼:https://github.com/anomalizer/rx-stream-blog
取消檢查異常和異常型別
多年來,人們普遍認為 Java 中最大的語言設計錯誤之一是檢查異常的概念。
幾十年前,錯誤在軟體世界中很少見。原因是多方面的,與本次討論無關。相關的是,當時的程式設計師在執行理論上可能出錯的操作時不會檢查潛在的錯誤。隨著時間的推移,程式遇到錯誤的機率增加,並且選擇忽略錯誤導致難以在應用程式級別除錯問題。Java 的第一個版本是在那個時代設計的,因此,語言設計者希望提供一種能力,其中任何庫/模組的建立者都能夠傳達預期的錯誤,並且這些庫/模組的呼叫者更好地準備處理他們。檢查異常是強制這種行為的好方法。
當前圍繞檢查異常的遺憾反映了當前時代的問題。首先,我們生活在一個“萬事皆失敗”的時代。其次,任何現代應用程式堆疊都包含來自多個開源專案、多個商業供應商以及可能來自多個內部團隊的庫。最終應用程式層中的錯誤處理邏輯通常僅限於簡單區分成功條件與出現問題的情況。很少用一種程式碼來破譯異常型別、解開其中的欄位等,並且對錯誤進行復雜的條件處理。
Java 中的函數語言程式設計不允許存在已檢查的異常,而反應式程式設計在某種程度上鼓勵將編碼模式限制為 aonSuccess()和onError()處理程式。這在非同步處理的世界中成為絕對必須的,因為異常會發生在呼叫堆疊(即執行緒)上,而呼叫堆疊(即執行緒)與正在使用結果的呼叫堆疊完全不同。在檢查很久以前完成的操作的結果時引發異常會非常奇怪。
優雅的取消協作:殺手級功能
微服務世界中使用的一種近乎通用的構造是能夠“超時”未完成的操作並繼續前進。這些超時是跨程式內庫邊界和程式間邊界實現的。我們將通過考慮分頁需要遍歷 100 頁才能產生最終結果並且“呼叫者”在遍歷 2 頁後宣佈超時的情況來過度誇大超時模式。
至少需要兩個執行緒才能在命令式方面發生這種情況。第一個執行緒是執行庫的給定呼叫的地方,第二個執行緒是執行呼叫者的地方;等待計時器關閉或返回結果。當計時器響起時,呼叫者繼續做其他事情。如果您想知道如何實現這一點,請考慮Future. 然而,其中fetchCity() 方法正在執行的第一個執行緒沒有注意到呼叫者已經離開的事實。相反,它將繼續遍歷剩餘的 98 頁,組合結果並返回。
反應式版本隱式地理解超時並在儘可能早的時間停止未來的處理。這可能根本不明顯,所以讓我解釋一下這是如何發生的。為此,我們需要考慮觸發超時時程式碼可能處於的所有可能的執行狀態
Mono<PaginatedResponse<City>> siloResponse = WebClient.create().get() .uri(urlPath) .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(new ParameterizedTypeReference<PaginatedResponse<City>>() { }); Flux<City> currentPage = siloResponse.map(PaginatedResponse::getResults).flatMapMany(Flux::fromIterable); Flux<City> nextPage = siloResponse.map(PaginatedResponse::getNext) .filter(x -> !Strings.isNullOrEmpty(x)) .flatMapMany(ReactiveApproach::recursiveRead2); return Flux.concat(currentPage, nextPage); |
取消流程需要從最後一行開始向後處理,我們將這樣做。
- concat 方法將根據它接下來要處理的一個currentPage或一個發出取消nextPage
- 如果currentPage正在執行,它將向siloResponseI/O 呼叫或map/flayMayMap呼叫發出取消,具體取決於它是哪個階段
- 如果nextPage正在執行,則它正在等待遞迴呼叫的結果,即flatMapMany(ReactiveApproach::recursiveRead2)。取消被簡單地推入這個巢狀呼叫中。取消的處理隨後在巢狀呼叫中遵循相同的整體流程。
- 如果siloResponse正在執行,Spring Boot 的WebClient(siloResponse 物件)將中止底層 HTTP 請求/響應處理。
之所以所有這一切都是可能的,是因為反應模型是基於拉動的模型。呼叫者可以通過各種機制發出訊號,表明它不再對結果感興趣。設定超時就是這樣一種機制。一旦發出這個“不感興趣”的訊號,反應器就會知道結果沒有消費者/訂閱者,因此它會停止整個流程。
試圖在命令式模型中完成這樣的事情是非常具有侵入性的。有兩種方法可以解決這個問題:使用執行緒中斷或顯式傳遞取消訊息持有者物件。然後必須if(notCancelled/notInterrupted){}在每個階段對程式碼進行檢查,以防止浪費計算。
相關文章
- 什麼是反應式程式設計?程式設計
- 聊聊Spring Reactor反應式程式設計SpringReact程式設計
- Linux系統程式設計之程式替換:exec 函式族Linux程式設計函式
- 【Perl程式設計-6】正規表示式--替換+轉化程式設計
- 反應式程式設計讀書筆記程式設計筆記
- 解構反應式程式設計——Java8,RxJava,Reactor之比較程式設計RxJavaReact
- Java程式設計中資源物件管理的進化Java程式設計物件
- 使用Java 9 Flow進行響應式程式設計Java程式設計
- 設計原則之【裡式替換原則】
- 反應式程式設計在微服務下的重生程式設計微服務
- 使用Reactor響應式程式設計React程式設計
- 【OracleSQL】常用自動替換總結OracleSQL
- 反應式程式設計是正確的方法嗎? - JAXenter程式設計
- Java 正規表示式替換斜槓Java
- centos 替換yum源CentOS
- SQL中的替換函式replace()使用SQL函式
- Java開發人員的反應程式設計介紹 - Fernando AlmeidaJava程式設計NaN
- 介面自動化之引數動態生成替換
- 如果程式設計替換成中文就會怎樣? 程式設計師看了表示頭疼程式設計師
- [靈性程式設計]函式委託,自動事件程式設計函式事件
- winget 替換國內源
- Linux系統程式設計之程式控制(程式建立、終止、等待及替換)Linux程式設計
- Java資料型別自動轉換(++ ,+=)Java資料型別
- Java 網路程式設計 —— 非阻塞式程式設計Java程式設計
- 談反應式程式設計在服務端中的應用,資料庫操作優化,提速 Upsert程式設計服務端資料庫優化
- 程式等待和程式替換
- java try(){}catch(){}自動資源釋放Java
- Spring 程式設計式事務管理Spring程式設計
- 設計模式六大原則(二)----裡式替換原則設計模式
- 不同人對BUG的反應,程式設計師:誰動了我的程式碼?程式設計師
- Java反應式事件溯源:領域Java事件
- 揚帆起航:從指令式程式設計到函式響應式程式設計程式設計函式
- 設計模式例項講解 - 里氏替換設計模式
- 使用JavaScript設定Tab欄自動切換JavaScript
- 函式響應式程式設計與RxSwift函式程式設計Swift
- spring AOP 程式設計式應用Spring程式設計
- 好程式設計師Java培訓分享SpringBoot -自動配置原理程式設計師JavaSpring Boot
- 記一次正規表示式替換,使用 ideaIdea