OkHttp 在 Java 和 Android 世界中被廣泛使用,深入學習源代碼有助于掌握軟件特性和提高編程水平。
本文首先從源代碼入手簡要分析了一個請求發起過程中的核心代碼,接著通過流程圖和架構圖概括地介紹了OkHttp的整體結構,重點分析了攔截器的責任鏈模式設計,最后列舉了OkHttp攔截器在項目中的實際應用。
一、背景介紹
在生產實踐中,常常會遇到這樣的場景:需要針對某一類 Http 請求做統一的處理,例如在 Header 里添加請求參數或者修改請求響應等等。這類問題的一種比較優雅的解決方案是使用攔截器來對請求和響應做統一處理。
在 Android 和 Java 世界里 OkHttp 憑借其高效性和易用性被廣泛使用。作為一款優秀的開源 Http 請求框架,深入了解它的實現原理,可以學習優秀軟件的設計和編碼經驗,幫助我們更好到地使用它的特性,并且有助于特殊場景下的問題排查。本文嘗試從源代碼出發探究 OkHttp 的基本原理,并列舉了一個簡單的例子說明攔截器在我們項目中的實際應用。本文源代碼基于 OkHttp 3.10.0。
二、OkHttp 基本原理
2.1 從一個請求示例出發
OkHttp 可以用來發送同步或異步的請求,異步請求與同步請求的主要區別在于異步請求會交由線程池來調度請求的執行。使用 OkHttp 發送一個同步請求的代碼相當簡潔,示例代碼如下:
同步 GET 請求示例
//?1.創建OkHttpClient客戶端 OkHttpClient?client?=?new?OkHttpClient(); public?String?getSync(String?url)?throws?IOException?{ ??????OkHttpClient?client?=?new?OkHttpClient(); ??????//?2.創建一個Request對象 ??????Request?request?=?new?Request.Builder() ??????????????.url(url) ??????????????.build(); ??????//?3.創建一個Call對象并調用execute()方法 ??????try?(Response?response?=?client.newCall(request).execute())?{ ??????????return?response.body().string(); ??????} ??}
其中 execute() 方法是請求發起的入口,RealCall 對象的 execute() 方法的源代碼如下:
RealCall 的 execute() 方法源代碼
@Override?public?Response?execute()?throws?IOException?{ ??synchronized?(this)?{?//?同步鎖定當前對象,將當前對象標記為“已執行” ????if?(executed)?throw?new?IllegalStateException("Already?Executed"); ????executed?=?true; ??} ??captureCallStackTrace();?//?捕獲調用棧 ??eventListener.callStart(this);?//?事件監聽器記錄“調用開始”事件 ??try?{ ????client.dispatcher().executed(this);?//?調度器將當前對象放入“運行中”隊列 ????Response?result?=?getResponseWithInterceptorChain();?//?通過攔截器發起調用并獲取響應 ????if?(result?==?null)?throw?new?IOException("Canceled"); ????return?result; ??}?catch?(IOException?e)?{ ????eventListener.callFailed(this,?e);?//?異常時記錄“調用失敗事件” ????throw?e; ??}?finally?{ ????client.dispatcher().finished(this);?//?將當前對象從“運行中”隊列移除 ??} }
execute() 方法首先將當前請求標記為“已執行”,然后會為重試跟蹤攔截器添加堆棧追蹤信息,接著事件監聽器記錄“調用開始”事件,調度器將當前對象放入“運行中”隊列 ,之后通過攔截器發起調用并獲取響應,最后在 finally 塊中將當前請求從“運行中”隊列移除,異常發生時事件監聽器記錄“調用失敗”事件。其中關鍵的方法 是
getResponseWithInterceptorChain() 其源代碼如下:
Response?getResponseWithInterceptorChain()?throws?IOException?{ ????//?構建一個全棧的攔截器列表 ????List?interceptors?=?new?ArrayList<>(); ????interceptors.addAll(client.interceptors()); ????interceptors.add(retryAndFollowUpInterceptor); ????interceptors.add(new?BridgeInterceptor(client.cookieJar())); ????interceptors.add(new?CacheInterceptor(client.internalCache())); ????interceptors.add(new?ConnectInterceptor(client)); ????if?(!forWebSocket)?{ ??????interceptors.addAll(client.networkInterceptors()); ????} ????interceptors.add(new?CallServerInterceptor(forWebSocket)); ? ????Interceptor.Chain?chain?=?new?RealInterceptorChain(interceptors,?……); ? ????return?chain.proceed(originalRequest); ??}
該方法中按照特定的順序創建了一個有序的攔截器列表,之后使用攔截器列表創建攔截器鏈并發起 proceed() 方法調用。在chain.proceed() 方法中會使用遞歸的方式將列表中的攔截器串聯起來依次對請求對象進行處理。攔截器鏈的實現是 OkHttp 的一個巧妙所在,在后文我們會用一小節專門討論。在繼續往下分析之前,通過以上的代碼片段我們已經大致看到了一個請求發起的整體流程。
2.2 OkHttp 核心執行流程
一個 OkHttp 請求的核心執行過程如以下流程圖所示:
圖 2-1 OkHttp請求執行流程圖
圖中各部分的含義和作用如下:
OkHttpClient :是整個 OkHttp 的核心管理類,從面向對象的抽象表示上來看它代表了客戶端本身,是請求的調用工廠,用來發送請求和讀取響應。在大多數情況下這個類應該是被共享的,因為每個 Client 對象持有自己的連接池和線程池。重復創建則會造成在空閑池上的資源浪費。Client對象可以通過默認的無參構造方法創建也可以通過 Builder 創建自定義的 Client 對象。Client 持有的線程池和連接池資源在空閑時可以自動釋放無需客戶端代碼手動釋放,在特殊情況下也支持手動釋放。
Request :一個 Request 對象代表了一個 Http 請求。它包含了請求地址 url,請求方法類型 method ,請求頭 headers,請求體 body 等屬性,該對象具有的屬性普遍使用了 final 關鍵字來修飾,正如該類的說明文檔中所述,當這個類的 body 為空或者 body 本身是不可變對象時,這個類是一個不可變對象。
Response :一個 Response 對象代表了一個 Http 響應。這個實例對象是一個不可變對象,只有 responseBody 是一個可以一次性使用的值,其他屬性都是不可變的。
RealCall :一個 RealCall 對象代表了一個準備好執行的請求調用。它只能被執行一次。同時負責了調度和責任鏈組織的兩大重任。
Dispatcher :調度器。它決定了異步調用何時被執行,內部使用 ExecutorService 調度執行,支持自定義 Executor。
EventListener :事件監聽器。抽象類 EventListener 定義了在一個請求生命周期中記錄各種事件的方法,通過監聽各種事件,可以用來捕獲應用程序 HTTP 請求的執行指標。從而監控 HTTP 調用的頻率和性能。
Interceptor :攔截器。對應了軟件設計模式中的攔截器模式,攔截器可用于改變、增強軟件的常規處理流程,該模式的核心特征是對軟件系統的改變是透明的和自動的。OkHttp 將整個請求的復雜邏輯拆分成多個獨立的攔截器實現,通過責任鏈的設計模式將它們串聯到一起,完成發送請求獲取響應結果的過程。
2.3 OkHttp 整體架構
通過進一步閱讀 OkHttp 源碼,可以看到 OkHttp 是一個分層的結構。軟件分層是復雜系統設計的常用手段,通過分層可以將復雜問題劃分成規模更小的子問題,分而治之。同時分層的設計也有利于功能的封裝和復用。OkHttp 的架構可以分為:應用接口層,協議層,連接層,緩存層,I/O層。不同的攔截器為各個層次的處理提供調用入口,攔截器通過責任鏈模式串聯成攔截器鏈,從而完成一個Http請求的完整處理流程。如下圖所示:
圖 2-2 OkHttp架構圖(圖片來自網絡)
2.4 OkHttp 攔截器的種類和作用
OkHttp 的核心功能是通過攔截器來實現的,各種攔截器的作用分別為:
client.interceptors :由開發者設置的攔截器,會在所有的攔截器處理之前進行最早的攔截處理,可用于添加一些公共參數,如自定義 header、自定義 log 等等。
RetryAndFollowUpInterceptor :主要負責進行重試和重定向的處理。
BridgeInterceptor :主要負責請求和響應的轉換。把用戶構造的 request 對象轉換成發送到服務器 request對象,并把服務器返回的響應轉換為對用戶友好的響應。
CacheInterceptor :主要負責緩存的相關處理,將 Http 的請求結果放到到緩存中,以便在下次進行相同的請求時,直接從緩存中讀取結果,提高響應速度。
ConnectInterceptor :主要負責建立連接,建立 TCP 連接或者 TLS 連接。
client.networkInterceptors :由開發者設置的攔截器,本質上和第一個攔截器類似,但是由于位置不同,所以用處也不同。
CallServerInterceptor :主要負責網絡數據的請求和響應,也就是實際的網絡I/O操作。將請求頭與請求體發送給服務器,以及解析服務器返回的 response。
除了框架提供的攔截器外,OkHttp 支持用戶自定義攔截器來對請求做增強處理,自定義攔截器可以分為兩類,分別是應用程序攔截器和網絡攔截器,他們發揮作用的層次結構如下圖:
圖 2-3 攔截器(圖片來自OkHttp官網)
不同的攔截器有不同的適用場景,他們各自的優缺點如下:
應用程序攔截器
無需擔心重定向和重試等中間響應。
總是被調用一次,即使 HTTP 響應是從緩存中提供的。
可以觀察到應用程序的原始請求。不關心 OkHttp 注入的標頭。
允許短路而不調用 Chain.proceed()方法。
允許重試并多次調用 Chain.proceed()方法。
可以使用 withConnectTimeout、withReadTimeout、withWriteTimeout 調整呼叫超時。
網絡攔截器
能夠對重定向和重試等中間響應進行操作。
緩存響應不會調用。
可以觀察到通過網絡傳輸的原始數據。
可以訪問攜帶請求的鏈接。
2.5 責任鏈模式串聯攔截器調用
OkHttp 內置了 5 個核心的攔截器用來完成請求生命周期中的關鍵處理,同時它也支持添加自定義的攔截器來增強和擴展 Http 客戶端,這些攔截器通過責任鏈模式串聯起來,使得的請求可以在不同攔截器之間流轉和處理。
2.5.1 責任鏈模式
責任鏈模式 是一種行為設計模式, 允許將請求沿著處理者鏈發送。收到請求后, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下一個處理者。
適用場景 包括:
當程序需要使用不同方式處理不同種類的請求時
當程序必須按順序執行多個處理者時
當所需要的處理者及其順序必須在運行時進行改變時
優點:
可以控制請求處理的順序
可對發起操作和執行操作的類進行解耦。
可以在不更改現有代碼的情況下在程序中新增處理者。
2.5.2 攔截器的串聯
責任鏈的入口從第一個 RealInterceptorChain 對象的 proceed() 方法調用開始。這個方法的設計非常巧妙,在完整的 proceed() 方法里會做一些更為嚴謹的校驗,去掉這些校驗后該方法的核心代碼如下:
public?Response?proceed(Request?request,?StreamAllocation?streamAllocation,?HttpCodec?httpCodec,?RealConnection?connection)?throws?IOException?{ ????if?(index?>=?interceptors.size())?throw?new?AssertionError(); ? ????//?……?? ????//?Call?the?next?interceptor?in?the?chain. ????RealInterceptorChain?next?=?new?RealInterceptorChain(interceptors,?streamAllocation,?httpCodec,?connection,?index?+?1,?request,?call,?eventListener,?connectTimeout,?readTimeout,?writeTimeout); ????Interceptor?interceptor?=?interceptors.get(index); ????Response?response?=?interceptor.intercept(next); ? ????//?…… ????return?response; ??}
這段代碼可以看成三個步驟:
索引判斷。 index 初始值為0,它指示了攔截器對象在列表中的索引順序,每執行一次 proceed 方法該參數自增1,當索引值大于攔截器列表的索引下標時異常退出。
創建 下一個責任鏈對象。
按照索引順序獲取 一個攔截器,并調用 intercept() 方法。
責任鏈串聯
單獨看這個方法似乎并不能將所有攔截器都串聯起來,串聯的關鍵在于 intercept() 方法,intercept() 方法是實現 interceptor 接口時必須要實現的方法,該方法持有下一個責任鏈 對象 chain,在攔截器的實現類里只需要在 intercept() 方法里的適當地方再次調用 chain.proceed() 方法,程序指令便會重新回到以上代碼片段,于是就可以觸發對于下一個攔截器的查找和調用了,在這個過程中攔截器對象在列表中的先后順序非常重要,因為攔截器的調用順序就是其在列表中的索引順序。
遞歸方法
從另一個角度來看,proceed() 方法可以看成是一個遞歸方法。遞歸方法的基本定義為“函數的定義中調用函數自身”,雖然 proceed() 方法沒有直接調用自身,但是除了最后一個攔截器以外,攔截器鏈中的其他攔截器都會在適當的位置調用 chain.proceed() 方法,責任鏈對象和攔截器對象合在一起則組成了一個自己調用自己的邏輯循環。按照筆者個人理解,這也是為什么源代碼里 Chain 接口被設計成 Interceptor 接口的內部接口,在理解這段代碼的時候要把它們兩個接口當成一個整體來看,從這樣的角度看的話,這樣的接口設計是符合“高內聚”的原則的。
攔截器 interceptor 和責任鏈 chain 的關系如下圖:
圖 2-5 Interceptor 和 Chain 的關系圖
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
三、OkHttp 攔截器在項目中的應用
在我們的項目中,有一類請求需要在請求頭 Header 中添加認證信息,使用攔截器來實現可以極大地簡化代碼,提高代碼可讀性和可維護性。核心代碼只需要實現符合業務需要的攔截器如下:
添加請求頭的攔截器
public?class?EncryptInterceptor?implements?Interceptor?{
????@Override ????public?Response?intercept(Chain?chain)?throws?IOException?{ ????????Request?originRequest?=?chain.request(); ? ????????//?計算認證信息 ????????String?authorization?=?this.encrypt(originRequest); ????????? ????????//?添加請求頭 ????????Request?request?=?originRequest.newBuilder() ????????????????.addHeader("Authorization",?authorization) ????????????????.build(); ????????//?向責任鏈后面傳遞 ????????return?chain.proceed(request); ????} }
之后在創建 OkHttpClient 客戶端的時候,使用 addInterceptor() 方法將我們的攔截器注冊成應用程序攔截器,即可實現自動地、無感地向請求頭中添加實時的認證信息的功能。
注冊應用程序攔截器
OkHttpClient?client?=?new?OkHttpClient.Builder() ????.addInterceptor(new?EncryptInterceptor()) ????.build();
四、回顧總結
OkHttp 在 Java 和 Android 世界中被廣泛使用,通過使用 OkHttp 攔截器可以解決一類問題——針對一類請求統一修改請求或響應內容。深入了解 OkHttp 的設計和實現不僅可以幫助我們學習優秀開源軟件的設計和編碼經驗,也有利于更好地使用軟件特性以及對特殊場景下問題的排查。本文嘗試從一個同步 GET 請求的例子開始,首先通過源代碼片段簡要分析了一個請求發起過程中涉及的核心代碼,接著用流程圖的形式總結了請求執行過程,然后用架構圖展示了OkHttp的分層設計,介紹了各種攔截器的用途、工作層次及優缺點,之后著重分析了攔截器的責任鏈模式設計——本質是一個遞歸調用,最后用一個簡單的例子介紹了 OkHttp 攔截器在實際生產場景中的應用。
編輯:黃飛
?
評論
查看更多