OkHttp3原始碼分析之攔截器Interceptor
前言
在上一篇部落格中,我們從原始碼分析了,一次非同步網路請求的整個大概表面的流程,但是涉及到某些具體的內容呢,就直接帶過了。本篇文章我們就先來了解一下在發起一次網路請求時,OkHttp是怎麼發起請求獲取響應的。這裡邊就涉及到OkHttp的一個很棒的設計——攔截器Interceptor。
分析
原始碼基於最新的版本:3.10.0。
還記得上一篇部落格中一次非同步任務中,到最後一步執行的程式碼嗎?
那就是RealCall.AsyncCall#execute()
方法
@Override
protected void execute() {
boolean signalledCallback = false ;
try {
//執行具體的耗時任務
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
//回撥,注意這裡回撥是線上程池中
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
//回撥,同上
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
我們注意到其實真正執行網路請求,或者說入口是在
Response response = getResponseWithInterceptorChain();
這一句程式碼中。
另外,在發起同步請求的時候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);
}
}
核心入口是一樣,只不過我們要知道,同步請求的時候是在主執行緒中執行的,而非同步請求我們在上一篇部落格中也分析過了,是線上程池中執行的。
下面我們就來看一下RealCall#getResponseWithInterceptorChain()
這個方法:
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> 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, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
首先呢,正如註釋中所說,構建一整套攔截器,就是將一個個不同的攔截器新增到一個集合中,上邊各個攔截器的功能作用我們下邊再說,先接著往下看程式碼。
Interceptor
是一個介面,它有個內部介面Chain
,它的具體的實現類RealInterceptorChain
,顧名思義,就是攔截器鏈,就是將一個個攔截器串起來,然後執行RealInterceptorChain#proceed(Request request)
方法來返回得到Response
。
那麼上邊具體的實現是在另一個過載方法裡邊,下邊看一下這個過載的方法proceed(...)
都幹了什麼?
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// 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);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
上邊程式碼去除一些錯誤丟擲判斷的內容後,主要的邏輯就是在喚起鏈中的下一個攔截器這三行程式碼。
①第一行:
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
建立一個RealInterceptorChain物件。注意這裡傳入的一個引數index+1
。
②第二行:
Interceptor interceptor = interceptors.get(index);
獲得集合中當前索引的攔截器,index
初始為0。
③第三行:
Response response = interceptor.intercept(next);
呼叫當前索引攔截器的intercept(Chain chain)
方法。
我們就拿集合中第一個攔截器RetryAndFollowUpInterceptor
為例,
看一下RetryAndFollowUpInterceptor#intercept(Chain chain)
方法:
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
//執行(index+1)攔截器鏈的proceed()方法
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
...
}
}
上邊最關鍵的程式碼,其實也就是這一句:
response = realChain.proceed(request, streamAllocation, null, null);
我們會發現,這一句執行的是下一個攔截器鏈的proceed
方法,接著回到上邊,會呼叫下一個攔截器的intercept()
方法,這樣反覆相互遞迴呼叫,直到鏈的盡頭,即攔截器鏈的集合已經遍歷完成。
我們檢視其它攔截器的原始碼可以發現,他們的樣式非常類似:
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1、在發起請求前對request進行處理
// 2、遞迴呼叫下一個攔截器遞迴,獲取response呼叫
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
//3、對response進行處理,返回給上一個攔截器
return response;
}
}
那麼分析到這裡,我們思考一個問題,也可能面試中會問到:
這裡為什麼每次都重新建立RealInterceptorChain
物件next
,為什麼不直接複用上一層的RealInterceptorChain
物件?
因為這裡是遞迴呼叫,在呼叫下一層攔截器的interupter()方法的時候,本層的 response階段還沒有執行完成,如果複用RealInterceptorChain物件,必然導致下一層修改該RealInterceptorChain物件,所以需要重新建立RealInterceptorChain物件。
我們通過一張經典圖來描述一下上面說的整個邏輯,我們可以看的更清晰一些:
這裡的攔截器分層的思想就是借鑑的網路裡的分層模型的思想。請求從最上面一層到最下一層,響應從最下一層到最上一層,每一層只負責自己的任務,對請求或響應做自己負責的那塊的修改。
原始碼中核心攔截器
原始碼中核心攔截器完成了網路訪問的核心邏輯,由上邊往list中新增攔截器的順序,我們可以知道它依次呼叫順序(也可以在上圖中看出),下邊大致瞭解一下各個攔截器所負責的內容:
RetryAndFollowUpInterceptor
①在網路請求失敗後進行重試;
②當伺服器返回當前請求需要進行重定向時直接發起新的請求,並在條件允許情況下複用當前連線BridgeInterceptor
這個攔截器是在v3.4.0之後由HttpEngine
拆分出來的。
①設定內容長度,內容編碼
②設定gzip壓縮,並在接收到內容後進行解壓。省去了應用層處理資料解壓的麻煩
說到這裡有一個小坑需要注意:
原始碼中註釋也有說明,
If we add an "Accept-Encoding: gzip" header field,
we're responsible for also decompressing the transfer stream.
就是說OkHttp是自動支援gzip壓縮的,也會自動新增header,這時候如果你自己添加了"Accept-Encoding: gzip"
,它就不管你了,就需要你自己解壓縮。
③新增cookie
④設定其他報頭,如User-Agent,Host,Keep-alive等。其中Keep-Alive是實現多路複用的必要步驟CacheInterceptor
這個攔截器是在v3.4.0之後由HttpEngine
拆分出來的。
①當網路請求有符合要求的Cache時直接返回Cache
②當伺服器返回內容有改變時更新當前cache
③如果當前cache失效,刪除ConnectIntercetot
這個攔截器是在v3.4.0之後由HttpEngine
拆分出來的。
為當前請求找到合適的連線,可能複用已有連線也可能是重新建立的連線,返回的連線由連線池負責決定CallServerInterceptor
負責向伺服器發起真正的訪問請求,並在接收到伺服器返回後讀取響應返回。
自定義攔截器
除了官方已經寫好的比較核心的攔截器之外,我們還可以自定義攔截器。比如官方給的一個LoggingInterceptor
來列印請求或者響應的時間日誌;再比如我們在向伺服器傳輸比較大的資料的時候,想對post的資料進行Gzip壓縮,這個可以參考github上的這個issue:
Add GZip Request Compression #350
或者我之前寫的一篇部落格:
Okhttp3請求網路開啟Gzip壓縮
首先我們看一張官方wiki的圖:
攔截器可以被註冊為應用攔截器(application interceptors)或者網路攔截器(network interceptors)。
其實在上邊原始碼分析中,RealCall#getResponseWithInterceptorChain()
這個方法去構建一個攔截器集合的時候(也決定了其遞迴呼叫順序):
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());//Application 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());//Network Interceptors
}
interceptors.add(new CallServerInterceptor(forWebSocket));
...
return chain.proceed(originalRequest);
}
我們可以知道應用攔截器和網路攔截器在遞迴呼叫的順序。
那麼,這兩者到底有什麼區別呢?這裡給出官方wiki的翻譯:
Application Interceptors
通過OkHttpClient.Builder
的addInterceptor()
註冊一個 application interceptor:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
URLhttp://www.publicobject.com/helloworld.txt
會重定向到https://publicobject.com/helloworld.txt
,
OkHttp
會自動跟蹤這次重定向。application interceptor會被呼叫一次,並且通過呼叫chain.proceed()
返回攜帶有重定向後的response。
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
我們可以看到,會重定向是因為response.request().url()
和request.url()
是不同的,日誌也列印了兩個不同的URL。
Network Interceptors
註冊一個Network Interceptors的方式是非常類似的,只需要將addInterceptor()
替換為addNetworkInterceptor()
:
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
當我們執行上面這段程式碼,這個interceptor會執行兩次。一次是呼叫在初始的request http://www.publicobject.com/helloworld.txt
,另外一次是呼叫在重定向後的request https://publicobject.com/helloworld.txt
。
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
那麼我們如何選擇這兩種攔截器的使用?它們各有各的優缺點,我們可以根據自己需要來合理選擇。
Application Interceptors
- 不需要關心由重定向、重試請求等造成的中間response產物。
- 總會被呼叫一次,即使HTTP response是從快取(cache)中獲取到的。
- 關注原始的request,而不關心注入的headers,比如If-None-Match。
- interceptor可以被取消呼叫,不呼叫Chain.proceed()。
- interceptor可以重試和多次呼叫Chain.proceed()。
Network Interceptors
- 可以操作由重定向、重試請求等造成的中間response產物。
- 如果是從快取中獲取cached responses ,導致中斷了network,是不會呼叫這個interceptor的。
- 資料在整個network過程中都可以通過Network Interceptors監聽。
- 可以獲取攜帶了request的Connection。
結語
本篇文章算是接著上篇文章將整個網路請求中重要的互動步驟作了原始碼分析,詳細瞭解了攔截器的相關內容。
在之後,我們再對OkHttp的任務排程佇列,快取,連線等方面的內容作深入瞭解。