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