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