OkHttp4.3源碼解析之 - RetryAndFollowUpInterceptor
回顧
上一篇文章:發起請求
大家還記得OkHttp是如何發起一條請求的嗎?上面這篇文章裏介紹了OkHttp是在什麼時候把多個攔截器加入到責任鏈中的。如果大家沒看過的可以先去了解一下,因爲這個流程和本文息息相關。
如果是忘了的話我們再簡單的回顧一遍:
- 構建OkHttpClient對象
- 構建Request對象
- 使用enqueue()發起請求,並處理回調
在第一步裏,我們可以通過addInterceptor(xxx)來添加自定義的攔截器,在第三步裏,我們通過源碼可以看到在RealCall中通過getResponseWithInterceptorChain()方法來處理這些攔截器。
一個簡單的例子
現在有個需求,希望每個請求都能把請求時間給打印出來,該怎麼做呢?
OkHttpTest類
class OkHttpTest {
internal fun test() {
Request.Builder()
.url("https://www.baidu.com").build().let { request ->
OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor()) // 添加自定義的攔截器
.build()
.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("bluelzy --- ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
println("bluelzy --- ${response.body.toString()}")
}
})
}
}
}
LoggingInterceptor
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startTime = System.nanoTime()
logger.info(String.format("Bluelzy --- Sending request %s", request.url)
val response = chain.proceed(request)
val endTime = System.nanoTime()
logger.info(String.format("Bluelzy --- Received response from %s in %.1fms%n%s",
response.request.url, (endTime-startTime)/1e6, response.headers))
return response
}
}
運行程序然後打開Logcat
com.bluelzy.kotlinlearning I/TaskRunner: Bluelzy --- Sending request https://www.baidu.com/
com.bluelzy.kotlinlearning I/TaskRunner: Bluelzy --- Received response for https://www.baidu.com/ in 172.9ms
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Type: text/html
Date: Sat, 15 Feb 2020 12:41:37 GMT
Last-Modified: Mon, 23 Jan 2017 13:24:32 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
Transfer-Encoding: chunked
可以看到這裏把我們需要的信息都打印出來了。
思考一下:
- 這個logger攔截器是怎麼添加到OkHttp的呢?
- OkHttp如何運用責任鏈模式進行多個不同網絡層之間的責任劃分?
- 我們在實際開發中還能運用責任鏈模式做其他什麼操作嗎?
OkHttp中的責任鏈模式
自定義的攔截器是如何添加到OkHttp中的?
還記得上一篇文章說的嗎,我們構建一個OkHttpClient對象,使用的就是Builder模式,通過addInterceptor(interceptor: Interceptor) 這個方法,把攔截器加入到隊列中,這個攔截器隊列就是OkHttpClient中的一個全局變量,不僅用於存放我們自定義的攔截器,也用於存放OkHttp默認實現的攔截器。
fun addInterceptor(interceptor: Interceptor) = apply {
interceptors += interceptor
}
OkHttp的責任鏈模式是如何起作用的?
我們看RealCall裏的這段代碼:
@Throws(IOException::class)
fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
// 省略代碼
}
這裏做了幾件事:
- 把OkHttpClient裏面的Interceptor加入到列表中
- 把默認的幾個Interceptor加入到列表中
- 判斷是不是Socket請求,不是的話把networkInterceptor加入到列表中
- 最後加入CallServerInterceptor,這個攔截器是用來向服務器發起請求的,因此要加在最後
RetryAndFollowUpInterceptor
大家如果看了上一篇文章的話,應該還記得真正處理責任鏈的是RetryAndFollowUpInterceptor.proceed()
方法,通過index取出對應的攔截器並執行interceptor.intercept()
方法。而無論是我們自定義的Interceptor,還是OkHttp中默認實現的,都會繼承Interceptor這個接口,因此都會實現intercept()方法。
接下來,我們來看看RetryAndFollowUpInterceptor裏面做了什麼?
This interceptor recovers from failures and follows redirects as necessary. It may throw an [IOException] if the call was canceled. 這個攔截器主要是請求失敗時嘗試恢復連接,還有處理重定向的問題。 如果請求被取消了,可能會拋出IOException異常
既然主要是處理這兩個問題,那麼我們就重點關注看看,這個攔截器是如何處理的。
失敗時嘗試重連
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val realChain = chain as RealInterceptorChain
val transmitter = realChain.transmitter()
var followUpCount = 0
var priorResponse: Response? = null
while (true) {
transmitter.prepareToConnect(request) // 創建Stream
if (transmitter.isCanceled) {
throw IOException("Canceled") // 請求被取消時拋出異常
}
var response: Response
var success = false
try {
response = realChain.proceed(request, transmitter, null)
success = true
} catch (e: RouteException) {
// 路由異常,調用recover()
if (!recover(e.lastConnectException, transmitter, false, request)) {
throw e.firstConnectException
}
continue
} catch (e: IOException) {
// 服務端異常,調用recover()
val requestSendStarted = e !is ConnectionShutdownException
if (!recover(e, transmitter, requestSendStarted, request)) throw e
continue
} finally {
// The network call threw an exception. Release any resources.
if (!success) {
transmitter.exchangeDoneDueToException()
}
}
// 省略代碼
}
}
在發起請求的時候,如果出現了異常,根據不同異常會調用recover()方法,我們看看這個方法做了什麼
private fun recover(
e: IOException,
transmitter: Transmitter,
requestSendStarted: Boolean,
userRequest: Request
): Boolean {
// 判斷客戶端是否禁止重連,是的話直接返回false
if (!client.retryOnConnectionFailure) return false
// 如果已經發送了請求,那麼也直接返回false
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false
// 如果是Fatal類異常,返回false
if (!isRecoverable(e, requestSendStarted)) return false
// 如果沒有其他路由可以嘗試重連,返回false
if (!transmitter.canRetry()) return false
return true
}
- 如果我們在OkHttpClient設置了不能重連,game over
- 這個重連只能發生在連接失敗的時候,如果是請求已經發送了,game over
- 如果這個異常是協議相關的問題,那麼同樣game over
- OkHttp會在連接池裏面嘗試不同的IP,如果沒有其他可用的IP,game over
如果這個異常是在連接的時候發生的,而且還有可用的IP,我們也設置了可以重試(默認爲true),那麼就會再構造一個Request,用於重試。
這裏OkHttp用了一個很巧妙的方法來實現,那就是遞歸。
-
首先在intercept()方法開頭加入了while(true),只要沒有return或者throw excpetion,就會一直執行下去
-
response = realChain.proceed(request, transmitter, null)
-
通過上面這一句代碼,每次構建一個Request,然後調用proceed進行請求
-
每一個請求的結果都會返回到上一個Response中
-
每次請求完畢followUpCount 加一,在OkHttp中,這個參數用於控制請求最大數,默認是20,一旦超過這個數,也會拋出異常
如何進行重定向
重定向和重試其實都在intercept()方法中,主要是這句代碼:
val followUp = followUpRequest(response, route)
followUpRequeset():
@Throws(IOException::class)
private fun followUpRequest(userResponse: Response, route: Route?): Request? {
val responseCode = userResponse.code
val method = userResponse.request.method
when (responseCode) {
HTTP_PROXY_AUTH -> {
// 省略代碼
// 1.重定向 (307, 308)
HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT -> {
if (method != "GET" && method != "HEAD") {
return null
}
return buildRedirectRequest(userResponse, method)
}
// 2.重定向(300,301,302,303)
HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
return buildRedirectRequest(userResponse, method)
}
HTTP_CLIENT_TIMEOUT -> {
// 省略代碼
return userResponse.request
}
HTTP_UNAVAILABLE -> {
// 省略代碼
return null
}
else -> return null
}
}
可以看到,這裏通過判斷responseCode
來進行不同的處理,我們重點關注重定向,看看buildRedirectRequest()
方法:
private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
// 1.客戶端是否支持重定向
if (!client.followRedirects) return null
val location = userResponse.header("Location") ?: return null
// 2.是不是http/https協議,不是的話拋異常,返回null
val url = userResponse.request.url.resolve(location) ?: return null
// 3.是否支持Ssl重定向
val sameScheme = url.scheme == userResponse.request.url.scheme
if (!sameScheme && !client.followSslRedirects) return null
// 4.構建請求體
val requestBuilder = userResponse.request.newBuilder()
if (HttpMethod.permitsRequestBody(method)) {
val maintainBody = HttpMethod.redirectsWithBody(method)
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null)
} else {
val requestBody = if (maintainBody) userResponse.request.body else null
requestBuilder.method(method, requestBody)
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding")
requestBuilder.removeHeader("Content-Length")
requestBuilder.removeHeader("Content-Type")
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!userResponse.request.url.canReuseConnectionFor(url)) {
requestBuilder.removeHeader("Authorization")
}
return requestBuilder.url(url).build()
}
這裏就是一個很標準的Builder模式的應用了,通過request.newBuilder()
和後續的builder.xxx()
構建一個Request.Builder對象。最終調用build()
方法返回Request對象。
總結
在RetryAndFollowUpInterceptor中,主要做了兩件事
- 重試
- 重定向
這兩者都是通過followUpRequest()
方法來構建一個新的Request,然後遞歸調用proceed()方法進行請求。中間有很多的錯誤/異常判斷,一旦條件不滿足就會停止請求並且釋放資源。
我們在實際工作中如何使用?
例如,我們現在不想使用OkHttp默認的重試攔截器,希望自己定義重試次數,那麼可以這樣寫:
RetryInterceptor:
/**
* author : BlueLzy
* e-mail : [email protected]
* date : 2020/02/16 16:07
* desc : 重試攔截器
*/
class RetryInterceptor(private val maxRetryCount: Int = 2) : Interceptor {
private var retryNum = 0
override fun intercept(chain: Interceptor.Chain): Response =
chain.proceed(chain.request()).apply {
while (!isSuccessful && retryNum < maxRetryCount) {
retryNum++
logger.info("BlueLzy --- 重試次數:$retryNum")
chain.proceed(request)
}
}
}
爲了能測試重試攔截器,我們定義一個測試請求,每次返回400
TestInterceptor:
/**
* author : BlueLzy
* e-mail : [email protected]
* date : 2020/02/16 16:18
* desc :
*/
class TestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return if (request.url.toString() == TEST_URL) {
val responseString = "我是響應數據"
Response.Builder()
.code(400)
.request(request)
.protocol(Protocol.HTTP_1_1)
.message(responseString)
.body(responseString.toResponseBody("application/json".toMediaTypeOrNull()))
.addHeader("content-type", "application/json")
.build()
} else {
chain.proceed(request)
}
}
}
最後發起請求
OkHttpTest:
class OkHttpTest {
internal fun test() {
Request.Builder()
.url(TEST_URL).build().let { request ->
OkHttpClient.Builder()
.addInterceptor(RetryInterceptor()) // 添加重試攔截器,默認重試次數爲2
.addInterceptor(TestInterceptor()) // 添加測試請求
.build()
.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("BlueLzy --- ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
println("BlueLzy --- ${response.message}")
}
})
}
}
companion object {
const val TEST_URL = "https://www.baidu.com/"
}
}
我們可以看到logcat打印出來的信息:
2020-02-16 16:46:29.338 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- Sending request https://www.baidu.com/
2020-02-16 16:46:29.338 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- 重試次數:1
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- 重試次數:2
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/TaskRunner: BlueLzy --- Received response for https://www.baidu.com/ in 1.0ms
content-type: application/json
2020-02-16 16:46:29.339 1914-2113/com.bluelzy.kotlinlearning I/System.out: BlueLzy --- 我是響應數據
發起了1次默認請求+2次重試的請求 = 3次請求。這樣就實現了自由控制重試次數的需求了,當然我們也可以在每次重試中做其他的操作,例如更改請求頭和請求體。
總結
本文我們主要說了OkHttp中的重試機制和重定向,分析了RetryAndFollowUpinterceptor的源碼。最後舉了個在實際工作中應用的小例子。
下一篇我們說一下OkHttp中的緩存機制 - CacheInterceptor
如有不對之處,歡迎大家多多交流。感謝閱讀!