使用反應式程式設計替換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){}在每個階段對程式碼進行檢查,以防止浪費計算。
相關文章
- .Net 中的反應式程式設計程式設計
- 什麼是反應式程式設計?程式設計
- 【Perl程式設計-6】正規表示式--替換+轉化程式設計
- 聊聊Spring Reactor反應式程式設計SpringReact程式設計
- 反應式程式設計讀書筆記程式設計筆記
- 解構反應式程式設計——Java8,RxJava,Reactor之比較程式設計RxJavaReact
- Linux系統程式設計之程式替換:exec 函式族Linux程式設計函式
- Linux Shell程式設計(28)——程式替換Linux程式設計
- java正規表示式替換Java
- 使用Java 9 Flow進行響應式程式設計Java程式設計
- 設計原則之【裡式替換原則】
- 【OracleSQL】常用自動替換總結OracleSQL
- 反應式程式設計在微服務下的重生程式設計微服務
- Linux Shell程式設計(24)——命令替換Linux程式設計
- 使用Reactor響應式程式設計React程式設計
- gulp自動化構建html靜態資源路徑版本號新增和替換HTML
- 反應式程式設計是正確的方法嗎? - JAXenter程式設計
- Java程式設計中資源物件管理的進化Java程式設計物件
- Linux Shell程式設計(6)——變數替換Linux程式設計變數
- Java 正規表示式替換斜槓Java
- centos 替換yum源CentOS
- SQL中的替換函式replace()使用SQL函式
- 介面自動化之引數動態生成替換
- 自適應網頁設計/響應式Web設計網頁Web
- Java資料型別自動轉換(++ ,+=)Java資料型別
- RedHat 7 yum源替換Redhat
- [靈性程式設計]函式委託,自動事件程式設計函式事件
- Java併發程式設計資源總目錄Java程式設計
- 多國語言程式設計之資源轉換 (轉)程式設計
- java 替換特殊字元Java字元
- java try(){}catch(){}自動資源釋放Java
- js使用replace()函式替換所有指定字元JS函式字元
- python使用正規表示式文字替換Python
- Node.JS程式設計師的反應Node.js程式設計師
- 如果程式設計替換成中文就會怎樣? 程式設計師看了表示頭疼程式設計師
- Java開發人員的反應程式設計介紹 - Fernando AlmeidaJava程式設計NaN
- winget 替換國內源
- Java反應式事件溯源:領域Java事件