分發(fā)器的邏輯執(zhí)行完成就會進(jìn)入攔截器了天揖,OkHttp使用了攔截器模式來處理一個請求從發(fā)起到響應(yīng)的過程烦粒。
代碼還是從我們上一篇提到的getResponseWithInterceptorChain開始
@Override
public Response execute() throws IOException {
...
try {
...
// 發(fā)起請求
Response result = getResponseWithInterceptorChain();
...
return result;
} catch (IOException e) {
} finally {
}
}
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) {
//自定義攔截器加入到集合,(和上邊client.interceptors()的區(qū)別僅在于添加的順序)
//但是不同的順序也會產(chǎn)生不同的效果承匣,具體可參考下
//https://segmentfault.com/a/1190000013164260
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);
}
addInterceptor 與 addNetworkInterceptor 的區(qū)別婿着?
1.添加順序不同
應(yīng)用攔截器是最先執(zhí)行的攔截器溶耘,也就是用戶自己設(shè)置request屬性后的原始請求通惫,第一個被添加的,而網(wǎng)絡(luò)攔截器位于ConnectInterceptor和CallServerInterceptor之間驰吓,此時網(wǎng)絡(luò)鏈路已經(jīng)準(zhǔn)備好涧尿,只等待發(fā)送請求數(shù)據(jù)
2.可能執(zhí)行的次數(shù)不同
網(wǎng)絡(luò)攔截器(addNetworkInterceptor)可能執(zhí)行多次(如果發(fā)生了錯誤重試或者網(wǎng)絡(luò)重定向),也可能一次都沒有執(zhí)行(如果在CacheInterceptor中命中了緩存)檬贰;而addInterceptor只會執(zhí)行一次姑廉,因?yàn)樗窃谡埱蟀l(fā)起之前最先執(zhí)行的(在RetryAndFollowUpInterceptor之前)
3.應(yīng)用場景不同
應(yīng)用攔截器因?yàn)橹徽{(diào)用一次,可用于統(tǒng)計(jì)客戶端發(fā)起的次數(shù)翁涤,而網(wǎng)絡(luò)攔截器一次調(diào)用代表了一定會發(fā)起一次網(wǎng)絡(luò)通信桥言,因此通常可用于統(tǒng)計(jì)網(wǎng)絡(luò)鏈路上傳輸?shù)臄?shù)據(jù)葵礼。
可以看到OkHttp內(nèi)部默認(rèn)存在五大攔截器号阿,而今天這篇要講的就是retryAndFollowUpInterceptor,這個攔截器在RealCall被new出來時已經(jīng)創(chuàng)建了鸳粉,從他的名字就可以看出來扔涧,他負(fù)責(zé)的是失敗重試和重定向的邏輯處理。
失敗重試
從這個攔截器的intercept方法中可以看出,雖然這個攔截器是第一個被執(zhí)行的枯夜,但是其實(shí)他真正的重試和重定向操作是在請求被響應(yīng)之后才做的處理.
@Override
public Response intercept(Chain chain) throws IOException {
...
while (true) {
...
try {
//請求出現(xiàn)了異常弯汰,那么releaseConnection依舊為true。
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//路由異常卤档,連接未成功蝙泼,請求還沒發(fā)出去
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
//請求發(fā)出去了,但是和服務(wù)器通信失敗了劝枣。(socket流正在讀寫數(shù)據(jù)的時候斷開連接)
// HTTP2才會拋出ConnectionShutdownException。所以對于HTTP1 requestSendStarted一定是true
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
...
}
...
}
}
可以看到被處理的exception只有RouteException和IOException织鲸,RouteException是路由異常舔腾,連接未成功,請求還沒發(fā)出去搂擦,所以recover方法中第三個參數(shù)直接傳的false稳诚,表示請求還沒有開始;而IOException是請求發(fā)出去了瀑踢,但是和服務(wù)器通信失敗了扳还,所以所以recover方法中第三個參數(shù)值取決于
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
HTTP2才會拋出ConnectionShutdownException。所以對于HTTP1 requestSendStarted一定是true橱夭。
從上面的代碼可以看出氨距,realChain.proceed是后續(xù)的責(zé)任鏈執(zhí)行的邏輯,如果這些執(zhí)行發(fā)生了異常棘劣,在RetryAndFollowUpInterceptor會被捕獲俏让,然后通過recover方法判斷當(dāng)前異常是否滿足重試的條件(并不是所有失敗都會被重試),如果滿足茬暇,則continue首昔,再進(jìn)行一次,這個操作是在while循環(huán)中進(jìn)行的糙俗,也就是只要滿足重試的條件勒奇,可以進(jìn)行無數(shù)次的重試,但事實(shí)上巧骚,由于重試的條件比較苛刻赊颠,一般也不會被多次重試。那么這個重試的條件
究竟有哪些呢网缝?
重試條件
進(jìn)入recover方法
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
//調(diào)用方在OkhttpClient初始化時設(shè)置了不允許重試(默認(rèn)允許)
if (!client.retryOnConnectionFailure()) return false;
//RouteException不用判斷這個條件巨税,
//當(dāng)是IOException時,由于requestSendStarted只在http2的io異常中可能為false粉臊,所以主要是第二個條件草添,body是UnrepeatableRequestBody則不必重試
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
return false;
//對異常類型進(jìn)行判斷
if (!isRecoverable(e, requestSendStarted)) return false;
//不存在更多的路由也沒辦法重試
if (!streamAllocation.hasMoreRoutes()) return false;
//以上條件都允許了,才能重試
return true;
}
進(jìn)入isRecoverable方法
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// 協(xié)議異常扼仲,那么重試幾次都是一樣的
if (e instanceof ProtocolException) {
return false;
}
// 請求超時導(dǎo)致的中斷远寸,可以重試
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
//證書不正確 可能證書格式損壞 有問題
if (e instanceof SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//證書校驗(yàn)失敗 不匹配
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
return true;
}
總結(jié)一下:
1抄淑、協(xié)議異常,如果是那么直接判定不能重試;(你的請求或者服務(wù)器的響應(yīng)本身就存在問題驰后,沒有按照http協(xié)議來 定義數(shù)據(jù)肆资,再重試也沒用)
2、超時異常灶芝,可能由于網(wǎng)絡(luò)波動造成了Socket連接的超時郑原,可以使用不同路線重試。
3夜涕、SSL證書異常/SSL驗(yàn)證失敗異常犯犁,前者是證書驗(yàn)證失敗,后者可能就是壓根就沒證書
所以說要滿足重試的條件還是比較苛刻的女器。
重定向
OkHttp支持重定向請求酸役,見followUpRequest方法,主要是對響應(yīng)頭的一些判斷
private Request followUpRequest(Response userResponse, Route route) throws IOException {
//重定向的判斷必須在服務(wù)器有返回的情況下驾胆,否則拋出異常
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
//407 客戶端使用了HTTP代理服務(wù)器涣澡,在請求頭中添加 “Proxy-Authorization”,讓代理服務(wù)器授權(quán) @a
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not " +
"using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
//401 需要身份驗(yàn)證 有些服務(wù)器接口需要驗(yàn)證使用者身份 在請求頭中添加 “Authorization” @b
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
// 308 永久重定向
// 307 臨時重定向
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// 如果請求方式不是GET或者HEAD丧诺,框架不會自動重定向請求
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// 300 301 302 303
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// 如果設(shè)置了不允許重定向入桂,那就返回null
if (!client.followRedirects()) return null;
// 從響應(yīng)頭取出location
String location = userResponse.header("Location");
if (location == null) return null;
// 根據(jù)location 配置新的請求
HttpUrl url = userResponse.request().url().resolve(location);
// 如果為null,說明協(xié)議有問題锅必,取不出來HttpUrl事格,那就返回null,不進(jìn)行重定向
if (url == null) return null;
// 如果重定向在http到https之間切換搞隐,需要檢查用戶是不是允許(默認(rèn)允許)
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
//重定向請求中 只要不是 PROPFIND 請求驹愚,無論是POST還是其他的方
//法都要改為GET請求方式, * 即只有 PROPFIND 請求才能有請求體
//請求不是get與head HttpMethod.permitsRequestBody ===> return !(method.equals("GET") || method.equals("HEAD"));
//HttpMethod.permitsRequestBody ===> return method.equals("PROPFIND");
//HttpMethod.permitsRequestBody ===> return !method.equals("PROPFIND");
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
// 除了 PROPFIND 請求之外都改成GET請求
//HttpMethod.redirectsToGet ===> return !method.equals("PROPFIND");
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() :
null;
requestBuilder.method(method, requestBody);
}
// 不是 PROPFIND 的請求(不包含請求體的請求)劣纲,把請求頭中關(guān)于請求體的數(shù)據(jù)刪掉
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// 在跨主機(jī)重定向時逢捺,刪除身份驗(yàn)證請求頭
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 客戶端請求超時
case HTTP_CLIENT_TIMEOUT:
// 408 算是連接失敗了,所以判斷用戶是不是允許重試
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
return null;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
// 如果是本身這次的響應(yīng)就是重新請求的產(chǎn)物同時上一次之所以重請求還是因?yàn)?08癞季,那我們這次不再重請求 了
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null;
}
// 如果服務(wù)器告訴我們了 Retry-After 多久后重試劫瞳,那框架不管了。
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 服務(wù)不可用 和408差不多绷柒,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重請求
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
return null;
}
}
@a:在OkHttpClient Builder構(gòu)建的時候可以設(shè)置志于,對應(yīng)HTTP_PROXY_AUTH響應(yīng)頭
public Builder proxyAuthenticator(Authenticator proxyAuthenticator) {
if (proxyAuthenticator == null)
throw new NullPointerException("proxyAuthenticator == null");
this.proxyAuthenticator = proxyAuthenticator;
return this;
}
@b:在OkHttpClient Builder構(gòu)建的時候可以設(shè)置,對應(yīng)HTTP_UNAUTHORIZED響應(yīng)頭
public Builder authenticator(Authenticator authenticator) {
if (authenticator == null) throw new NullPointerException("authenticator == null");
this.authenticator = authenticator;
return this;
}
整個是否需要重定向的判斷內(nèi)容很多废睦,關(guān)鍵在于理解他們的意思伺绽。如果此方法返回空,那就表 示不需要再重定向了,直接返回響應(yīng);但是如果返回非空奈应,那就要重新請求返回的 Request 澜掩,但是需要注意的是, 我們的 followup 在攔截器中定義的最大次數(shù)為20次杖挣。
總結(jié)
RetryAndFollowUpInterceptor攔截器是整個責(zé)任鏈中的第一個肩榕,這意味著它會是首次接觸到 Request 與最后接收到 Response 的角色,在這個 攔截器中主要功能就是判斷是否需要重試與重定向惩妇。
重試的前提是出現(xiàn)了 RouteException 或者 IOException 株汉。一但在后續(xù)的攔截器執(zhí)行過程中出現(xiàn)這兩個異常,就會 通過 recover 方法進(jìn)行判斷是否進(jìn)行連接重試屿附。
重定向發(fā)生在重試的判定之后郎逃,如果不滿足重試的條件,還需要進(jìn)一步調(diào)用 followUpRequest 根據(jù) Response 的響 應(yīng)碼(當(dāng)然挺份,如果直接請求失敗, Response 都不存在就會拋出異常)贮懈。 followup 最大發(fā)生20次匀泊。